Skip to content

@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.

License: MIT

What This Demonstrates

  • Shared auth machine reused without framework-specific business logic
  • connectRouter + createBrowserHistory wiring actor ↔ browser History API
  • Shell-driven DOM rendering via connectRenderer with 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:

Terminal window
pnpm install
pnpm --filter @xmachines/play-dom-router-demo run dev

Then open http://localhost:3005.

Step-by-Step Code Flow

Use this order to understand the implementation:

  1. src/main.ts looks up the host element (throws if absent) and calls initApp(app) from src/runtime.ts. It tears nothing down at the end of the page, because the browser frees the page.
  2. src/runtime.ts creates the page’s inspector with createDemoInspector(), creates the actor from the shared machine via definePlayer with options.inspect wired to it, starts it, initializes router sync via initRouter(actor), mounts the shared DOM shell via initShell(actor, host, inspector), and returns one cleanup function.
  3. initRouter(actor) from src/router.ts extracts route metadata, creates a browser history wrapper, instantiates a router, and calls connectRouter to wire bidirectional actor ↔ URL sync.
  4. 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.
  5. Navigation buttons and links dispatch play.route, auth.login, and auth.logout events directly to the actor — no URL mutations from the shell.
  6. 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 },
actor: compose(PlayerActor, withRouting, withView),
});
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 bootstrap
  • src/runtime.ts - actor startup, router initialization, and shared shell wiring
  • src/router.ts - createBrowserHistory, createRouter, and connectRouter wiring
  • @xmachines/play-dom-demo - shared DOM shell, NavBar, DebugPanel (including the Show Inspector button), and renderer wiring reused by this router demo
  • test/library-pattern.test.ts - architecture boundary and invariant assertions
  • test/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:

  1. Actor Authority: Intercepting browser popstate events or navigation button clicks dispatches a play.route event to the actor. The actor processes the request against the current state, ensuring any auth logic is correctly applied.
  2. Passive Infrastructure: The router simply updates window.history and the DOM solely renders HTML based on what actor.currentView yields. Neither holds business state.
  3. Atom-Only Reactivity: The integration registers watchAtom observers 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:

  1. The atom notifies the subscription, synchronously, from the set that changed it
  2. watchAtom queues one microtask, and it drops each duplicate notification of that batch
  3. 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:

CommandDescription
pnpm --filter @xmachines/play-dom-router-demo run devStart Vite dev server (http://localhost:3005/)
pnpm --filter @xmachines/play-dom-router-demo run buildBuild production bundle
pnpm --filter @xmachines/play-dom-router-demo run previewPreview built bundle
pnpm --filter @xmachines/play-dom-router-demo run testRun Vitest test suite
pnpm --filter @xmachines/play-dom-router-demo run test:browserRun browser-focused Vitest suite

Learn More