Skip to content

Route worker responses by correlation id#50

Merged
kmatzen merged 1 commit into
mainfrom
worker-bridge-correlation-ids
Jul 25, 2026
Merged

Route worker responses by correlation id#50
kmatzen merged 1 commit into
mainfrom
worker-bridge-correlation-ids

Conversation

@kmatzen

@kmatzen kmatzen commented Jul 18, 2026

Copy link
Copy Markdown
Owner

The WorkerBridge held a single responseHandler slot and dispatched worker responses by message type alone, so concurrent requests displaced and settled each other.

TLA+ models of the old and new protocols are in specs/. TLC produced two counterexamples against the old design, both replayed as tests in workerBridge.test.ts:

Cross-talk between evaluates. Two evaluates in flight: the first's sdf response reached the second's handler, passed its seq === evalSeq check (it was the newest), and resolved the second promise with the first's geometry. The staleness guard checked whether the handler was current, never whether the response matched it — so wrong geometry could reach the viewport.

Export orphaned by an edit. useEvaluator fires on any store change and overwrote the export's handler, so the exportResult matched neither branch of the evaluate handler and was discarded. Toolbar's await never returned.

Changes

  • Every request carries an rid that the worker echoes on all responses, including progress and errors.
  • The single handler slot becomes a map keyed by rid. Routing never consults message type.
  • Superseded evaluates resolve(null) instead of returning unsettled — useEvaluator already has its own evalSeq guard.
  • worker.onerror rejects everything outstanding rather than only logging.
  • evaluate now bumps evalSeq before awaiting readyPromise rather than after, so sequence numbers follow call order rather than ready-resolution order.

Verification

check result
tsc --noEmit exit 0
new tests, this branch 6/6 pass
new tests, old design 5 fail, 1 pass
full suite, this branch 102/103
full suite, main baseline 97/97

The single full-suite failure is simplify.test.ts > preserves mesh validity, a pre-existing CPU-bound test with a marginal 5000ms timeout. It passes in isolation and its failure count varied run-to-run on identical code; nothing in this change touches simplifyMesh or its imports.

specs/check.sh runs both models through TLC. The fixed spec checks exhaustively at MaxReq = 4 (2,535 states, depth 13) including liveness under weak fairness. tla2tools.jar is gitignored and fetched on first run.

Follow-ups

#51 — cancel in-flight worker jobs. There is still no cancellation. Every request now settles, but a 256³ export still blocks every evaluate queued behind it, since the worker runs each message to completion. The viewport will stall during exports as before — it just won't hang permanently. Approaches and the race worth specifying are laid out in the issue.

#52 — a more rigorous way to verify rendering correctness. Out of scope here, but surfaced while reading the SDF pipeline: evaluate.ts and codegen.ts are two independent implementations of every operation, and nothing checks they agree.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

🚀 Preview deployed: https://worker-bridge-correlation-id.sinter.pages.dev

(updates on every push to this PR)

The WorkerBridge held a single responseHandler slot and dispatched worker
responses by message *type* alone, so concurrent requests displaced and
settled each other. TLA+ models of the old and new protocols are in specs/;
TLC produced two counterexamples against the old design, both replayed as
tests in workerBridge.test.ts:

- Two evaluates in flight: the first's `sdf` response reached the second's
  handler, passed its `seq === evalSeq` check (it *was* the newest), and
  resolved the second promise with the first's geometry. The staleness guard
  checked whether the handler was current, never whether the response matched
  it.

- An export displaced by an edit: `useEvaluator` fires on any store change and
  overwrote the export's handler, so the `exportResult` matched neither branch
  of the evaluate handler and was discarded. Toolbar's await never returned.

Every request now carries an rid that the worker echoes on all responses
including progress and errors, and the handler slot becomes a map keyed by
rid. Superseded evaluates resolve(null) instead of returning unsettled —
useEvaluator already has its own evalSeq guard. worker.onerror now rejects
everything outstanding rather than only logging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kmatzen
kmatzen force-pushed the worker-bridge-correlation-ids branch from 1ac903f to 9d7a12b Compare July 25, 2026 17:00
@kmatzen
kmatzen merged commit 03f0a0f into main Jul 25, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant