You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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/main275d7df13).
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:
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:
create a view in the console — lands as an invisible per-item draft. listViewOverrides reads published rows, so the map is still correct here.
Publish — the row becomes published, and the batch override map is now genuinely stale.
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.
#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:
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.
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.
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.
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 inpackages/app-shell, on a different writer. Filed under PD #10.Measured on the #4363 branch (worktree at
origin/main275d7df13).What
#4363 fixed the four write paths on
ObjectStackAdapterso each invalidates both view-shaped cache keys (view:{object}:{viewId}andview-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:createRuntimeMetadata(packages/app-shell/src/views/runtime-metadata-persistence.ts:100) callsctx.metadataClient.save(type, name, body, { mode: 'draft' })and invalidates nothing. Publish is the same shape —publishRuntimeMetadata(:240) is a barectx.metadataClient.publish(type, name), reached fromRuntimeDraftBar.tsx:118.So the sequence a user actually performs:
listViewOverridesreads published rows, so the map is still correct here.view-overrides:{object}, so the map keeps answering from its pre-publish snapshot for the rest ofMetadataCache'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-viewgetViewfallback 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:
grepforcreateView(finds no production caller ofObjectStackAdapter.createViewanywhere in the repo — it is a publishedDataSourcesurface (packages/types/src/data.ts:524) for external consumers, while the console writes throughmetadataClient. Both need to invalidate; only one now does.Dormant sibling, recorded not filed separately
MetadataService.saveMetadataItem/deleteMetadataItem(packages/app-shell/src/services/MetadataService.ts:140and:150) invalidate${category}:${name}. That is the right key for the generic metadata read (data-objectstack/src/index.ts:3739), but forcategory === '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:
createRuntimeMetadata) invalidate the adapter's two view keys — needs the adapter reachable from those call sites, which today they do not hold.MetadataService— through it. Fixes the dormant sibling for free.listViewOverridesnot be cached at all, and letlistViews-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 (
handleViewCreatetocreateRuntimeMetadatatometadataClient.save, andRuntimeDraftBartopublishRuntimeMetadatatometadataClient.publish), the absence of anyinvalidateon those paths, and the TTL. Whether a user notices depends on whether they revisit the object within the TTL window.Related
view-overrides:{object}is only invalidated by updateViewConfig — createView / updateView / deleteView leave the batch map stale for the 5-minute TTL #4363 — the same defect onObjectStackAdapter's own write paths (fixed); this is the writer it does not reachmeta/{对象名}(把对象名当 metadata type),写却落在type='view'—— 两边键空间不相交,已保存的视图个性化永远读不回来 #3774 — madelistViewOverridesauthoritative, which removes the accidental self-healingviews:{objectName}缓存键被 4 处 invalidate,却从没有任何读路径写入过 —— listViews 实为无缓存,那 4 行是惰性代码 #3778 / sweep sweep: dead-surface deletions batch 3 — 3 zero-consumer items, one claim, one PR (findings-triage 2026-08-11) #4328 — the deadviews:{object}invalidations, whose removal made this family visibleGenerated by Claude Code