// `createRouter` — the only piece of `RouterCore` setup that's framework-specific. // `RouterCore`'s constructor takes `(options, getStoreFactory)`; react-router // passes a factory that, on the client, builds REACTIVE atoms (`createAtom`/`batch` // from `@tanstack/store`) and, on the server, non-reactive snapshot stores. Those // atoms are framework-agnostic — `createAtom` lives in `@tanstack/store`, not in // `@tanstack/react-store` — so octane reuses the exact same factory. The reactive // atoms expose `.subscribe(cb) → { unsubscribe }` + `.get()`, which `useStore` binds // to octane's `useSyncExternalStore`. import { RouterCore, createNonReactiveMutableStore, createNonReactiveReadonlyStore, } from '@tanstack/router-core'; import { createAtom, batch } from '@tanstack/store'; import { startTransition } from 'octane'; import type { RouterHistory } from '@tanstack/history'; import type { AnyRoute, CreateRouterFn, RouterConstructorOptions, TrailingSlashOption, } from '@tanstack/router-core'; const isServerEnv = typeof document === 'undefined'; // Batch router mutations atomically, including resolved commits delivered by a // later View Transition callback. The transition scope preserves navigation // bookkeeping; useSyncExternalStore notifications still render urgently. Routes // that keep stale content while new data loads should defer their render input // with useDeferredValue. Server snapshots use the non-reactive factory below. const octaneStoreFactory = (opts: { isServer?: boolean }) => { if (opts?.isServer ?? isServerEnv) { return { createMutableStore: createNonReactiveMutableStore, createReadonlyStore: createNonReactiveReadonlyStore, batch: (fn: () => void) => fn(), }; } return { createMutableStore: createAtom, createReadonlyStore: createAtom, batch: (fn: () => void) => startTransition(() => batch(fn)), }; }; export class Router< in out TRouteTree extends AnyRoute, in out TTrailingSlashOption extends TrailingSlashOption = 'never', in out TDefaultStructuralSharingOption extends boolean = false, in out TRouterHistory extends RouterHistory = RouterHistory, in out TDehydrated extends Record = Record, > extends RouterCore< TRouteTree, TTrailingSlashOption, TDefaultStructuralSharingOption, TRouterHistory, TDehydrated > { constructor( options: RouterConstructorOptions< TRouteTree, TTrailingSlashOption, TDefaultStructuralSharingOption, TRouterHistory, TDehydrated >, ) { super(options, octaneStoreFactory); // router-core starts the resolved-match commit through startViewTransition, // whose browser callback may run after router-core's load promise resolves. // Track those callbacks so `await router.load()` is a real render-readiness // boundary: the active match tree is committed before a consumer's first // render or hydration pass. const coreLoad = this.load.bind(this); const coreStartViewTransition = this.startViewTransition.bind(this); const pendingViewCommits = new Set>(); const activeLoadScopes = new Set>>(); this.startViewTransition = (fn: () => Promise) => { let resolveCommit!: () => void; let rejectCommit!: (error: unknown) => void; const commit = new Promise((resolve, reject) => { resolveCommit = resolve; rejectCommit = reject; }); pendingViewCommits.add(commit); for (const scope of activeLoadScopes) scope.add(commit); // Keep only unsettled callbacks globally. A later load waits for a prior // callback so no mutation can land after its readiness boundary, but only // the active scopes above own (and therefore propagate) this failure. void commit.then( () => pendingViewCommits.delete(commit), () => pendingViewCommits.delete(commit), ); const runCommit = async () => { try { await fn(); resolveCommit(); } catch (error) { rejectCommit(error); } }; try { coreStartViewTransition(runCommit); } catch (error) { rejectCommit(error); throw error; } }; this.load = async (...args: any[]) => { const prerequisiteCommits = new Set(pendingViewCommits); const viewCommits = new Set>(); activeLoadScopes.add(viewCommits); let hasLoadError = false; let loadError: unknown; let result: void; try { result = await coreLoad(...args); } catch (error) { hasLoadError = true; loadError = error; } finally { // All commits started by this core load are now registered. Stop // accepting commits from later navigations before awaiting this scope. activeLoadScopes.delete(viewCommits); } let hasCommitError = false; let commitError: unknown; const [, outcomes] = await Promise.all([ Promise.allSettled(prerequisiteCommits), Promise.allSettled(viewCommits), ]); const rejected = outcomes.find((outcome) => outcome.status === 'rejected'); if (rejected?.status === 'rejected') { hasCommitError = true; commitError = rejected.reason; } if (hasLoadError) throw loadError; if (hasCommitError) throw commitError; // RouterCore derives the final HTTP status immediately after its internal // load promise resolves. A platform-deferred View Transition can commit the // new tree only after that point, so finalize every branch once the // render-ready tree is present. RouterCore has already committed a // redirect and its HTTP status together, so preserve that authoritative // status; otherwise a successful tree must also clear a stale 404/500. const state = this.state; const statusCode = state.redirect != null ? state.statusCode : this.hasNotFoundMatch() ? 404 : state.matches.some((match: any) => match.status === 'error') ? 500 : 200; this.stores.statusCode.set(statusCode); return result; }; } } export const createRouter: CreateRouterFn = (options) => new Router(options);