Skip to content

feat(joint-router-avoid): add routeAllSync() and routeSubgraphSync() for the main-thread provider - #3491

Draft
kumilingus wants to merge 1 commit into
clientIO:devfrom
kumilingus:feat/router-avoid-eager-main-thread
Draft

feat(joint-router-avoid): add routeAllSync() and routeSubgraphSync() for the main-thread provider#3491
kumilingus wants to merge 1 commit into
clientIO:devfrom
kumilingus:feat/router-avoid-eager-main-thread

Conversation

@kumilingus

@kumilingus kumilingus commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

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 and async keywords. 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, return void. 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 originating cell.set() in started mode. There is nothing to return: the pass either completed or threw — RoutingResult's 'cancelled' only exists so destroy() 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:
    • throw when a Worker provider is in use (worker: true) — "use routeAll() instead";
    • throw when a routeAll()/routeSubgraph() pass is still in flight — running now would have the queued pass silently override the result a microtask later;
    • throw when called after destroy().
  • RouterService.isSynchronous — new getter, true with the main-thread provider. Portable code checks it before reaching for the sync methods.

Under the hood 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; the promise-returning API still sees them as rejections through performRoute's await. Side effect worth knowing: with the main-thread provider, an error during start()'s initial sync now throws out of start() instead of surfacing as an unhandled rejection — consistent with how started-mode incremental errors already behave.

Provider.isSynchronous is the capability flag driving all of this (true for MainThreadProvider, false for WorkerProvider).

Tests

TDD — written first and watched fail:

  • routeAllSync() applies the routes and returns the result during the call
  • routeSubgraphSync() routes exactly the given cells during the call
  • routeAll() stays asynchronous: routes land only once the promise resolves — pins the unchanged contract
  • a synchronous pass refuses to run while an asynchronous one is in flight — and is allowed again once it settles
  • a throwing consumer callback escapes routeAllSync() as a synchronous throw / the same consumer error rejects the asynchronous routeAll()
  • isSynchronous reports true for the main-thread provider

The 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-avoid minor ×2 (sync methods, isSynchronous).

Notes

🤖 Generated with Claude Code

@kumilingus
kumilingus force-pushed the feat/router-avoid-eager-main-thread branch from 54e99dd to a6535a3 Compare August 26, 2026 21:08
@kumilingus kumilingus changed the title feat(joint-router-avoid): run one-shot routing passes during the call when idle feat(joint-router-avoid): run one-shot routing passes during the call with a synchronous provider Aug 26, 2026
@kumilingus
kumilingus force-pushed the feat/router-avoid-eager-main-thread branch 2 times, most recently from 6a3bb81 to 6f388d4 Compare August 26, 2026 21:21
@kumilingus
kumilingus marked this pull request as draft August 26, 2026 21:24
@kumilingus
kumilingus force-pushed the feat/router-avoid-eager-main-thread branch from 6f388d4 to d0125e0 Compare August 27, 2026 14:02
@kumilingus kumilingus changed the title feat(joint-router-avoid): run one-shot routing passes during the call with a synchronous provider feat(joint-router-avoid): add routeAllSync() and routeSubgraphSync() for the main-thread provider Aug 27, 2026
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
kumilingus force-pushed the feat/router-avoid-eager-main-thread branch from d0125e0 to 500cc7f Compare August 27, 2026 14:13
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