fix(joint-router-avoid): cancel a queued one-shot pass superseded by start() - #3490
Merged
Geliogabalus merged 2 commits intoAug 28, 2026
Merged
Conversation
…start()
routeAll()/routeSubgraph() defer their pass by a microtask (the pass
queue), but start()'s full-graph sync ran outside the queue - so a pass
still queued when start() was called ran after the full sync and reset
the engine to just that pass's cells while the graph listener was live.
A live listener referencing an element the engine no longer holds
aborts the WASM module irrecoverably.
The queued pass now checks isStarted when it runs and resolves with
{ status: 'cancelled' } - start()'s own sync has already produced the
correct engine state, so there is nothing left for the pass to add.
start()'s synchronous main-thread timing is unchanged and now pinned
by a test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Geliogabalus
approved these changes
Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Implements the §3.3 fix from the sync-routing analysis:
routeAll()/routeSubgraph()defer their pass by a microtask (the pass queue), butstart()'s full-graph sync ran outside the queue. A pass still queued whenstart()was called therefore ran after the full sync and reset the engine to just that pass's cells while the graph listener was live:A live listener referencing an element the engine does not hold aborts the WASM module irrecoverably (per
start()'s own doc), so this is a crash path, not just wrong routes.Fix
The queued pass re-checks
isStartedwhen it actually runs and resolves with{ status: 'cancelled' }— the same supersession contractdestroy()already has.start()'s own sync has already produced the correct engine state; the final routes are identical to running the pass first and full-syncing after it, so cancelling is both safer and honest to the caller.Considered and rejected: serializing
backgroundSync()through the pass queue (the analysis's literal option A). Chaining always defers by a microtask, which breaksstart()'s synchronous main-thread behaviour (existing tests assert routes are applied whenstart()returns); an eager-when-idle counter cannot be made race-free because promise settlement is never observable synchronously.Tests
TDD — regression written first and watched fail (the moved element's link outside the subgraph never routed):
a queued routeSubgraph() pass is superseded by start(), leaving the engine holding the full graph— asserts thecancelledstatus and that a link outside the one-shot subset still routes while startedstart() with no pass in flight still applies routes synchronously (main thread)— pins the timing the rejected approach would have broken30/30 passing; lint clean. Changeset:
@joint/router-avoidpatch.Out of scope, tracked separately: the explicit synchronous API (
routeAllSync(), option C of the analysis) and the worker-mode stale-route generation race.🤖 Generated with Claude Code