fix(viewer): composeTeardown's equality filter is now structural and per-key - #3379
fix(viewer): composeTeardown's equality filter is now structural and per-key#3379BIMvoice wants to merge 1 commit into
Conversation
…r-key `composeTeardown` dropped a contribution's key only when Object.is matched the live state. visibilitySlice.teardown.ts and selectionSlice.teardown.ts gate their model-removed arm with one `touched` boolean over several fields, so once any single field named the removed model, every field in that group was rebuilt - including a sibling Set/Map/array whose own content did not change. The rebuilt-but-equal collection is a fresh reference, so Object.is could not see through it and wrote it into the composed patch anyway, defeating any Object.is/memo check downstream. composeTeardown now compares structurally (Set/Map/array, recursing into Map values) before dropping a key, so an untouched field keeps its identity regardless of which sibling field triggered the group's gate. NEVER_DROPPED's exemption - forcing isolatedEntities/ghostExceptEntities into the patch so withVisibilityOwnershipInvalidation keeps running even when their value did not change - is now scoped to session-reset and all-models-cleared, the two scopes whose contribution always carries both keys unconditionally. model-removed never made that promise; its own `touched` gate already decides presence, so forcing presence there just reproduced this bug for exactly those two keys. Two new tests reproduce the mixed case with identity assertions (presence in the composed patch, not value equality) for both affected slices, RED before this change and GREEN after. teardown-registry.test.ts and teardown.idempotence.test.ts pass unchanged - no owns/pin list moved, since this only changes which unchanged keys composeTeardown drops. Refs #3346
|
Warning Review limit reached
This review includes 2 billable files and costs up to $0.50. Or wait 45 minutes for your next included review. View limit detailsLimit details: You’ve used all 2 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Comment |
Viewer benchmark✅ No threshold regressions detected. 01_Snowdon_Towers_Sample_Structural(1).ifcBaseline recorded 2026-07-01T20:31:05.538Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.
AC20-FZK-Haus.ifcBaseline recorded 2026-07-01T20:30:59.972Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.
Refresh the baseline from a CI run: dispatch the Benchmark workflow with |
Summary
Fixes #3346.
composeTeardowndropped a contribution's key only whenObject.ismatched the live state.visibilitySlice.teardown.tsandselectionSlice.teardown.tsgate theirmodel-removedarm with onetouchedboolean over several fields, so once any single field named the removed model, every field in that group was rebuilt — including a siblingSet/Map/array whose own content did not change. The rebuilt-but-equal collection is a fresh reference, soObject.iscould not see through it and wrote it into the composed patch anyway, defeating anyObject.is/ memo check downstream.Root cause, one sentence: the equality gate that decides whether a teardown key gets written was per-slice (a single
touchedboolean covering several fields) at the contribution level andObject.is-only at the composition level, so neither layer could catch a sibling field being rebuilt equal-but-new.Fix:
composeTeardown(apps/viewer/src/store/teardown.ts) now compares structurally —Set/Map/array, recursing intoMapvalues — before dropping a key, so an untouched field keeps its identity regardless of which sibling field tripped the group'stouchedgate.One consequence needed handling:
NEVER_DROPPEDforcesisolatedEntities/ghostExceptEntitiesinto the patch sowithVisibilityOwnershipInvalidationkeeps running even when their value hasn't changed. That's a real requirement forsession-reset/all-models-cleared, whose contributions declare both keys unconditionally — butmodel-removednever made that promise (its owntouchedgate already decides presence), so forcing presence there just reproduced the reported bug for exactly those two keys.NEVER_DROPPED's exemption is now scoped tosession-reset/all-models-clearedonly (FORCED_PRESENCE_SCOPES).Proof of RED, then GREEN
New file
apps/viewer/src/store/teardown.object-is-gate.test.ts, two cases (visibilitySlice, selectionSlice), asserting presence in the composed patch for an untouched field — value-equality assertions can't see a reference change, so this asserts on the same signal anObject.issubscriber would.Before the fix (source unmodified, only the new test added):
After the fix:
Wider run (visibility ownership + slice tests) also green:
pnpm typecheck(root) andnode scripts/check-module-size.mjsboth pass;teardown.tsis 399 lines (comments trimmed to stay under the 400-line ratchet rather than allowlisting).What didn't change, and why
ownslist, no pin (PINNED_SESSION_RESET_KEYS/PINNED_ALL_MODELS_CLEARED_KEYS/PINNED_MODEL_REMOVED_KEYS/PINNED_OWNED_KEYS) moved. Those pins read rawentry.teardown(scope, state)output, bypassingcomposeTeardownentirely — this fix only changes which unchanged keys the composition drops, not what any contribution's body writes.visibilitySlice.teardown.ts/selectionSlice.teardown.tsthemselves are unchanged: the issue's proposed fix (give the seam structural equality) is what's implemented, keeping the per-slicetouchedpre-check as a cheap common-case skip rather than removing it (the issue measured that removal as a net perf loss in the common case).#3345 — reported, not fixed
Read #3345 ("a fourth teardown scope would silently no-op in 22 of 28 slice contributions"). It's a real, separate defect: the fix there is making
TeardownScope's arms a required record per contribution instead of anif (scope.kind !== 'session-reset') return {}guard, so a fourth scope kind fails to compile in all 28 files rather than silently clearing nothing. That's a shape change to every*.teardown.tsfile and toSliceTeardown's declaration surface — not something this PR's fix touches or depends on, and not inseparable from it. Leaving it for its own PR.Overlap with in-flight teardown PRs
Per the task brief, checked all four open PRs against this mechanism (#3371, #3372, #3375, #3367) for textual overlap — none of them touch
apps/viewer/src/store/teardown.ts(this PR's only source change) or the new test file, so there's no diff conflict to resolve. (#3372, #3375, #3367 all touchteardown-registry.test.ts, which this PR does not.)🤖 Generated with Claude Code
https://claude.ai/code/session_01QPHChk3Ve9N519A4kY7436