Skip to content

Worker routes (service-worker handlers) + worker redirect & bundle fixes#3

Merged
siffogh merged 2 commits into
mainfrom
feat/worker-functions
Jun 29, 2026
Merged

Worker routes (service-worker handlers) + worker redirect & bundle fixes#3
siffogh merged 2 commits into
mainfrom
feat/worker-functions

Conversation

@siffogh

@siffogh siffogh commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds worker routes — the mirror of TanStack server routes, but the handlers run in the service worker. Declare them with the worker route option; the route's path is the identity (no name, no codegen — the router is the dispatch table). This lets a single stateful client (a local-first / sync-engine client like Zero or Replicache) live in the worker so reads and writes both go through it, and a hard-load WSR render reflects exactly what the user just did — one client, one store, no cross-client round-trip, no stale flash.

// src/routes/api/todos.ts
export const Route = createFileRoute("/api/todos")({
  worker: {
    handlers: {
      GET: async () => Response.json(await getClient().list()),
      POST: async ({ request }) => { await getClient().add(await request.json()); return new Response(null, { status: 204 }); },
    },
  },
  // optional: a `server` handler for the same method = the no-worker (SSR/first-load) fallback
});

Call them with workerFetch(path, init), which resolves in the right place:

  • in the worker (a wsr loader on hard load) → runs the matched handler directly (a worker can't fetch itself);
  • on the main thread (nav, event handlers) → fetches the path, which the worker intercepts;
  • on the origin server (SSR, no worker) → 503, so the caller falls back (e.g. empty shell).

broadcastToClients(message) pushes invalidations to pages.

Supersedes the earlier createWorkerFn approach in this PR: routing by path drops the explicit name and needs no build-time transform — same ergonomics as server routes.

Also in here (fixes surfaced building on WSR)

  • Honor thrown redirects. A redirect() thrown in beforeLoad/loader was captured on router state but never acted on, so the worker rendered the original route instead of redirecting (e.g. an auth gate → /login). renderRequest now returns a real redirect Response.
  • Keep the dev worker under the SW script-size limit. The inline sourcemap ~doubled the bundle and could push it past Chromium's ~6 MB limit ("Failed to access storage"); the worker build no longer inlines a sourcemap.
  • Single React/router copy in the worker bundle (resolved from the app root) so a duplicate instance can't make SSR hooks read null.

Tests & docs

  • worker-fetch.test.ts (dispatch, request gating, the worker/main/server paths) and worker-redirect.test.ts. 47 tests passing.
  • README: Worker routes section + API entry. Changeset included (minor).

(Branch name is feat/worker-functions for history reasons; the feature is worker routes.)

@siffogh siffogh force-pushed the feat/worker-functions branch from 9f613bb to a7dd059 Compare June 29, 2026 12:04
@siffogh siffogh changed the title Worker functions (service-worker-side RPC) + worker redirect & bundle fixes Worker routes (service-worker handlers) + worker redirect & bundle fixes Jun 29, 2026
@siffogh siffogh force-pushed the feat/worker-functions branch from 38859eb to a7dd059 Compare June 29, 2026 12:32
siffogh added 2 commits June 29, 2026 16:37
Drop the inline sourcemap from the worker build — its base64 map roughly
doubled the served script and could push it past Chromium's ~6MB
service-worker script-size limit (registration fails with 'Failed to
access storage'). Also resolve react / react-dom / @tanstack/react-router
/ @tanstack/router-core from the app root so the worker bundles a SINGLE
copy; a duplicate React/router instance makes SSR hooks read null.
Worker routes are the mirror of TanStack server routes, but the handlers
run in the service worker. Declare them with the `worker` route option
(createFileRoute('/api/x')({ worker: { handlers: { GET, POST, … } } })) —
the route's PATH is the identity (no name, no codegen; the router is the
dispatch table). A single stateful client can live in the worker so reads
and writes share one store. Call them with workerFetch(path, init): runs
the handler directly in the worker (e.g. a wsr loader), fetches the path
on the main thread (the worker intercepts it), 503 on the origin so callers
fall back. broadcastToClients posts invalidations to pages.

Also honor a redirect() thrown in beforeLoad/loader: it was captured on
router state but never acted on, so the worker rendered the original route
instead of redirecting. renderRequest now returns a real redirect Response.

Unit tests for both (47 passing) + a README 'Worker routes' section.
@siffogh siffogh force-pushed the feat/worker-functions branch from a7dd059 to 3e8d18b Compare June 29, 2026 12:38
@siffogh siffogh merged commit 2b158fc into main Jun 29, 2026
2 checks passed
@siffogh siffogh deleted the feat/worker-functions branch June 29, 2026 12:39
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