Skip to content

app-shell: publishing a draft view does not invalidate the adapter's view caches — the console's real create-view flow bypasses ObjectStackAdapter entirely #4373

Closed

Description

@yinlianghui

Adjacent finding, surfaced while fixing #4363 (the adapter's own write paths). Not fixed there — that PR's scope is packages/data-objectstack/src/index.ts, and this defect lives in packages/app-shell, on a different writer. Filed under PD #10.

Measured on the #4363 branch (worktree at origin/main 275d7df13).

What

#4363 fixed the four write paths on ObjectStackAdapter so each invalidates both view-shaped cache keys (view:{object}:{viewId} and view-overrides:{object}). But the console's actual create-a-view flow never calls any of them.

ObjectView.handleViewCreate (packages/app-shell/src/views/ObjectView.tsx:626) writes through the ADR-0034 metadata seam, not the DataSource:

createdId = await createRuntimeMetadata('view', env.name, env, { metadataClient });

createRuntimeMetadata (packages/app-shell/src/views/runtime-metadata-persistence.ts:100) calls ctx.metadataClient.save(type, name, body, { mode: 'draft' }) and invalidates nothing. Publish is the same shape — publishRuntimeMetadata (:240) is a bare ctx.metadataClient.publish(type, name), reached from RuntimeDraftBar.tsx:118.

So the sequence a user actually performs:

  1. create a view in the console — lands as an invisible per-item draft. listViewOverrides reads published rows, so the map is still correct here.
  2. Publish — the row becomes published, and the batch override map is now genuinely stale.
  3. nothing invalidates view-overrides:{object}, so the map keeps answering from its pre-publish snapshot for the rest of MetadataCache's default 5-minute TTL (packages/data-objectstack/src/cache/MetadataCache.ts:90).

The staleness does not self-heal, for the same reason #4363 gave: loadViewOverrides (ObjectView.tsx:283) treats a RESOLVED map as authoritative and deliberately does not re-probe per view (#3774), so the per-view getView fallback that would mask it is by design unreachable.

Why #4363 does not cover it

#4363 makes the adapter correct at its own door. This is a second door into the same rows. The two writers are disjoint: grep for createView( finds no production caller of ObjectStackAdapter.createView anywhere in the repo — it is a published DataSource surface (packages/types/src/data.ts:524) for external consumers, while the console writes through metadataClient. Both need to invalidate; only one now does.

Dormant sibling, recorded not filed separately

MetadataService.saveMetadataItem / deleteMetadataItem (packages/app-shell/src/services/MetadataService.ts:140 and :150) invalidate ${category}:${name}. That is the right key for the generic metadata read (data-objectstack/src/index.ts:3739), but for category === 'view' it names neither view key, so the same gap would open. No caller passes 'view' today — in fact these two methods have no callers outside their own class — so this is observation-class, not a live defect. Worth fixing in the same pass if the direction below is taken.

Direction (left to triage)

This is the recurrence #4363's own "Direction 2" predicted: the key list is restated per call site, so each new writer forgets it independently. Options:

  1. Have the seam's publish path (and createRuntimeMetadata) invalidate the adapter's two view keys — needs the adapter reachable from those call sites, which today they do not hold.
  2. Give the adapter one exported "view rows for this object changed" invalidation entry point and route every writer — adapter methods, seam, MetadataService — through it. Fixes the dormant sibling for free.
  3. Have listViewOverrides not be cached at all, and let listViews-style freshness apply. Cheapest to reason about; costs the batch read's whole point (data-objectstack 的 listViewOverrides 读的是 meta/{对象名}(把对象名当 metadata type),写却落在 type='view' —— 两边键空间不相交,已保存的视图个性化永远读不回来 #3774 introduced it to remove a 404 flurry), so probably wrong.

Not claimed

No browser repro was run. What is measured is the call graph (handleViewCreate to createRuntimeMetadata to metadataClient.save, and RuntimeDraftBar to publishRuntimeMetadata to metadataClient.publish), the absence of any invalidate on those paths, and the TTL. Whether a user notices depends on whether they revisit the object within the TTL window.

Related


Generated by Claude Code

Activity

  1. yinlianghui commented on Aug 11, 2026

    @yinlianghui
    CollaboratorAuthor

    Triage + PM ruling (delegated authority, session session_017Qqyix2QcnpUC9XeYVDzx3; veto window open as always): finding → pm:queue, and the fix direction is decided now so the eventual dispatch doesn't re-litigate it.

    Ruling: one exported invalidation entry point, routed by every writer — the option B recorded in PR #4374's report, which is also this repo's standing derivation-over-enumeration doctrine applied to cache keys:

    1. ObjectStackAdapter exports a single "view rows for this object changed" invalidation seam (shape at implementer's discretion, e.g. invalidateViewKeys(objectName, viewName?)) — the ONE place that knows the key set is view:{object}:{name} + view-overrides:{object}.
    2. The adapter's four write paths (createView/updateView both halves/deleteView/updateViewConfig) route through it — deleting the four restated key lists PR fix(data-objectstack): every view write path invalidates the override map (#4363) #4374 just fixed. Their pins keep passing unchanged; that is the acceptance evidence the refactor changed nothing.
    3. The defect proper: app-shell's real create/publish flow (handleViewCreate → createRuntimeMetadata → metadataClient.save, and Publish via RuntimeDraftBar → publishRuntimeMetadata) calls the seam when the saved/published item is a view — closing the user-reachable staleness this card measures. Red-first test on that flow per the The report config panel is titled "Title" and the empty state says "Object Not Found" — call sites asking for a key whose value is not the copy for that slot #4118 guard convention.
    4. The dormant sibling (MetadataService.saveMetadataItem/deleteMetadataItem invalidating a reader-less view:{name} if ever passed view) is wired through the same seam or explicitly documented as unreachable — implementer measures which is honest.

    Why B and not leaving four restated lists: this is the SECOND time the repo has paid for per-site restatement of this key set (#3778 removed five copies of a wrong key; #4363 fixed four copies of an incomplete one), and #4373 itself is the measured proof that a new writer forgets the keys by default. A pin suite can only guard writers that exist; a mandatory seam makes the next writer structurally unable to forget.

    Dispatch gate: cross-package (data-objectstack + app-shell). Wait for BOTH: PR #4374 landing (auto-merge armed) and #4040 tranche 5 landing (app-shell surface currently held). Not before.


    Generated by Claude Code

  2. self-assigned this
    on Aug 12, 2026
  3. yinlianghui commented on Aug 12, 2026

    @yinlianghui
    CollaboratorAuthor

    CLAIM — PM seat session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4373-view-invalidation-seam, one dev agent, one PR, Fixes #4373.

    Dispatch gate status: PR #4374 (adapter half) is merged; PR #4379 (tranche 5, which held the app-shell surface) is ready + auto-merge-armed with converged CI — the dev is instructed to verify its merge commit is in origin/main before creating the worktree. The ruling recorded in my triage comment above is binding and unchanged: one exported invalidation seam, four adapter paths route through it (their pins keep passing unchanged as the refactor's acceptance evidence), app-shell's save/publish flow calls it when the item is a view (red-first test per #4118), the MetadataService dormant sibling wired-or-documented as measured.


    Generated by Claude Code

  4. added 2 commits that reference this issue on Aug 17, 2026
    d2f6e6b
    2a40f69
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions