Skip to content

fix(components): page:tabs probes related counts from the named record, not the loaded row (objectui#13010 part 1) - #13019

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-13010-record-open-waves
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-13010-record-open-waves

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #13010
Clause-②: no

Part 1 of the card, Wanted items 1–2, under the seat rulings in comment 6258704497. Item 3 is dropped because its premise was disproved. Items 4 and 5 stay on the card.

What changed

  • Item 2 (landed). page:tabs (packages/components/src/renderers/layout/containers.tsx) now keys its related-count badge probes on ctx.recordId, the record the context names (the route's id on a record page). It used to key them on ctx.data.id, the id inside the loaded row, so every probe waited for the record read and went out a wave behind it. recordId is a required member of RecordContextValue and every provider fills it, so this is a single read, not a fallback. A list whose visibility the record decides keeps that wait: one behind a node-level visibleWhen, or in a tab whose item-level visibleWhen decides it, is probed once the row has landed, as before.
  • Item 1 (not landed; fallback B, as the ruling allows). Rule A was to share the rail's in-flight read with the tab badge through RelatedCountStore.fetch. It cannot register first without a timing hack. After item 2, the tab strip's probe effect runs in the first commit and reaches RelatedCountStore.fetch after a single microtask hop (its getObjectSchema await). The rail's read is gated by its IntersectionObserver visibility latch, which flips after a frame, and only then does it start its own schema await. So the probe is always on the wire first; the after-table below shows the top=1 probes issued ahead of the top=3 reads. Making the rail win would mean dropping its visibility gate (which keeps a hidden xl:flex rail from querying at all) and relying on effect order plus microtask order. Each badge probe now shares wave 1 with its card's read, so the extra wave is gone; the three count requests remain.
  • Item 3 (dropped). The two security/explain POSTs on a record open have different bodies, operation: update and operation: delete, from plugin-detail's useRecordEditable. objectui#11699's in-flight sharing is in the card's pin. The harness pins the two bodies.

Wave table: the rendered harness, before and after

RecordDetailView.recordOpenWaves-13010.test.tsx mounts the real RecordDetailView over the real ObjectStackAdapter under StrictMode, on a HotCRM-shaped authored page: a tab strip whose Related tab holds 3 related lists, the third with a declared status filter, plus a reference rail aside with 3 entries at limit 3. A held transport answers one wave at a time.

Wave 1 Wave 2 Total
Before (d0934b919d) approvals; explain update; explain delete; record; sys_user top=50; sys_comment; sys_activity top=200; quote/line/task top=3 quote/line/task top=1 13 requests, 2 waves
After (355c0146f3) all of the above, the three top=1 probes included (issued before the three top=3 reads) — 13 requests, 1 wave

In the card's in-app measurement the probes were wave 3 (at 1858 ms, after the record landed at 1.81 s). That wave is the one this removes.

Pins

  • packages/components/src/__tests__/page-tabs-probe-route-id-13010.test.tsx:
    • an ungated list probes before the row lands, scoped to recordId;
    • a list behind a node-level visibleWhen waits for the row;
    • a tab whose item-level visibleWhen reads true while the row is absent waits for it;
    • CONTROL: with no record named, nothing is probed.
  • The harness:
    • every read is in wave 1;
    • CONTROL: the task badge, whose declared filter differs from its rail card's, still probes with its own filter (not_equals), and the card's read carries none;
    • the two explain bodies are update and delete.
  • Pin (a) from the ruling (no top=1 probe where the card's read answered) is NOT here; see item 1 above.

Ablations

Each was run once through scripts/ablation-replace.mjs, which checks the anchor counts. Each restore was proven by blob hash: 09c621807d6a equals HEAD, and git diff HEAD is empty.

  • Re-keying parentId on ctx?.data?.id: 3 tests red (the harness's wave-1 pin, and the ungated-list and node-gate pins).
  • Deleting the node-gate filter line: 1 red (the node-level visibleWhen pin).
  • Deleting the item-gate return: 1 red (the item-level visibleWhen pin). On its first run this ablation stayed green, because that pin's predicate (record.plan == 'pro') already reads false while the row is absent, so the line was untested. I rewrote the pin's predicate to record.plan != 'free', which reads true while the row is absent, and on the rerun it went red.

Verification (head 355c0146f3)

  • Targeted vitest, --maxWorkers=1, under the shared lock: 32 files, 252 tests passed. The run covers the page-tabs-* family (the objectui#12987 visibleWhen count pins and the objectui#12992 scope builder included), related-count-store*, record-reference-rail*, the RecordDetailView.related* pins (objectui#8882, objectui#4664, objectui#5795, objectui#11733), childWriteRefreshesParent-12244, recordOpenRequests-*, accessibleNames-12625, authoredActivityThreading-12249, PagePreview, SchemaRenderer.visibleWhenRecordBinding, and the two new files.
  • Closure build --filter '@object-ui/app-shell^...': exit 0. @object-ui/components type-check (src and tests): exit 0, re-run after the final edit. @object-ui/app-shell type-check: exit 0.
  • eslint on the 3 changed source and test files: 0 errors. containers.tsx has 94 warnings, the same as on base.
  • Pre-tsc battery:
    • exit 0: 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, test-path-roots, new-line-citations, vi-mock-specifiers, vi-mock-inherit, and check-hand-rolled-comment-mask;
    • check:spec-floors exits 1 with no-artifact (it needs a whole-workspace build), so it is NOT MEASURED; the order notes it may exit 1 on main.
  • Changeset checks: changeset-fixed, changeset-presence and changeset-no-major all exit 0.
  • Declared to CI: the full package directories of @object-ui/components and @object-ui/app-shell, and repo-wide pnpm lint.

Acceptance notes

  • The probes no longer wait for the record body. On a record that turns out to be missing or refused, the ungated badge probes still go out once, beside the record read, before the page swaps to its not-found or denied state. Each costs one count request and draws nothing.
  • Not an objectui#13010 item: a page that renders the rail and the tab badge for the same unfiltered relation still pays two reads per relation in one wave. Removing that needs the rail's read to register before the probe. A deterministic way to do that would be a store-level change or a rework of the rail's visibility gate; that is the seat's call.

Generated by Claude Code

A held-transport harness that mounts the real record page over the real
adapter on a HotCRM-shaped authored page (tab strip with related lists,
reference rail aside) and logs every request with the wave it went out in.
It pins what holds today: the record read, both explain questions (update
and delete, two different bodies) and the rail's top=3 card reads go out in
wave 1. The tab strip's top=1 badge probes follow in wave 2, keyed on the
record body's id; their fix lives outside this part's file surface.

Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Co-authored-by: Claude <noreply@anthropic.com>
…d, not the loaded row (objectui#13010)

The tab strip keyed its badge probes on ctx.data.id, so every probe waited
for the record read and went out a wave behind it. It now reads
ctx.recordId, the required member every RecordContextProvider fills (the
route's id on a record page). A list behind a node-level visibleWhen, or
in a tab whose item-level visibleWhen decides it, keeps waiting for the
record body, since its gate reads it.

Pins: page-tabs-probe-route-id-13010 (ungated list probes before the row
lands, gated ones after, no record named no probe) and the record-open
harness (13 requests in one wave; a badge whose declared filter differs
from its card still probes with its own filter).

Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Co-authored-by: Claude <noreply@anthropic.com>
… true while the row is absent (objectui#13010)

Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Co-authored-by: Claude <noreply@anthropic.com>
…t; declare app-shell in the changeset (objectui#13010)

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 75 chunks) 3165.2 KB 3744.1 KB
Main entry chunk (gzip) 80.5 KB 350 KB
Entry file index-BOE3vFwh.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.41KB 7.88KB
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) 613.16KB 149.22KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 247.01KB 68.85KB
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.06KB 23.39KB
plugin-chatbot (index.js) 233.38KB 54.86KB
plugin-dashboard (index.js) 147.53KB 39.95KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 263.31KB 69.81KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 181.16KB 46.84KB
plugin-gantt (index.js) 178.46KB 45.13KB
plugin-grid (index.js) 271.30KB 75.32KB
plugin-kanban (index.js) 63.37KB 19.51KB
plugin-list (index.js) 140.49KB 36.24KB
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

Development

Successfully merging this pull request may close these issues.

2 participants