Adjacent finding, surfaced while removing the dead views:{object} invalidations for #3778 (sweep #4328). Not fixed there — that PR is a pure dead-code removal, and this is a behavioural gap that predates it. Filed under PD #10.
Measured on origin/main @ 7e4f0e530.
What
ObjectStackAdapter caches exactly two view-shaped reads:
| reader |
cache key |
line |
getView |
view:{object}:{viewId} |
packages/data-objectstack/src/index.ts:2969 |
listViewOverrides |
view-overrides:{object} |
packages/data-objectstack/src/index.ts:2931 |
Four write paths touch view rows. Only one of them invalidates the batch map:
| write path |
invalidates |
view-overrides:{object}? |
updateViewConfig |
view:{object}:{viewId}, view-overrides:{object} |
yes |
createView |
(nothing) |
no |
updateView (draft and published halves) |
view:{object}:{viewName} |
no |
deleteView |
view:{object}:{viewName} |
no |
MetadataCache's default TTL is 5 minutes (packages/data-objectstack/src/cache/MetadataCache.ts:90), and the adapter is long-lived, so the stale window is up to 5 minutes of real use rather than one navigation.
Why the stale map is not self-correcting
loadViewOverrides (packages/app-shell/src/views/ObjectView.tsx:283) treats a resolved batch answer as authoritative and deliberately does not re-probe per view — that is #3774's fix, and it is correct:
RESOLVES (including to an empty map) → authoritative. […] do not "helpfully" re-probe per view on an empty map, that reinstates the 404 flurry it removes.
So when the cached map is stale, the per-view getView fallback is by design unreachable. listViews is uncached and answers fresh, which means the switcher can show a view whose override body came from a map written up to five minutes earlier — the two reads disagree, and the fresher one is not the one that supplies the override.
The sharpest shape is updateView: it is the rename/patch path (#4139), it invalidates the per-view key it just wrote, and it leaves the batch map holding the pre-edit body. A user who edits a view and returns to the object can be served the old override until the TTL expires.
Not claimed
No browser repro was run — this is a read of the invalidation table against the two cache keys plus the TTL, not a reproduction. What is measured is the asymmetry (one of four write paths invalidates the map) and the fact that the fallback which would mask it is intentionally disabled.
Direction (left to triage)
- Have
createView / updateView / deleteView invalidate view-overrides:{object} as updateViewConfig already does — smallest change, keeps the batch read authoritative.
- Give the four paths one shared "view rows for this object changed" invalidation helper, so the next write path cannot forget a key by omission.
Option 2 is the shape that stops this recurring, since the defect here is precisely that the key list is restated per call site.
Related
Adjacent finding, surfaced while removing the dead
views:{object}invalidations for #3778 (sweep #4328). Not fixed there — that PR is a pure dead-code removal, and this is a behavioural gap that predates it. Filed under PD #10.Measured on
origin/main@7e4f0e530.What
ObjectStackAdaptercaches exactly two view-shaped reads:getViewview:{object}:{viewId}packages/data-objectstack/src/index.ts:2969listViewOverridesview-overrides:{object}packages/data-objectstack/src/index.ts:2931Four write paths touch view rows. Only one of them invalidates the batch map:
view-overrides:{object}?updateViewConfigview:{object}:{viewId},view-overrides:{object}createViewupdateView(draft and published halves)view:{object}:{viewName}deleteViewview:{object}:{viewName}MetadataCache's default TTL is 5 minutes (packages/data-objectstack/src/cache/MetadataCache.ts:90), and the adapter is long-lived, so the stale window is up to 5 minutes of real use rather than one navigation.Why the stale map is not self-correcting
loadViewOverrides(packages/app-shell/src/views/ObjectView.tsx:283) treats a resolved batch answer as authoritative and deliberately does not re-probe per view — that is #3774's fix, and it is correct:So when the cached map is stale, the per-view
getViewfallback is by design unreachable.listViewsis uncached and answers fresh, which means the switcher can show a view whose override body came from a map written up to five minutes earlier — the two reads disagree, and the fresher one is not the one that supplies the override.The sharpest shape is
updateView: it is the rename/patch path (#4139), it invalidates the per-view key it just wrote, and it leaves the batch map holding the pre-edit body. A user who edits a view and returns to the object can be served the old override until the TTL expires.Not claimed
No browser repro was run — this is a read of the invalidation table against the two cache keys plus the TTL, not a reproduction. What is measured is the asymmetry (one of four write paths invalidates the map) and the fact that the fallback which would mask it is intentionally disabled.
Direction (left to triage)
createView/updateView/deleteViewinvalidateview-overrides:{object}asupdateViewConfigalready does — smallest change, keeps the batch read authoritative.Option 2 is the shape that stops this recurring, since the defect here is precisely that the key list is restated per call site.
Related
views:{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; this is what remained visible once they were gonemeta/{对象名}(把对象名当 metadata type),写却落在type='view'—— 两边键空间不相交,已保存的视图个性化永远读不回来 #3774 — madelistViewOverridesauthoritative (and its empty map trustworthy), which is what removes the accidental self-healingupdateView's draft/published addressing