@xmachines/play-dom-router-demo
Examples / @xmachines/play-dom-router-demo
Pure TypeScript integration demo for @xmachines/play-dom-router — actor-authoritative routing with the Browser History API and no framework.
What This Demonstrates
- Shared auth machine reused without framework-specific business logic
connectRouter+createBrowserHistorywiring actor ↔ browser History API- Shell-driven DOM rendering via
connectRendererwith actor-authoritative navigation - Manual atom watcher lifecycle — the mechanism that framework adapters abstract
- Non-browser invariant tests plus browser E2E coverage
- Stately inspector attached at creation via
options.inspect— the debug panel’s Show Inspector button opens the live machine in its own window
Running the Demo
From the repository root:
pnpm installpnpm --filter @xmachines/play-dom-router-demo run devThen open http://localhost:3005.
Step-by-Step Code Flow
Use this order to understand the implementation:
src/main.tslooks up the host element (throws if absent) and callsinitApp(app)fromsrc/runtime.ts. It tears nothing down at the end of the page, because the browser frees the page.src/runtime.tscreates the page’s inspector withcreateDemoInspector(), creates the actor from the shared machine viadefinePlayerwithoptions.inspectwired to it, starts it, initializes router sync viainitRouter(actor), mounts the shared DOM shell viainitShell(actor, host, inspector), and returns one cleanup function.initRouter(actor)fromsrc/router.tsextracts route metadata, creates a browser history wrapper, instantiates a router, and callsconnectRouterto wire bidirectional actor ↔ URL sync.initShell(actor, host, inspector)is imported from@xmachines/play-dom-demo, so the router demo reuses the shared DOM shell — inspector button included — instead of duplicating the scaffold and renderer wiring.- Navigation buttons and links dispatch
play.route,auth.login, andauth.logoutevents directly to the actor — no URL mutations from the shell. - Browser tests in
test/browser/validate startup rendering and the full auth route flow.
// src/main.ts (shape)const app = document.getElementById("app");if (!app) throw new Error("Root element not found");
// The app lives as long as the page, so the demo never tears it down: the browser frees// it with the page. `initApp` returns a cleanup function for a host that removes the app// before the page ends.initApp(app);// src/router.ts (shape)export function initRouter(actor: RoutableActor) { const routeTree = extractMachineRoutes(authMachine); const routeMap = createRouteMap(authMachine); const history = createBrowserHistory({ window }); const router = createRouter({ routeTree, history }); const disconnect = connectRouter({ actor, router, routeMap });
return () => { disconnect(); router.destroy(); };}// src/runtime.ts (shape)export function initApp(host: HTMLElement): () => void { const inspector = createDemoInspector(); const createPlayer = definePlayer({ machine: authMachine, options: { inspect: inspector.inspect }, }); const actor = createPlayer(); actor.start();
const cleanupRouter = initRouter(actor); const cleanupShell = initShell(actor, host, inspector);
return () => { cleanupShell(); cleanupRouter(); actor.stop(); };}Key Files
src/main.ts- host lookup and app bootstrapsrc/runtime.ts- actor startup, router initialization, and shared shell wiringsrc/router.ts-createBrowserHistory,createRouter, andconnectRouterwiring@xmachines/play-dom-demo- shared DOM shell, NavBar, DebugPanel (including the Show Inspector button), and renderer wiring reused by this router demotest/library-pattern.test.ts- architecture boundary and invariant assertionstest/browser/shared-demo.browser.test.ts- startup rendering and full auth route browser flow
State Machine & Architecture Details
The demo utilizes XMachines architectural invariants at the lowest level:
- Actor Authority: Intercepting browser
popstateevents or navigation button clicks dispatches aplay.routeevent to the actor. The actor processes the request against the current state, ensuring any auth logic is correctly applied. - Passive Infrastructure: The router simply updates
window.historyand the DOM solely renders HTML based on whatactor.currentViewyields. Neither holds business state. - Atom-Only Reactivity: The integration registers
watchAtomobservers that trigger DOM reconciliation, illustrating the underlying mechanism that framework adapters abstract.
Watcher Lifecycle and Cleanup Contract
Even in the vanilla demo, watcher handling follows the canonical lifecycle:
- The atom notifies the subscription, synchronously, from the
setthat changed it watchAtomqueues one microtask, and it drops each duplicate notification of that batch- The microtask reads the actor atoms and projects DOM state
One subscription lives for the whole watch, and no caller arms a watcher again. Router/shell teardown must explicitly call cleanup (disconnect/unwatch) and not rely on GC-only behavior. In vanilla TypeScript, keep the cleanup functions, and call them when the app ends before the page does, or during hot-module replacement. Do not call them on beforeunload: that event also fires when the page goes into the back/forward cache, and Back then shows a page that does not respond. At the end of the page, the browser frees everything.
Adapter Boundaries
connectRouter and browser-history wiring (from @xmachines/play-dom-router) are passive infrastructure. Actor logic remains authoritative for route validity, and shared synchronization policy stays in RouterBridgeBase for framework bridges. This demo proves frameworks are not required for Play Architecture to work.
Available Scripts
These commands are defined in package.json:
| Command | Description |
|---|---|
pnpm --filter @xmachines/play-dom-router-demo run dev | Start Vite dev server (http://localhost:3005/) |
pnpm --filter @xmachines/play-dom-router-demo run build | Build production bundle |
pnpm --filter @xmachines/play-dom-router-demo run preview | Preview built bundle |
pnpm --filter @xmachines/play-dom-router-demo run test | Run Vitest test suite |
pnpm --filter @xmachines/play-dom-router-demo run test:browser | Run browser-focused Vitest suite |