mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(explorer): dedupe temporal snapshot requests and apply latest-wins The temporal snapshot effect fetched /api/temporal/snapshot with no idempotency or ordering guards. Upstream churn (timeline recreation while bounds settle, play ticks resetting the playhead, drag events) could re-request the same `at` repeatedly, and with variable network latency an older position's response could land after a newer one's, overwriting the active-node count, so the chip visibly lagged the scrubber. Add a small stateful guard module (temporalSnapshotGuards.ts) built around a per-position cache, keyed by the debounced timestamp's primitive millisecond value rather than the Date object, so upstream object-identity churn cannot defeat the dedup on its own: - at most one in-flight request per scrubber position, so identical `at` values arriving while a request is pending are dropped instead of firing a fresh fetch, breaking the idle/play polling loop; - successful snapshots are cached per position and re-applied when the scrubber returns to it (play wrap-around, back-scrubbing) without a network round trip; - a response is applied only while the scrubber is still on the position it was requested for, so an out-of-order response can never clobber a newer position's count; - failed, cancelled, or superseded requests release their position so it can be fetched again the next time it's visited, rather than stalling it permanently; - reset() drops all cached and in-flight state when the underlying graph summary changes (reload/retry), since snapshots cached against the previous graph no longer describe anything real. Keyed on the summary query's data identity, which react-query keeps stable (staleTime: Infinity plus structural sharing) unless the graph data itself was replaced, so reset fires exactly on a real reload and not on cosmetic re-renders. The snapshot effect is wired through the guards end to end: begin() returns either a fresh sequence number to fetch under or a cached snapshot to reapply directly; the same shouldApply()/apply() gate handles both the network and cached-reapply paths so they can't drift apart; finish() runs from both the fetch's failure branch and its cleanup function, so a cancelled or failed request is always retryable on the next visit instead of leaving its position stuck in-flight. 16 unit tests cover dedup, independent positions, revisit re-apply, play wrap-around, failure retry, stale-sequence protection (a late response or a late release from a superseded request cannot act on a newer request's position), reset-on-reload, and cache-bound eviction. Closes #1128