Skip to content

@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
  • PlayRenderer rendering actor-projected views with a typed defineRegistry catalog
  • Auth machine states drive view switching via auth.login / auth.logout events only — no URL routing
  • Svelte 5 runes and mount API mapped to TC39 Signals lifecycle
  • Non-browser invariant tests plus browser renderer coverage

Running the Demo

From the repository root:

Terminal window
npm install
npm run dev -w @xmachines/play-svelte-demo

Then open http://localhost:5173.

Step-by-Step Code Flow

Use this order to understand the implementation:

  1. src/main.ts calls definePlayer({ machine: authMachine }) to create a factory, invokes it to get an actor, starts it, and mounts App.svelte via Svelte’s mount.
  2. The actor is passed to App as a prop.
  3. src/App.svelte calls defineRegistry(authCatalog, { components, actions }) to build the typed registryResult — real async action handlers dispatching to actor.send().
  4. App.svelte delegates to Shell.svelte, passing both actor and registryResult.
  5. Shell.svelte renders <PlayUIProvider><PlayRenderer /></PlayUIProvider>, <NavBar>, and <DebugPanel> — the latter two observe actor signals directly via watchSignal inside $effect.
  6. HMR cleanup calls actor.stop() via import.meta.hot.dispose.
  7. Browser tests in test/browser/ validate startup and interaction behavior.
// src/main.ts (shape)
const createPlayer = definePlayer({ machine: authMachine });
const actor = createPlayer();
actor.start();
mount(App, { target: document.getElementById("app")!, props: { actor } });
<!-- src/App.svelte (shape) -->
<script lang="ts">
let { actor }: { actor: AuthActor } = $props();
const registryResult = defineRegistry(authCatalog, {
components: {
Home, About, Contact, Login, Dashboard,
Overview, Stats, Profile, Settings, Navigation,
NavBar: NavBarView,
},
actions: {
login: async (args) =>
actor.send({ type: "auth.login", username: assertNonNullable(args, "args").username }),
logout: async () => actor.send({ type: "auth.logout" }),
route: async (args) => {
const { to, params } = assertNonNullable(args, "args");
actor.send({ type: "play.route", to, ...(params != null && { params }) });
},
},
});
</script>
<Shell {actor} {registryResult} />

Key Files

  • src/main.ts - actor creation/start and Svelte app mount with actor as prop
  • src/App.svelte - registry construction, delegates rendering to Shell.svelte
  • src/components/Shell.svelte - shared shell with PlayUIProvider + PlayRenderer, NavBar, and DebugPanel
  • src/components/ - demo view Svelte components bound to catalog component keys
  • test/library-pattern.test.ts - architecture boundary and invariant assertions
  • test/browser/renderer-demo.browser.test.ts - browser-mode renderer coverage

State Machine & Architecture Details

The demo utilizes XMachines architectural invariants:

  1. Actor Authority: View components dispatch auth.login and auth.logout events to the actor. The actor evaluates guards and transitions — Svelte never decides which view to render.
  2. Passive Infrastructure: PlayRenderer observes actor.currentView signals only. It holds no business state and makes no routing decisions.
  3. Signal-Only Reactivity: The bridge leverages Svelte 5 rune reactivity internally to react precisely when actor signals update, without ad-hoc $state stores for business logic.

Watcher Lifecycle and Cleanup Contract

This demo follows the canonical watcher lifecycle used across all @xmachines framework adapters:

  1. notify
  2. queueMicrotask
  3. getPending()
  4. Read actor signals and project Svelte-local render state
  5. Re-arm with watch()/watch(...signals)

Watcher notifications are one-shot. 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:

CommandDescription
npm run dev -w @xmachines/play-svelte-demoStart Vite dev server
npm run build -w @xmachines/play-svelte-demoBuild production bundle
npm run preview -w @xmachines/play-svelte-demoPreview built bundle
npm run test -w @xmachines/play-svelte-demoRun Vitest test suite
npm run test:browser -w @xmachines/play-svelte-demoRun browser-focused Vitest suite

Verification

Use these checks to validate README claims against the current demo implementation:

Terminal window
npm run test -w @xmachines/play-svelte-demo
npm run test:browser -w @xmachines/play-svelte-demo

Expected result: library-pattern invariant tests pass and the browser renderer suite completes.

Learn More

Type Aliases

Variables