Repository navigation
fix(plugin-timeline,plugin-list,plugin-map,components): five more page blocks re-read on the data-invalidation bus (objectui#10623) - #10649
Conversation
…on bus (objectui#10623) The fetch effect now names the useDataInvalidation nonce for the object it queries, so a write declared with notifyDataChanged re-reads the rows in place. Authored items, host data and bound rows are not subscribed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…s (objectui#10623) The fetch effect now names the useDataInvalidation nonce for the object it queries, so a write declared with notifyDataChanged re-reads the cards in place; collapsed groups survive. Host data, authored data and bound rows are not subscribed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…lently (objectui#10623) The fetch effect now names the useDataInvalidation nonce for the object it queries. A run the bus caused does not flip loading, so MapGL stays mounted and the user's camera survives the re-read. Only the object provider without host rows is subscribed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…ata-invalidation bus (objectui#10623) Each effect now names the useDataInvalidation nonce for the object in its properties.object, so a write declared with notifyDataChanged re-reads the value or the rows. A block with no object or no adapter is not subscribed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…n bus (objectui#10623) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
|
changeset-claim-re-read
|
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsThe idiom, per block (read from the head source; the hook's
The census and the pins. The four pin files are well-formed: each mounts a bare The dev's three findings.
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL What stops landing is wording that the squash merge would publish, plus one unmeasured reach change; the mechanical fix is the right idiom in all five blocks. Required edits, exact text:
Generated by Claude Code |
…; newest run commits; errors clear on success (objectui#10623) Mirrors ObjectGantt: a run is silent only when the invalidation nonce moved and its query inputs equal the ones whose rows were last committed, so a query change arriving with a bus event still goes through the loading gate and re-fits. A sequence ref lets only the newest run commit rows, an error or the loading flag, and the effect cleanup retires a run on re-run or unmount. A failed silent run keeps the last good rows and the mounted map and logs, as ObjectGantt's silent reload does. Committed rows clear an earlier error (objectui#10578's rule). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
… failure and the error clear; changeset wording (objectui#10623) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…host) (objectui#10623) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsThe four mechanisms, read from the head source (
Measured. Worktrees on
Census table and Acceptance notes. The table's head column is the dev's probe; H3's "still 2 calls at ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #10623
Clause-②: no
Five readers begin honouring the published data-invalidation contract; no export, schema or accept set moves.
What changed
Each renderer's fetch effect now names the
useDataInvalidationnonce for the object it reads, the objectui#10494 shape. There is no host counter prop, no relay and nokey=remount. An unscoped change ('*') matches too, and a change to another object does not.object-timelineplugin-timeline/src/ObjectTimeline.tsxschema.objectName(whatfindnames)items, hostdataorbindrowsobject-galleryplugin-list/src/ObjectGallery.tsxschema.objectNamedata, authoreddataorbindrowsobject-mapplugin-map/src/ObjectMap.tsxobjectprovider's object (dataObjectName)objectprovider and no hostdatapropelement:numbercomponents/src/renderers/basic/elements.tsx(ElementNumberRenderer)properties.objectaggregateare presentelement:repeatercomponents/src/renderers/basic/data-list.tsx(RepeaterRenderer)properties.objectfindis presentobject-mapneeded more than the nonce, and its fetch effect now answers the bus the wayObjectGantt'sreloaddoes (objectui#10494, objectui#7237, objectui#10578). Four mechanisms, each pinned inObjectMap.busReread-10623.test.tsx:MapGLfor the length of a fetch, andMapGLreads its camera once, at mount. A run is silent (no loading gate, so the map and the user's camera stay) only when the nonce moved since the effect's last run AND the run's query inputs equal, value for value, the inputs whose rows were last committed. So a query change always goes through the gate and re-fits the camera, even when a bus event lands in the same commit. A bus event before the first rows, or after a query other than the one on screen failed, is loud too, because nothing on screen answers that query yet. This isObjectGantt's split between its silent reload (same query) and its reported one (changed query). One difference: the gantt keeps its chart mounted through a changed query too (objectui#7237), and the map deliberately does not, because remounting is how its camera re-fits.isCurrent()check, as inObjectGantt'sreloadSeqRef, guards every write of rows, the error and the loading flag. The effect's cleanup advances the sequence, so a run superseded by a re-run, or outliving an unmount, commits nothing. Before this change the effect had no cleanup, no cancelled flag and no sequence, so a slow older answer overwrote a newer one.[ObjectMap] Failed to refresh data:to the console, which is exactly whatObjectGantt's silent reload does on failure ([ObjectGantt] Failed to refresh data:). The gantt shows no toast or notice for it, so neither does the map. A failed loud run is reported on the error screen, as before, but only when it is the newest run.setError(null). The error is cleared when the current run commits, never when a run starts, which is objectui#10578's rule onObjectGantt, for the reason that PR measured. Before this change nothing ever cleared the map's error.The
initialViewStatedocblock was reworded to match.Census (H1): real
SchemaRendererand registrations over a counting fake data sourceThe probe was temporary and has been deleted. It ran one mount per block with the
AdapterCtxprovider around the tree. It read the counts at mount, afternotifyDataChanged({ objectName: '*' }), after an event naming ANOTHER object (the negative control), and after an event naming the block's own object. A read is onefindoraggregatecall.d583db11b008a5df9d, and again atcef514df8)object-timelineobject-galleryobject-mapelement:numberelement:repeaterobject-gridThe dispatch hypotheses
ObjectGridandObjectChartuse:useDataInvalidation(fetchesForItself ? object : undefined), with the nonce in the fetch effect's dependency list.useDataInvalidation(undefined)anduseDataInvalidation('')add no listener, because the hook's effect returns early on a falsy name. The nonce therefore stays0, and a block with no object binding never re-runs its effect. Each pin's static-rows case measures this (0 reads before and after a'*'event).object-map's two reads on mount. It is a double fetch, measured with the probe above. Without a lookup field the two calls are the same query:find taskwith$top2001, twice. With a lookup field the first call carries no$expandand the second carries$expand: ['owner']. The fetch effect keys onobjectSchema, which lands after the first query. That is the ungated shape objectui#7895 and objectui#7903 removed from the timeline and the gallery. The nonce neither causes nor cures it: the bus adds exactly one query in both variants (2 → 3). Round 2's sequence guard does not change the count either (still 2 calls atcef514df8); when the second run starts before the first answer lands, that answer is now discarded instead of painted. It is recorded as a finding for the seat and is not fixed here; carrier: card to be filed by the seat.key=. The pins measure that the same DOM node survives on the two blocks with local state: the timeline canvas (timeline-canvas, so its scroll survives) and the map (theMapGLnode, so its camera survives, on a failed re-read as well as a successful one). The gallery pin collapses a group, fires the event, sees the renamed card, and sees the group still collapsed.useDataInvalidationreader mounted beside the block that must read1after the event, and the negative control of an event naming another object.plugin-map.mdx,plugin-timeline.mdx,guide/slotted-pages.md,guide/architecture-overview.md) make no claim this change moves. The pending changesets that mention the bus (10035-*,flow-resume-result-5417,7237-gantt-refresh-in-place,10578-gantt-error-clears,10184-*,10059-*) make no claim about these five blocks that this change moves either.flow-resume-result-5417's "Everything mounted refetches in place" is closer to true after this change, not further from it. No dated note was needed.Tests, round 1
All of the following ran at
008a5df9d, unless a line says otherwise. Round 2 re-ran what it touched; see the next section.turbo run build --concurrency=2with a--filter=PKG^...for each of the four packages exits 0 (13 tasks).type-check(tsc --noEmit && tsc -p tsconfig.test.json) exits 0 for plugin-timeline, plugin-list, plugin-map and components.--listFilescounts each new pin as a program input once per package (1/1/1/1).pnpm exec vitest run --maxWorkers=2 packages/PKG/under the verify lock:Test Files 30 passed (30),Tests 341 passed (341).Test Files 95 passed (95),Tests 1134 passed (1134).Test Files 29 passed (29),Tests 173 passed (173).Test Files 306 passed | 1 skipped (307),Tests 3022 passed | 17 skipped (3039).d583db11bworktree, where they gaveTests 11 failed | 5 passed (16). Every failure is the missing re-read ("expected to be called 2 times, but got 1 times" / "expected +0 to be 1"; in the components file's new-data case, the new row never appears). In each failing case that mounts the positive control, the control had already read1; the new-data case mounts none. The 5 passes are the static-rows cases, which assert no read either way. On head the result isTests 16 passed (16).ablation-replace.mjswith a trap that restores fromHEADby absolute path. Nothing involvesdist: each pin imports its subject through a relative path., invalidationNonce]was dropped from all five dependency lists. The on-disk count went 1 → 0 in each file, and the result wasTests 11 failed | 5 passed (16): every block's re-read cases went red.if (!silent) setLoading(true);becameif (silent || !silent) setLoading(true);. The on-disk count went 1 → 0, the marker count 0 → 1, and the result wasTests 1 failed | 15 passed (16), with exactly "the re-read unmounted the map". A first attempt at this leg used the replacementsetLoading(true);. The tool refused it (the replacement is a substring of the anchor, so its count cannot rise), restored the file, and ran no tests. It is not counted.HEADandgit diff HEADis empty.check-changeset-presence("9 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)"),check:new-line-citations,check:control-bytes,check:changeset-claims,check:pending-changeset-literals,check-changeset-no-major,check:phantom-deps,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inheritandcheck:vi-mock-override-shape.check-governed-queue-guard --testanswers NOT GOVERNED.eslint --no-inline-config --format jsonover the 9 touched.ts/.tsxfiles gives 0 errors. For the five source files, the per-rule warning counts are identical on base and head. The repo-widepnpm lintandpnpm testare left to CI.git merge-treeagainst origin/mainf99f9cde7is clean, and none of the 11 commits main gained since the base touch these files.Round 2 (contract review of the map's silent re-read)
The review failed the map's round-1 silent re-read: silence was decided from the nonce alone, the effect had no stale guard, a failed silent run unmounted the map through the error screen, and nothing cleared the error. Round 2 changes only
ObjectMap.tsx, addsObjectMap.busReread-10623.test.tsx, and corrects the changeset. The four fixes are described under "What changed".All of the following ran at
cef514df8. The pins, the ablations, the build, the type-check and the suite ran under the verify lock (VERDICT command-exit 0, held 200s, shared box). The gates, the lint and the census probe ran outside it.ObjectMap.busReread-10623.test.tsxhas 8 cases: the control (a plain query change mounts a new map fitted to the new rows), A (a query change in the same commit as a bus event re-fits), three for B (a slow older answer does not overwrite a newer one; a slow older failure does not show the error screen; a superseded answer does not release the loading gate), C (a failed bus re-read keeps the rows and the same map node, and logs), and two for D (a later query that succeeds clears the error; a bus re-read after a reported failure goes through the gate and clears it).008a5df9d:Tests 7 failed | 1 passed (8). The one pass is the control. Every failure is the fault named in its case.cef514df8: all 8 pass.*.invalidationRefetch-10623.test.tsx) ond583db11bgiveTests 4 failed | 2 passed (6); the passes are the static-rows cases. Atcef514df8those two files plus the round-2 file giveTest Files 3 passed (3),Tests 14 passed (14). The census probe (temporary, deleted; run unlocked at--maxWorkers=1) re-run atcef514df8reads the same as the table above for all six rows.ablation-replace.mjswith a trap restoring fromHEADby absolute path. Each leg's anchor count went 1 → 0 and its marker count 0 → 1 on disk, and each restore left the blob equal toHEADwithgit diff HEADempty. Run over the round-2 file and the round-1 map pin (11 tests):Tests 1 failed | 10 passed (11), the A case.isCurrentalways true):Tests 3 failed | 8 passed (11), the three B cases.Tests 1 failed | 10 passed (11), the C case.Tests 2 failed | 9 passed (11), the two D cases.Tasks: 13 successful, 13 total.type-checkexits 0, and--listFilescounts the new pin once. The whole suite givesTest Files 30 passed (30),Tests 181 passed (181).check-changeset-presence("10 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)"),check-changeset-no-major,check:control-bytes,check:new-line-citations("0 new citation(s)"),check:changeset-claims,check:pending-changeset-literals,check:phantom-deps,check:test-path-rootsandcheck:vi-mock-specifiers/inherit/override-shape.check:changeset-claimslists four pending changesets that name files this PR edits (6726-*,6783-*,6939-*,9618-*). I re-read each paragraph, and none is made false.eslint --no-inline-configgives 0 errors onObjectMap.tsxand on the new pin.ObjectMap.tsxhas the same per-rule warning counts as on008a5df9d(13no-explicit-any). The first draft of the pin had 2 errors (rules-of-hooksin the map stub,react-hooks/globalsin the host); the harness was reworked before the red and green readings above were taken.onMutationnor the bus, so any write — through the data source or past it (a page action over raw HTTP, a flow, a server action) — showed only when something remounted them." Ond583db11b,onMutationappears 0 times in each of the five files; the control isListView.tsxwith 3. The map bullet now says what a failed re-read does and names the two older faults fixed here. The timeline bullet is scoped to "while the re-read runs", because a failed re-read still shows the error screen (see the timeline finding under Acceptance notes).git merge-treeagainst a freshly fetched origin/main (785d3a731, 16 commits past the base) is clean, and none of those commits touches this PR's files, so no merge was made.Acceptance notes
element:numbershows its…placeholder, andelement:repeateritsLoading…line, for the length of a re-read. Neither holds local state, so nothing is lost, but the list is swapped out briefly. Same shape as the sibling's calendar note.object-timelineandobject-galleryshow their placeholder during a re-read only when they have no rows yet. Otherwise the canvas and the card grid stay mounted.object-map's stale-answer guard and error clear are fixed here (round 2, B and D). Both faults predate this PR, and both are pinned red on008a5df9dand green oncef514df8.object-timelinenever clearserror, and its error screen is an early return above the canvas. So a failed re-read replaces the canvas with the error screen, and a later re-read that succeeds leaves it there until a remount. A temporary probe measured it atcef514df8through the real registration: after a failing bus re-read the canvas is gone and the error shows; after a succeeding one (3findcalls in all) the error still shows. The mechanism predates this PR, and this PR widens its reach, since a bus event can now trigger the re-read. Not fixed here; carrier: card to be filed by the seat.element:repeater's effect readsproperties.sortbut does not list it as a dependency. Read from the source, not measured. Carrier: card to be filed by the seat.Generated by Claude Code