@xmachines/play-svelte-demo
Examples / @xmachines/play-svelte-demo
Svelte 5 renderer demo for @xmachines/play-svelte — actor + PlayRenderer without a router.
What This Demonstrates
- Shared auth machine reused without framework-specific business logic
PlayRendererrendering actor-projected views with a typeddefineRegistrycatalog- Auth machine states drive view switching via
auth.login/auth.logoutevents only — no URL routing - Svelte 5 runes and
mountAPI mapped to atoms lifecycle - Non-browser invariant tests plus browser renderer coverage
- Stately inspector attached at creation via
options.inspect— the debug panel’s Show Inspector button opens the live machine in its own window - json-render 0.20 showcase pages driven purely by spec:
- Overview — two data planes side by side:
/context/tasks(machine context via the read-only/contextprojection;repeat+key,$item-filtered lists, per-row actions sending inspectable machine events) next to ephemeral store state (pushState/removeState/setStatebuilt-ins driving filter chips and toggles that survive context updates) - Stats — nested repeats (
repeat.statePath: { $item: "members" }) over/context/teams - Settings — declarative field validation, the
validateFormbuilt-in, a confirm-gatedresetFormhost action, andonSuccess/onErrorchains - Contact — a chained named action: submit validates, then routes home on success or writes
"$error.message"to state on failure
- Overview — two data planes side by side:
Running the Demo
From the repository root:
pnpm installpnpm --filter @xmachines/play-svelte-demo run devThen open http://localhost:3003.
Step-by-Step Code Flow
Use this order to understand the implementation:
src/main.tscreates the page’s inspector withcreateDemoInspector(), callsdefinePlayer({ machine: authMachine, options: { inspect: inspector.inspect } })to create a factory, invokes it to get an actor, starts it, and mountsApp.sveltevia Svelte’smountwith bothactorandinspectoras props.- The actor is passed to
Appas a prop. src/App.sveltecallsdefineRegistry(authCatalog, { components, actions })to build the typedregistryResult— real async action handlers dispatching toactor.send().App.sveltedelegates toShell.svelte, passingactor,registryResult, andinspector.Shell.svelterenders<PlayUIProvider><PlayRenderer /></PlayUIProvider>,<NavBar>, and<DebugPanel>— the latter two observe actor atoms directly viawatchAtominside$effect, and the debug panel’s Show Inspector button callsinspector.show().- HMR cleanup calls
actor.stop()viaimport.meta.hot.dispose. - Browser tests in
test/browser/validate startup and interaction behavior.
// src/main.ts (shape)const inspector = createDemoInspector();const createPlayer = definePlayer({ machine: authMachine, options: { inspect: inspector.inspect }, actor: compose(PlayerActor, withRouting, withView),});const actor = createPlayer();actor.start();
mount(App, { target: document.getElementById("app")!, props: { actor, inspector } });<!-- src/App.svelte (shape) --><script lang="ts"> import Shell from "./components/Shell.svelte";
let { actor, inspector }: { actor: AuthActor; inspector: DemoInspector } = $props();
const registryResult = defineRegistry(authCatalog, { components: demoComponents, // one shared map — Home…NavBar plus the 0.20 Panel/Field/Button leaves actions: createAuthActionHandlers(actor), // login/logout/route + task events + form actions });</script>
<Shell {actor} {registryResult} {inspector} />Key Files
src/main.ts- actor creation/start and Svelte app mount with actor as propsrc/App.svelte- registry construction, delegates rendering toShell.sveltesrc/components/Shell.svelte- shared shell withPlayUIProvider+PlayRenderer,NavBar, andDebugPanel(which owns the Show Inspector button)src/components/- demo view Svelte components bound to catalog component keystest/library-pattern.test.ts- architecture boundary and invariant assertionstest/browser/renderer-demo.browser.test.ts- browser-mode renderer coverage
State Machine & Architecture Details
The demo utilizes XMachines architectural invariants:
- Actor Authority: View components dispatch
auth.loginandauth.logoutevents to the actor. The actor evaluates guards and transitions — Svelte never decides which view to render. - Passive Infrastructure:
PlayRendererobservesactor.currentViewatoms only. It holds no business state and makes no routing decisions. - Atom-Only Reactivity: The bridge leverages Svelte 5 rune reactivity internally to react precisely when actor atoms update, without ad-hoc
$statestores for business logic.
Watcher Lifecycle and Cleanup Contract
This demo follows the canonical watcher lifecycle used across all @xmachines framework adapters:
- 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 Svelte-local render state
One subscription lives for the whole watch, and no caller arms a watcher again. Cleanup is explicit: actor.stop() is called during HMR disposal, and PlayRenderer handles internal watcher teardown natively on component destruction.
Adapter Boundaries
PlayRenderer and defineRegistry stay passive infrastructure. Business validity remains actor-owned. The registry maps catalog component keys to Svelte 5 component implementations — the actor owns which key is active. The Shell.svelte component is shared with all Svelte router demos, accepting an optional router prop for prop-shape parity.
Available Scripts
These commands are defined in package.json:
| Command | Description |
|---|---|
pnpm --filter @xmachines/play-svelte-demo run dev | Start Vite dev server (http://localhost:3003/) |
pnpm --filter @xmachines/play-svelte-demo run build | Build production bundle |
pnpm --filter @xmachines/play-svelte-demo run preview | Preview built bundle |
pnpm --filter @xmachines/play-svelte-demo run test | Run Vitest test suite |
pnpm --filter @xmachines/play-svelte-demo run test:browser | Run browser-focused Vitest suite |
Learn More
@xmachines/play-svelte— the Svelte 5 renderer package this demo exercises- Inspecting a Running Actor