Conversation
`localIdInParseRange` / `localIdInOverlay` (modelSlice.ts, #2697) and `teardown-scope.ts`'s `modelRemovedScope` survivor check re-spelled the same "does a surviving model own this global id" rule by hand — the latter's own comment already flagged this as known duplication that "consolidating is a change of its own". Move the two predicates into `store/globalId.ts`, the cycle-free home the comment already named, and have both modelSlice.ts and teardown-scope.ts call them instead of re-deriving the range/overlay arithmetic. Issue #3343 described this as four copies, two of which ignored the extraction and disagreed. That was accurate when filed, but an unrelated teardown refactor (#3358, already merged) collapsed the two disagreeing copies (modelSlice's removeModel and syncSourceModel's purgeStaleEntityState) into the single teardown-scope.ts copy fixed here. A 300k-case property check (isStale's old hand-rolled loop vs. the shared functions, and identity resolution vs. staleness, across random federated-model layouts with overlay ids bled into a neighbor's parse range) found zero behavioral disagreement between the current duplicate and the extraction — the boolean "some survivor owns it" is a straight OR across survivors, so check order never mattered for that question, only for the (unrelated, already-correct) identity resolution the modelSlice comment about the two-pass order already covers. The remaining risk was structural, not a live bug: nothing stopped the next edit to the rule from being made in one place and not the other. `apps/viewer/src/lib/lens/adapter.ts` holds a fifth, parse-range-only spelling in a result-object-reusing hot loop over its own `ModelEntry` type; left alone as the issue asks (different module, no overlay pass, doubly justified now since the shared function's callers all resolve global ids into `Map` entries rather than reuse a pooled object across a hot loop). RED proven by stashing only the source changes (globalId.ts, modelSlice.ts, teardown-scope.ts) and running the new test against the restored old code: it failed to import `localIdInOverlay` (SyntaxError: does not provide an export), then passed 20/20 after restoring the fix.
|
Warning Review limit reached
This review includes 4 billable files and costs up to $1.00. Or wait 32 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 (4)
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 |
Refs #3343.
What #3343 found, and what changed since it was filed
The issue named four copies of "does a surviving model own this global id" (parse-range, then overlay):
modelSlice.ts's extractedlocalIdInParseRange/localIdInOverlay(#2697),modelSlice.ts'sremoveModelsurvivor loop (re-spelled by hand, not calling the extraction),syncSourceModel.ts'spurgeStaleEntityState(a byte-identical second hand copy), andstore/globalId.ts'sfromGlobalIdFromModels(parse-range only, deliberately different — not a consolidation target).That was accurate at the commit the issue was filed against. Since then, an unrelated refactor (#3358, already merged) replaced
removeModel's andsyncSourceModel's hand-rolled loops with one sharedteardown-scope.ts'smodelRemovedScope. Its own doc comment already flagged what was left: "KNOWN DUPLICATION, deliberately left: the survivor predicate below is a third statement of the ownership rule...store/globalId.tsis the cycle-free home all three should share. Consolidating them is a change of its own."So on
upstream/maintoday there are three implementations, not four: the extraction, the deliberately-differentfromGlobalIdFromModels, andmodelRemovedScope's copy.Does the remaining pair actually disagree?
I checked before touching anything.
modelRemovedScope'sisStaleonly needs a boolean ("does some survivor own this id"), which is a plain OR across survivors of (parse-range match OR overlay match) — and OR is order-independent.resolveGlobalIdFromModelsneeds to pick which model owns an id, which is why it deliberately runs two full passes (every model's parse range, then every model's overlay) rather than checking each model's own overlay before the next model's parse range — its own comment explains why: an overlay id of the first model could shadow a plain parse-time id of the second. That hazard exists for identity resolution, not for the booleanisStaleasks.I fuzzed both formulations (300k randomized federated-model layouts, offsets, and overlay ids deliberately bled into a neighbor's parse range) and found zero disagreements between the old hand-rolled
isStaleand the shared functions, and zero disagreements between the old and newisStaleimplementations. So there is no live bug to reproduce as a failing test today — the two disagreeing copies the issue names were already unified by #3358. The remaining exposure was structural: nothing stopped the next edit to the rule from landing in one copy and not the other, exactly as the maintainer's own comment says.What this PR does
Moves
localIdInParseRange/localIdInOverlayintostore/globalId.ts(the cycle-free home named in the comment:modelSlice.tsimportsteardown-scope.ts, so the reverse would cycle;globalId.tsimports neither) and exports them.modelSlice.tsandteardown-scope.tsnow both call the shared functions instead of re-deriving the range/overlay arithmetic. Behavior is unchanged — verified by the fuzz check above and by the full scoped test run below.A fifth,
ModelEntry-typed, parse-range-only spelling remains inapps/viewer/src/lib/lens/adapter.ts'sresolveGlobalId(a result-object-reusing hot loop for lens evaluation, 100k+ calls). Left alone, per the issue's own direction — different module, different type, no overlay pass — and now more clearly justified: every caller of the shared functions works withMapentries, not a pooled result object reused across a hot loop.Proof of RED
apps/viewer/src/store/globalId.test.tsgained direct tests forlocalIdInParseRange/localIdInOverlay, plus a cross-check thatmodelRemovedScope'sisStaleand the shared functions agree on the "overlay of model A lands inside model B's parse range" scenariomodelSlice.ts's own comment warns about.Proved RED by stashing only the three source files (
globalId.ts,modelSlice.ts,teardown-scope.ts— keeping the new test), running the suite against the restored old code, confirming failure, then restoring by SHA (notstash pop):GREEN after restoring the fix:
# tests 20 / # pass 20 / # fail 0.Scoped regression run across every test file touching this rule (modelSlice, teardown-registry, teardown idempotence, model lifecycle x2, syncSourceModel, four removeModel-*-stale tests, clearAllModels-overlay-stale, lens adapter):
tsc --noEmitonapps/viewer: 0 errors.check-source-text-assertions.mjs: OK, 0 new.check-module-size.mjs: OK (both changed files shrink or stay well under budget; left the pre-existing unrelated allowlist headroom notes untouched to keep this PR scoped).Overlap with in-flight viewer-store PRs
Checked #3367, #3371, #3372, #3375, #3379 (per the issue triage) — none touch
modelSlice.ts,globalId.ts, orteardown-scope.ts. No file overlap.No changeset:
@ifc-lite/vieweris a private package and #3358 (the comparable teardown refactor this builds on) didn't carry one either.Does not close #3343 — leaving that to the maintainer per policy.