feat(joint-router-avoid): add routeAllSync() and routeSubgraphSync() for the main-thread provider - #3491
Draft
kumilingus wants to merge 1 commit into
Draft
Conversation
kumilingus
force-pushed
the
feat/router-avoid-eager-main-thread
branch
from
August 26, 2026 21:08
54e99dd to
a6535a3
Compare
kumilingus
force-pushed
the
feat/router-avoid-eager-main-thread
branch
2 times, most recently
from
August 26, 2026 21:21
6a3bb81 to
6f388d4
Compare
kumilingus
marked this pull request as draft
August 26, 2026 21:24
kumilingus
force-pushed
the
feat/router-avoid-eager-main-thread
branch
from
August 27, 2026 14:02
6f388d4 to
d0125e0
Compare
routeAll()/routeSubgraph() keep their uniform asynchronous contract on every provider. The new synchronous variants return the RoutingResult directly for the main-thread provider - every route is on its link when they return and errors throw out of the call, matching started-mode behaviour - and fail loudly otherwise: they throw when a Worker provider is in use or when an asynchronous pass is still in flight (running then would have the queued pass silently override the result a microtask later). RouterService.isSynchronous exposes the provider capability so portable code can branch before calling them. MainThreadProvider.sync and RouterService.sync are no longer async (they never awaited anything), so errors on the synchronous path escape the calling frame instead of being laundered into rejections. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kumilingus
force-pushed
the
feat/router-avoid-eager-main-thread
branch
from
August 27, 2026 14:13
d0125e0 to
500cc7f
Compare
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
With the main-thread provider a routing pass is synchronous WASM work end to end (raw libavoid is fully synchronous); the only asynchrony in
routeAll()/routeSubgraph()is the wrapper's own pass queue andasynckeywords. That forced every consumer into an async context even when nothing asynchronous happens — synchronous export, headless pipelines, fixtures and tests all needed promise plumbing.Change (option C of the sync-routing analysis)
routeAll()/routeSubgraph()— unchanged contract. Queued, asynchronous, errors reject the promise, identical timing on every provider.routeAllSync()/routeSubgraphSync()— new, returnvoid. Every route is on its link when they return; errors (a throwing consumer callback, a WASM abort) throw out of the call, the same way a failing callback throws out of the originatingcell.set()in started mode. There is nothing to return: the pass either completed or threw —RoutingResult's'cancelled'only exists sodestroy()can interrupt a queued async pass without rejecting, which cannot happen to a pass that runs inside the call. Loud failures instead of silent ordering differences:worker: true) — "userouteAll()instead";routeAll()/routeSubgraph()pass is still in flight — running now would have the queued pass silently override the result a microtask later;destroy().RouterService.isSynchronous— new getter,truewith the main-thread provider. Portable code checks it before reaching for the sync methods.Under the hood
MainThreadProvider.syncandRouterService.syncare no longerasync(they never awaited anything) so errors on the synchronous path escape the calling frame instead of being laundered into rejections; the promise-returning API still sees them as rejections throughperformRoute'sawait. Side effect worth knowing: with the main-thread provider, an error duringstart()'s initial sync now throws out ofstart()instead of surfacing as an unhandled rejection — consistent with how started-mode incremental errors already behave.Provider.isSynchronousis the capability flag driving all of this (trueforMainThreadProvider,falseforWorkerProvider).Tests
TDD — written first and watched fail:
routeAllSync() applies the routes and returns the result during the callrouteSubgraphSync() routes exactly the given cells during the callrouteAll() stays asynchronous: routes land only once the promise resolves— pins the unchanged contracta synchronous pass refuses to run while an asynchronous one is in flight— and is allowed again once it settlesa throwing consumer callback escapes routeAllSync() as a synchronous throw/the same consumer error rejects the asynchronous routeAll()isSynchronous reports true for the main-thread providerThe Worker-provider throw is not exercised in the karma suite (no Worker script can be spawned there); it is a one-line guard on
provider.isSynchronous.35/35 passing; lint clean. Changesets:
@joint/router-avoidminor ×2 (sync methods,isSynchronous).Notes
start(), againstmaster): unchanged async passes keep that supersession check; a sync pass completes beforestart()could be called.standalone-link-routing.mdx, docs repo) should gain a section on the sync variants once this lands.🤖 Generated with Claude Code