explorer: extract freshSelectionToken() primitive (closes #187 step 1)#188
explorer: extract freshSelectionToken() primitive (closes #187 step 1)#188rdhyee merged 3 commits intoisamplesorg:mainfrom
Conversation
…#187 step 1) Six rounds of Codex review on PR isamplesorg#186 surfaced the same class of bug eight times: async work mutating selection state without checking that a newer user event had already superseded it. The duct-tape was a `_selGen` counter inlined at three call sites with slightly different shapes (filter handler, hashchange handler, boot deep-link). Each site bumped the counter, captured the snapshot, and rolled its own stale-check arrow. Extract the pattern as `freshSelectionToken()` in zoomWatcher (~10 lines): bumps `viewer._selGen`, returns an `isStale()` closure. The four call sites now just do: const isStale = freshSelectionToken(); ... await someWork(); if (isStale()) return; — with `isStale` passed into hydrateClusterUI's existing predicate parameter for nested-await freshness. Behavior-preserving refactor: verified locally that the three round-trips from PR isamplesorg#186's verification (boot at #h3=, uncheck non-cluster source, uncheck cluster's own source) produce identical results. Also documents the async-selection invariant explicitly in EXPLORER_STATE.md §4 (a new row for `viewer._selGen` plus an "Async-selection invariant" subsection) so future selection-touching code finds the rule before tripping the wire. Per isamplesorg#187 conversation between Claude Code and Codex: Step 2 of that issue (consolidating selection mutations into a `selectSample` / `selectCluster` / `clearSelection` controller) is YAGNI-deferred until the next feature actually has to touch click + boot + hashchange + filter + URL paths. A wholesale state-machine rewrite is not justified. Refs isamplesorg#187, isamplesorg#186. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rdhyee
left a comment
There was a problem hiding this comment.
Finding:
- P2:
freshSelectionToken()does not cover the click path that the new invariant says it covers.EXPLORER_STATE.mdsays cluster/sample click handlers callfreshSelectionToken(), but the helper is defined inside the laterzoomWatchercell, while the Cesium click handler is in the earlierviewercell. The click handler still has async awaits with no freshness check: sample detail load and cluster nearby-samples load. A slow cluster click can still callupdateSamples(samples)after the user has clicked a different sample/cluster. Either move the primitive to shared helper scope and use it in the viewer click handler, or narrow the docs/claim and leave click as explicit follow-up.
Verification: quarto render explorer.qmd and git diff --check origin/main...HEAD both passed.
Codex's review of PR isamplesorg#188 caught a P2 inconsistency: EXPLORER_STATE.md's async-selection invariant claimed coverage of cluster/sample click handlers, but freshSelectionToken() was defined inside the zoomWatcher cell — the click handler lives in the earlier viewer cell and couldn't see it. The click handler still had two unguarded awaits (sample detail load, cluster nearby-samples load) where a slow earlier click could repaint the side panel after the user clicked a different sample/cluster. Fix: move `freshSelectionToken(v)` to top-level OJS scope alongside `readHash` / `buildHash` so both cells can reach it. Take the viewer as a parameter (rather than closing over it) since the function is now defined before the viewer cell runs. Apply `isStale()` checks at the click handler's two await sites: sample-detail post-await and pre-DOM-write, cluster-nearby same. Doc adjusted to reflect the top-level definition. Behavior-preserving regression check: identical results to PR isamplesorg#186's verification suite (boot at #h3=, unchecked-non-cluster source, unchecked cluster source). Refs isamplesorg#187 (post-mortem), isamplesorg#188 (origin refactor PR — Codex's review of that diff surfaced this). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed `797605a` addressing Codex's P2 finding: The original PR put `freshSelectionToken()` in the zoomWatcher cell, but the cluster/sample click handler lives in the earlier viewer cell and couldn't see it — yet `EXPLORER_STATE.md`'s invariant claimed coverage of clicks. Two unguarded awaits in the click handler (sample detail load, cluster nearby-samples load) were the actual race surfaces: a slow earlier click could repaint the side panel after the user clicked a different sample/cluster. Fix:
Behavior-preserving regression check passes (PR #186's three-step suite returns identical results). This makes the fifth call site of `freshSelectionToken` and the seventh `isStale()` check. The doc claim now matches the code. |
rdhyee
left a comment
There was a problem hiding this comment.
Finding:
- P3:
EXPLORER_STATE.mdhas one stale location note for_selGen. The row forviewer._selGenstill saysfreshSelectionToken()is “in zoomWatcher cell,” but the latest patch correctly moved it top-level so both the viewer click handler andzoomWatchercan use it. The invariant text below is already updated; this row just needs the same wording.
No runtime issues found in the re-review. The click path now uses freshSelectionToken(v) and guards both async repaint sites.
Verification: quarto render explorer.qmd and git diff --check origin/main...HEAD both passed.
Codex's P3 nit: the table row still said "in zoomWatcher cell" while the helper is now top-level. The invariant text below was already correct; this just aligns the row.
Summary
Step 1 of #187 (post-mortem on PR #186's 6-round Codex loop). Behavior-preserving refactor.
What changed
The same class of bug surfaced eight times across six review rounds: async work mutating selection state without checking that a newer user event had already superseded it. The duct-tape was a
_selGencounter inlined at three call sites with slightly different shapes — each site bumped the counter, captured the snapshot, and rolled its own stale-check arrow.This PR extracts that pattern as
freshSelectionToken()(~10 lines) in the zoomWatcher cell:Each user-input event handler that touches selection now does:
isStaleis also passed throughhydrateClusterUI's existing predicate parameter so nested awaits bail before touching the DOM.Sites refactored
viewer._selGen = (viewer._selGen || 0) + 1; const filterSelGen = viewer._selGen;+ 4filterSelGen !== viewer._selGenchecksconst isStale = freshSelectionToken();+ 4isStale()checksconst selGen = viewer._selGen;+ 2 manual checksconst isStale = freshSelectionToken();+ 2isStale()checksconst bootSelGen = ...; const isBootStale = () => ...;+ 4isBootStale()checksconst isStale = freshSelectionToken();+ 4isStale()checksAfter the refactor, raw
_selGenmentions exist only inside the helper. No other change in behavior.Doc update
EXPLORER_STATE.md §4adds:viewer._selGen(was previously undocumented, slipped in over the 6 rounds)._globeState/ URL hash / side-panel DOM must checkisStale()after every await.YAGNI
Step 2 of #187 (consolidating selection mutations into a
selectSample/selectCluster/clearSelectioncontroller) is deferred per the synthesis with Codex on that issue — only worth doing if the next selection feature again has to touch click + boot + hashchange + filter + URL. Three call sites isn't enough to force consolidation. A wholesale state-machine rewrite is not justified.Verified locally
Re-ran the same three-step regression from PR #186's verification:
#h3=843f6d3ffffffff&h3=preserved&h3=droppedOut of scope
Refs #187 (post-mortem + Codex synthesis), #186 (origin PR),
EXPLORER_STATE.md(state contract).🤖 Generated with Claude Code