Skip to content

fix(data-objectstack): every view write path invalidates the override map (#4363) - #4374

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4363-view-overrides-invalidation
Aug 11, 2026
Merged

fix(data-objectstack): every view write path invalidates the override map (#4363)#4374
yinlianghui merged 1 commit into
mainfrom
claude/issue-4363-view-overrides-invalidation

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4363

Built on post-#4366 main: branched from origin/main at 275d7df13, which is downstream of #4366's squash d9d346307 (dead-surface batch 3). This PR extends that sweep's viewCacheInvalidation.pin.test.ts rather than re-creating it, and lands in the slot #4366 deliberately left open — its createView case asserted "no views: key" rather than "invalidates nothing", precisely so this card could answer the override-map question without fighting a frozen pin.

The defect

ObjectStackAdapter caches two view-shaped reads. Four write paths touch view rows. Before this PR only one of them invalidated the batch map:

write path per-view key (getView) override map (listViewOverrides)
updateViewConfig yes yes
createView no — invalidated nothing at all no
updateView (draft half) yes no
updateView (published half) yes no
deleteView yes no

MetadataCache's default TTL is 5 minutes (cache/MetadataCache.ts:90) and the adapter is long-lived, so the stale window is minutes of real use, not one navigation.

It does not self-heal. loadViewOverrides (app-shell/src/views/ObjectView.tsx:283) treats a RESOLVED map as authoritative and deliberately does not re-probe per view — that is #3774's fix and it is correct, since re-probing reinstates the 404 flurry the batch read exists to remove. So the per-view getView fallback that would have masked a stale map is by design unreachable, and the stale map is served in full. Beside it, listViews is uncached and answers fresh: the switcher can list a view whose override body came from a map written minutes earlier, and the fresher of the two reads is not the one supplying the override.

The fix

All four paths now emit the same ordered pair — the per-view key, then the object's override map. The rule is uniform per method, not per branch.

objectName is spelled exactly as updateViewConfig already spells it: the method's own first parameter, interpolated directly. No new derivation was introduced — viewItemObjectName() (#3774's converged reader) is for narrowing items read back, and every one of these write paths already receives the object name as an argument, so there was nothing to parse.

Two choices in the diff that are decisions rather than mechanics, both recorded in the code and pinned:

  • updateView's draft half invalidates both keys, like its published half. This is deliberate over-invalidation: both readers enumerate PUBLISHED rows, so a draft write stales neither — exactly as was already true of the per-view line it now pairs with. The costs are not symmetric (a spare invalidation costs one refetch; a missed one costs the whole TTL), and "which half am I in?" is not a question a future edit to this method should have to re-answer.
  • createView names the per-view key too, not just the map. saveItem is an upsert, so an explicit spec.name that already exists overwrites a published row a prior getView may hold cached. On the ordinary create-a-new-row path the extra call is a Map.delete on an absent key — measured, not assumed: MetadataCache.get stores only on fulfilment (const data = await fetcher(); this.set(key, data)), so there is no negative caching for a generated name to collide with.

Tests

Extends #4366's pin suite from 6 cases to 8, keeping its two sweep pins as untouched controls.

pnpm exec vitest run packages/data-objectstack/
  →  Test Files  30 passed (30) | Tests 426 passed (426)

pnpm exec vitest run packages/data-objectstack/ packages/app-shell/   (consumer sweep)
  →  Test Files 380 passed (380) | Tests 3765 passed | 1 skipped (3766)

pnpm exec vitest run apps/console/
  →  Test Files  45 passed (45) | Tests 512 passed (512)

pnpm --filter @object-ui/data-objectstack type-check   →  clean
pnpm --filter @object-ui/data-objectstack lint         →  0 errors (363 pre-existing `any` warnings)
node scripts/check-changeset-presence.mjs  ✅ 2 source files of 1 released package, 1 changeset
node scripts/check-changeset-no-major.mjs  ✅
node scripts/check-control-bytes.mjs       ✅ 4082 tracked text files

Consumer sweep direction stated explicitly: prefix filter ...@object-ui/data-objectstack = the 34 downstream consumers; the two that call these paths are @object-ui/app-shell (ObjectView drives updateView for rename/pin, deleteView, updateViewConfig, and reads listViewOverrides) and @object-ui/console. Both are in the runs above. Repo-root vitest with path filters per AGENTS.md §测试纪律 — never pnpm --filter test, which would silently run someone else's package.

Reverse verification

Fix committed first, then removed with git checkout and restored from the commit — never git stash. Both directions predicted before running.

A — remove the fix, keep the new pin. Predicted: the four key-set cases red on a missing view-overrides:account; all four controls green. Observed exactly that (4 failed | 4 passed):

× createView invalidates the per-view key and the override map (#4363)
    AssertionError: expected [] to deeply equal [ 'view:account:account.mine', …(1) ]
× updateView (published overlay) invalidates both keys (#4363)
    AssertionError: expected [ 'view:account:v1' ] to deeply equal [ 'view:account:v1', …(1) ]
× updateView (pending draft) invalidates both keys (#4363)
    AssertionError: expected [ 'view:account:account.all' ] to deeply equal [ 'view:account:account.all', …(1) ]
× deleteView invalidates both keys (#4363)
    AssertionError: expected [ 'view:account:v1' ] to deeply equal [ 'view:account:v1', …(1) ]

createView's expected [] is the issue's table confirmed at the byte level: that path invalidated nothing whatsoever.

B — keep the fix, restore #4366's ORIGINAL pin file. The mirror check, and the one that proves the sweep's pins are undisturbed. Predicted: the three "…invalidates the getView key only" cases red on an extra key, while listViews, updateViewConfig and the no views: key control stay green. Observed exactly that (3 failed | 3 passed):

× updateView (published overlay) invalidates the getView key only
    AssertionError: expected [ 'view:account:v1', …(1) ] to deeply equal [ 'view:account:v1' ]
+   "view-overrides:account",

The no views: key pin staying green in direction B is the load-bearing control: createView now emits two keys where it previously emitted none, and neither is a views: key, so #3778's deletion is still pinned dead by an assertion this PR did not touch. Only that case's comment was updated, since "so it now invalidates nothing" had become false.

Pin suite shape

New case listViewOverrides reads back under the key the write paths invalidate asserts the reader/invalidator pairing rather than a bare string — the views: mistake was a key with no reader, and one rename away it could recur. The remaining new cases assert full ordered key sets per path.

Out of scope, filed as its own card

#4373 — the console's real create-view flow never calls ObjectStackAdapter.createView at all: handleViewCreate writes through the ADR-0034 seam (createRuntimeMetadatametadataClient.save), and Publish (RuntimeDraftBarpublishRuntimeMetadata) invalidates nothing, so publishing a draft view leaves the same map stale. This PR makes the adapter correct at its own door; that is a second door into the same rows, in packages/app-shell, outside this card's scope. The dormant sibling (MetadataService.saveMetadataItem would invalidate view:{name}, a key with no reader, if anything ever passed 'view' — nothing does) is recorded there rather than filed separately.

Related


Generated by Claude Code

… map (#4363)

`ObjectStackAdapter` caches two view-shaped reads — `getView` under
`view:{object}:{viewId}` and `listViewOverrides` under
`view-overrides:{object}`. Of the four write paths that touch view rows, only
`updateViewConfig` invalidated the second key, so `createView` / `updateView` /
`deleteView` left the batch override map stale for `MetadataCache`'s default
5-minute TTL.

The gap does not self-heal: `loadViewOverrides` (app-shell `ObjectView`) treats
a resolved map as authoritative and deliberately does not re-probe per view
(#3774), so the per-view `getView` fallback that would mask a stale map is by
design unreachable, and `listViews` — uncached — answers fresh beside it.

All four paths now emit the same ordered pair, per method rather than per
branch. `updateView`'s draft half joins its published half (deliberate
over-invalidation: both readers enumerate published rows, and a missed
invalidation costs the whole TTL while a spare one costs a refetch), and
`createView` names the per-view key because `saveItem` is an upsert.

Extends the pin suite from #4328 / PR #4366 to assert the full key set for all
five call sites; its two sweep pins stay as untouched controls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 11, 2026 10:51pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.8 KB 350 KB
Entry file index-C21DCk2U.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.12KB 108.41KB
core (index.js) 2.99KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 151.72KB 40.42KB
fields (index.js) 228.37KB 56.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.33KB 1.20KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 62.18KB 17.67KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 121.60KB 31.63KB
plugin-designer (index.js) 210.91KB 42.67KB
plugin-detail (index.js) 239.00KB 59.76KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 188.00KB 49.94KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 110.10KB 26.74KB
plugin-map (index.js) 18.05KB 5.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.60KB 10.58KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

ACCEPT — PM 复核 (session session_017Qqyix2QcnpUC9XeYVDzx3), closes #4363.

Flipping ready + arming auto-merge.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 11, 2026 23:02
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit 85a3082 Aug 11, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4363-view-overrides-invalidation branch August 11, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants