Repository navigation
explorer: refactor URL/mode state management (centralize writers, collapse dual mode) #208
Description
Structural follow-up from Codex's retrospective on #203 + #205. Two related smells; addressing them together because they touch the same code paths.
Smell 1: Many URL writers, no single boundary
Across explorer.qmd there are now (at least):
- Selection click handlers (sample, cluster) —
pushState - Mode-transition writes — inside
enterPointMode/exitPointMode writeQueryState()— query-string filters- Camera-debounce early write (explorer: fix URL state round-trip (closes #201 parts A+B) #203)
- Camera-debounce trailing write (pre-existing)
camera.moveEndlistener (explorer: add camera.moveEnd URL-write backstop (closes #204) #205)- Share-button copy
- Hashchange handler's internal
replaceState
Each write has its own gate (_suppressHashWrite, freshness checks, etc.). When something goes wrong with URL state, the answer to "which writer wrote that?" is non-obvious.
Codex's sketched interface:
writeGlobeHash({ replace: true })
reconcileCameraState({ reason, writeEarlyHash: true })
setExplorerMode(nextMode, { pushHistory: false | true })Then boot, hashchange, user camera movement, and share/copy call clear, intention-revealing paths.
Also flagged: moveEnd currently only writes the URL — sub-percentageChanged moves still bypass the rest of the camera-settled pipeline (sample reloads, cluster count refresh). If a small pan is important enough to update the URL, it should probably update the in-view stats too. A unified "settled-camera reconciliation" path would solve both.
Smell 2: Dual mode state
explorer.qmd carries the current mode in two places:
- Closure-private
modevariable (used by the camera-changed handler at lines 1971+) - Public
viewer._globeState.mode(used bybuildHash, tests, harnesses)
They're updated together today but the duplication is fragile. The Playwright investigation harness's iter 1 captured viewer._globeState.mode === 'cluster' shortly after waitReadyAndPointMode saw mode === 'point' — empirical evidence of transient sync mismatch.
Fix shape: make _globeState.mode the single source of truth, and wrap mode transitions in one setter (setExplorerMode(next, opts)). Add an invariant assertion / dev-time console log when the two diverge until the closure variable can be deleted.
Sequencing
Probably:
- Collapse
modeto single source (smell 2) — smaller, sets the foundation. - Introduce
writeGlobeHash+setExplorerModeand migrate the writers (smell 1). - Route both
camera.changedandcamera.moveEndthrough a singlereconcileCameraState(reason)that does URL write + viewport stats + sample reload as appropriate.
Each step is a separate PR. Codex review at each step is recommended given the structural nature.
Acceptance
- Single source of truth for
mode. - Single explicit URL-write API; old direct
history.{push,replace}Statecall sites migrated. camera.moveEndandcamera.changedshare the same settled-camera reconciliation entry point.- No regression in the existing URL round-trip Playwright assertions (post-explorer: add camera.moveEnd URL-write backstop (closes #204) #205 they all pass).
Refs
- PR explorer: fix URL state round-trip (closes #201 parts A+B) #203's retrospective Codex review comment — full discussion
- explorer: 'Samples in View' shows the 5000 fetch budget, not real count + URL round-trip doesn't reproduce view #201 / explorer: fix URL state round-trip (closes #201 parts A+B) #203 / explorer: add camera.moveEnd URL-write backstop (closes #204) #205 — original work this builds on
- Explorer state contract: URL/DOM/widget-state inventory + search-as-global-filter decision #164 — Explorer state contract (this refactor materializes that contract in code)
Activity
tests: Playwright spec for explorer URL state round-trip (addresses #209
explorer: collapse dual mode state to single source of truth (#208 st…
ci: explorer Playwright e2e gate (isamplesorg#249 PR 1) (#9)
isamplesorg#208 PR4a: centralize globe-hash writes behind writeGlobeH…
isamplesorg#208 PR4b: shared settled-camera tail via reconcileSettled…
#208 PR4a: centralize globe-hash writes behind writeGlobeHash() (#13) (…
Ship Material facet hierarchy tree to isamples.org (#281/#282) + PR4b +
#208 PR4c: extract filtersForcePoint() + computeTargetMode() (behavio…
rdhyee commentedon Jun 19, 2026
Closing as resolved. URL/mode state refactor complete: dual-mode state collapsed to a single source of truth (#213), URL writes centralized behind writeGlobeHash() (#288, PR4a), settled-camera reconcile shared (PR4b, folded into #289), and the point/cluster mode decision centralized via filtersForcePoint()/computeTargetMode() (#301, PR4c). All acceptance criteria in the issue are met.