@xmachines/play-vue-demo
Examples / @xmachines/play-vue-demo
Vue 3 renderer demo for @xmachines/play-vue — 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 - Vue Composition API mapping to TC39 Signals lifecycle
- Non-browser invariant tests plus browser renderer coverage
Running the Demo
From the repository root:
npm installnpm run dev -w @xmachines/play-vue-demoThen open http://localhost:5173.
Step-by-Step Code Flow
Use this order to understand the implementation:
src/main.tscallsdefinePlayer({ machine: authMachine }), starts the actor, and mounts the Vue app.- The actor is provided to all components via
app.provide("actor", actor). src/App.vueinjects the actor, declares a typedComponentsMap<typeof authCatalog>fordemoViews, and builds theregistryResultwithdefineRegistry(authCatalog, { components: demoViews, actions })insidecomputed— action handlers usesatisfies ActionFnfor catalog-typed safety and dispatch toactor.send().<PlayUIProvider :actor="typedActor" :registryResult="registryResult"><PlayRenderer /></PlayUIProvider>observesactor.currentViewand renders the active spec.- A
NavBarSFC observesactorsignals directly for nav visibility. <DebugPanel :actor="typedActor" />shows live state, auth status, and current route.- HMR cleanup calls
actor.stop()viaimport.meta.hot.dispose. - Browser tests in
test/browser/validate startup and interaction behavior.
const createPlayer = definePlayer({ machine: authMachine });const actor = createPlayer() as AuthActor;actor.start();
const app = createApp(App);app.provide("actor", actor);app.mount("#app");<script setup lang="ts">import type { ComponentsMap } from "@xmachines/play-vue";import type { ActionFn } from "@json-render/vue";
const typedActor = assertNonNullable(inject<AuthActor>("actor"), "actor");
const demoViews: ComponentsMap<typeof authCatalog> = { Home: HomeSFC, About: AboutSFC, Contact: ContactSFC, Login: LoginSFC, Dashboard: DashboardSFC, Overview: OverviewSFC, Stats: StatsSFC, Profile: ProfileSFC, Settings: SettingsSFC, Navigation: NavigationSFC, NavBar: NavBarViewSFC,};
const registryResult = computed(() => defineRegistry(authCatalog, { components: demoViews, actions: { login: (async (args) => { typedActor.send({ type: "auth.login", username: assertNonNullable(args, "args").username, }); }) satisfies ActionFn<typeof authCatalog, "login">, logout: async () => typedActor.send({ type: "auth.logout" }), route: (async (args) => { const { to, params } = assertNonNullable(args, "args"); typedActor.send({ type: "play.route", to, ...(params != null && { params }), }); }) satisfies ActionFn<typeof authCatalog, "route">, }, }),);</script>
<template> <div class="demo-app" data-demo-shell> <header class="demo-header"> <h1 class="demo-title">XMachines Play Vue Demo</h1> <NavBar :actor="typedActor" /> </header> <main class="demo-content" data-demo-content> <PlayUIProvider :actor="typedActor" :registryResult="registryResult"> <PlayRenderer /> </PlayUIProvider> </main> <DebugPanel :actor="typedActor" /> </div></template>Key Files
src/main.ts- actor creation/start and Vue app mount with actor injectionsrc/App.vue- typedComponentsMapdeclaration, registry construction, andPlayUIProvider+PlayRenderercompositionsrc/catalog.ts-authCatalogbuilt viadefineCatalogwith the@json-render/vueschemasrc/components/- demo view components bound to catalog component keys (Home, Login, Dashboard, Profile, etc.)src/index.ts- re-exports all components and catalog for library-pattern testingtest/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 — Vue never decides which view to render. - Passive Infrastructure:
PlayRendererobservesactor.currentViewsignals only. It holds no business state and makes no routing decisions. - Signal-Only Reactivity: The bridge leverages Vue’s reactivity model internally to react precisely when actor signals update, without polluting the component tree with reactive refs holding business state.
Watcher Lifecycle and Cleanup Contract
This demo follows the canonical watcher lifecycle used across all @xmachines framework adapters:
notifyqueueMicrotaskgetPending()- Read actor signals and sync Vue-local render state
- 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 unmount.
Adapter Boundaries
PlayRenderer and defineRegistry stay passive infrastructure. Business validity remains actor-owned. The registry maps catalog component keys to Vue SFC implementations — the actor owns which key is active. The demo intentionally omits URL routing to isolate the renderer contract.
Available Scripts
These commands are defined in package.json:
| Command | Description |
|---|---|
npm run dev -w @xmachines/play-vue-demo | Start Vite dev server |
npm run build -w @xmachines/play-vue-demo | Build production bundle |
npm run preview -w @xmachines/play-vue-demo | Preview built bundle |
npm run test -w @xmachines/play-vue-demo | Run Vitest test suite |
npm run test:browser -w @xmachines/play-vue-demo | Run browser-focused Vitest suite |
Verification
Use these checks to validate README claims against the current demo implementation:
npm run test -w @xmachines/play-vue-demonpm run test:browser -w @xmachines/play-vue-demoExpected result: library-pattern invariant tests pass and the browser renderer suite completes.