Skip to content

fix(data-objectstack,app-shell): one meta/view read per open: a settled view list that refresh and every console view write drop (objectui#13057) - #13092

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-13057-settled-view-list
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-13057-settled-view-list

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #13057
Clause-②: no

What changes

One object-page open now reads GET /api/v1/meta/view once (was 3 before objectui#13044, 2 after PR #13053). This completes objectui#13044 ruling 6268839381 (a) to (c).

The adapter keeps a settled list (@object-ui/data-objectstack).

  • MetadataCache.share still joins a request in flight and is never answered from a settled entry. It now also stores its landing. A request detached by invalidate / clear still stores nothing.
  • The client wrap (installViewListShare) is unchanged otherwise. A caller of getClient().meta.getItems('view') (the provider's eager read) always asks the server, and its answer is stored under list:view.
  • readViewList (the source for listViews and listViewOverrides) answers from the stored list while it holds one. Otherwise it reads through the client wrap. Every caller gets its own structuredClone.

Every path that should see a fresh list drops it first.

  • (a) User refresh. ObjectView's refreshData calls dropPublishedViewList(dataSource) before it bumps the counter. The saved-views read it re-runs asks the server again. That covers realtime, server actions, imports and the refresh action. Provider re-reads (refresh('view')) go through the client, so they always ask.
  • (b) Every in-SPA view write.
    • The adapter seam (invalidateViewKeys) and the adapter's client write wrap already dropped the key. Now that the key holds a settled list, they drop it.
    • New: the console MetadataClient. useMetadataClient (the hook every app-shell write path takes its client from, as it does for the advisory sink) wraps the client's write doors through installViewListDrop (views/metadata-admin/viewListDrop.ts). Each save / publish / publishDraft / reset / rollback whose type is view, and each publishPackageDrafts, drops the list once the write settles, whether it landed or was refused. It drops through the adapter's existing public invalidateCache(key).
    • Why an explicit drop and not the pulse. The metadata-refresh pulse does reach the adapter (the provider's clearCache). It also re-reads every loaded provider type, so firing it on every Studio draft autosave would be a refetch storm. Package-wide publishes get both.
  • PackagesPage publishDrafts now calls emitMetadataRefresh() after a 2xx batch, as Studio's publish-all, Home's and the chat bar's do. It is sent before the failed[] verdict, as Studio sends it.
  • (c) Org or session switch. Unchanged: MetadataProvider dropAdapterWorld clears the whole adapter cache. It now has a settled-list pin.

No new export, no new public member. MetadataCache is package-internal. viewListDrop.ts is module-internal: no barrel re-exports it, and metadata-admin/index.ts and the package index.ts are untouched. The one cross-package spelling is the key literal list:view in viewListDrop.ts. The provider and refresh pins drive a real adapter, so if the key is renamed they go red.

The writers, enumerated (and pinned so a new one cannot be silent)

The enumeration is pinned at the door, not by call-site list. Every console MetadataClient is built by useMetadataClient: the provider's draft-preview client is the read-only exception, and new MetadataClient( was already ratcheted. Every door of MetadataClient that sends a write is wrapped.

  • viewListDrop-13057.test.tsx derives the write doors from MetadataClient.prototype: every method whose comment-masked body calls this.sendWrite(. It asserts that set equals ITEM_WRITE_DOORS plus PACKAGE_WRITE_DOORS.
  • The same file scans app-shell src (comment-masked with scripts/js-comment-mask.mjs) for createConsoleMetadataClient( call sites. It asserts they are exactly the hook and MetadataProvider.

Call sites under views/metadata-admin/** and views/studio-design/** that can write a view row, from a grep of the write doors:

  • ResourceEditPage: create (save, live or draft), the guarded draft save, publish, reset (delete/reset and discard draft). The type is generic, so view included.
  • ResourceHistoryPage: rollback, generic type.
  • EmbeddedItemEditor: save(parentType, …).
  • DraftConflictDialog: the guarded save used by ResourceEditPage and Studio's ObjectListViewInspector (view drafts).
  • PackagesPage and StudioDesignSurface: publishPackageDrafts. Studio also has publishDraft('flow', …) and typed saves.

The other writes in those directories are typed object / hook / flow / permission, and they keep the list (control pin). All of these take their client from useMetadataClient.

Pins

  • (a) MetadataProvider.oneMetadataRead-13044.test.tsx: the console order (eager read, then listViews plus listViewOverrides, plus a second overrides read) is 1 request (was 2). The comment in the test gives the reason.
  • (a) The same file: within an open, later listViews reads are settled. A provider refresh('view') asks again, and the next list read sees what that refresh read (a row added server-side appears only after it).
  • (b) ObjectView.refreshRereadsViews-13057.test.tsx uses the real page, a real adapter and a realtime refresh. The open reads once; refreshData re-asks (1 then 2). Control: a re-render with no refresh reads nothing.
  • (c) viewListDrop-13057.test.tsx has one leg per writer class: save (draft), save (live), publish, publishDraft, reset, rollback, publishPackageDrafts. Each leg settles the list, writes, then the next read re-asks exactly once. A refused view write drops it too. Controls: a write of another type keeps the list, the write's answer passes through untouched, and with no adapter in context the writes still land.
  • (c) oneMetadataRead: invalidateViewKeys, and a saveItem('view') straight through the adapter's client, each drop the settled list.
  • (d) PackagesPage.publishPulse-13057.test.tsx uses the real sheet and useMetadataClient. Publish app emits the pulse once, and the next listViews re-asks. A batch answered with failed[] still emits it.
  • (e) CONTROLS:
    • MetadataProvider.switchDropsAdapter-13044.test.tsx: an org switch and a session switch each re-ask after a settled read.
    • oneMetadataRead: a draft-preview listViews is its own ?preview=draft request and never fills list:view (getCached('list:view') stays undefined, and the published read still asks once).
  • viewCacheInvalidation.pin.test.ts stays truthful: listViews consults exactly list:view (settled within an open), else the transport, and no per-object key. Its header says the named writers drop it.
  • MetadataCache.share-13044.test.ts: share is never answered from a settled entry and stores its landing. A request detached by invalidate stores nothing.

Ablations

Each was run through ablation-replace.mjs (anchor must hit; restore proven by blob hash equal to HEAD and an empty git diff HEAD). The subject resolves through the vitest alias to src/, so no dist build was involved.

# mutation went red restore (blob = HEAD)
1 readViewList: drop the settled read (if (settled !== undefined) return … replaced with void settled;) (a) console order expected 2 to be 1, plus the two settled legs index.ts 9ef65591060b
2 installViewListDrop: the package door without .finally(drop) the publishPackageDrafts leg (expected 1 to be 2) and (d)'s re-read leg; the other 6 writer legs stayed green viewListDrop.ts 286c0f6df1fa
3 installViewListDrop: the item doors without the view drop all 6 item-writer legs plus the refused-write leg; the package leg and the controls stayed green viewListDrop.ts 286c0f6df1fa
4 refreshData without dropPublishedViewList (b) expected 1 to be 2 ObjectView.tsx 6cc2e5cc98d1
5 publishDrafts without emitMetadataRefresh() both (d) legs expected +0 to be 1 PackagesPage.tsx 98ee9953967f

Gates

The numbers below are for head ca09edc959. Each exit code was captured before any pipe.

  • Type-check: closure built first (turbo run build --filter='@object-ui/app-shell^...', 28/28).
    • @object-ui/data-objectstack type-check: 0. Its --listFiles includes both edited tests.
    • @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json): 0. --listFiles includes all five new or edited app-shell tests.
  • Targeted vitest (--maxWorkers=1, under the verify lock): the 7 new or edited files, then the families of the touched files, all green.
    • The family set is every data-objectstack test that reads the view list or MetadataCache, every ObjectView* test, every PackagesPage* and MetadataProvider* test, and every app-shell test importing metadata-admin/useMetadata, which takes in 129 studio-design files.
    • That is 265 files run in 7 chunks, 0 failures.
  • pnpm lint, narrowed to the 13 changed .ts/.tsx files: 0 errors.
    • The 13 files come from eslint's own --format json output.
    • eslint.config.js sets no parserOptions.project / projectService, so linting is not type-aware, and this diff cannot move a verdict on an untouched file.
    • The new warnings are the two react-hooks ref warnings beside the existing tRef ones in useMetadataClient.
  • Pre-tsc battery, all 0: check:phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes, scripts/check-hand-rolled-comment-mask.mjs. Also check:test-path-roots and check:new-line-citations: 0.
  • node scripts/check-changeset-fixed.mjs: 0. check-changeset-presence: one changeset declared, .changeset/13057-settled-view-list.md (patch for both packages).
  • NOT MEASURED: check:spec-floors. It exits 1 with no-artifact, because it needs the whole workspace built; that is not a verdict on this diff.
  • NOT MEASURED: the full packages/app-shell/ and packages/data-objectstack/ directories. They are declared to CI per the dispatch (no whole-package runs on the shared box).

Acceptance notes (seen, not filed: static reading, no public-door reproduction)

  • The other PackagesPage lifecycle actions emit no metadata-refresh pulse either: registry publish, revert, delete, enable/disable and adopt-orphans all go through apiJson. Each can change live metadata, views included. Ruling 4 named only publishDrafts. Carrier: none.
  • A console MetadataClient view write drops list:view, but not the adapter's per-object view-overrides:OBJECT / view:OBJECT:NAME entries. Those need the object name, which reset / rollback / publish do not carry. This was already the case before this PR, and the stored override map can stay stale for the cache TTL after a metadata-admin view publish. Carrier: none.

Session https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB.


Generated by Claude Code

…n an open, dropped by refresh and every console view write (objectui#13057)

The adapter stores the answer to GET /api/v1/meta/view under list:view and
answers listViews / listViewOverrides from it, so the console order (eager
read, then the object page's two readers) reads the URL once. The client read
still always asks the server and stores what it got.

Every path that should see a fresh list drops it first:
- ObjectView refreshData drops it before re-reading the saved views;
- useMetadataClient drops it after each MetadataClient write door that can
  change a published view (item writes typed view, package-wide publishes);
- PackagesPage publishDrafts emits the metadata-refresh pulse the other
  publish paths emit;
- org and session switches keep clearing the adapter cache.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 77 chunks) 3207.2 KB 3744.1 KB
Main entry chunk (gzip) 81.5 KB 350 KB
Entry file index-CdSEMP-v.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (buildIdentity.js) 0.80KB 0.49KB
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 21.17KB 7.81KB
app-shell (runtime-config.js) 24.52KB 8.48KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 45.59KB 12.03KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.67KB 6.12KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.74KB 3.94KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 42.84KB 11.66KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
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) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 615.08KB 149.86KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 251.39KB 70.18KB
fields (index.js) 292.36KB 74.70KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.33KB 11.96KB
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.75KB
mobile (index.js) 1.99KB 0.87KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 55.04KB 16.10KB
plugin-charts (index.js) 85.14KB 23.42KB
plugin-chatbot (index.js) 238.47KB 56.30KB
plugin-dashboard (index.js) 147.53KB 39.95KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 265.63KB 70.60KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 184.11KB 47.60KB
plugin-gantt (index.js) 178.46KB 45.13KB
plugin-grid (index.js) 272.91KB 75.73KB
plugin-kanban (index.js) 64.49KB 19.83KB
plugin-list (index.js) 148.80KB 38.90KB
plugin-map (index.js) 27.17KB 9.00KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.31KB 11.88KB
plugin-tree (index.js) 15.27KB 5.40KB
plugin-view (index.js) 94.68KB 24.36KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 126.45KB 41.40KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.71KB 2.11KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (product-blocks.js) 0.20KB 0.19KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants