@xmachines/play-xstate 1.0.0-beta.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -338
  3. package/dist/define-player.d.ts +6 -56
  4. package/dist/define-player.d.ts.map +1 -1
  5. package/dist/define-player.js +7 -61
  6. package/dist/define-player.js.map +1 -1
  7. package/dist/define-player.typecheck.js +6 -0
  8. package/dist/define-player.typecheck.js.map +1 -1
  9. package/dist/errors.d.ts +170 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +202 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/guards/compose.d.ts +9 -2
  14. package/dist/guards/compose.d.ts.map +1 -1
  15. package/dist/guards/compose.js +3 -2
  16. package/dist/guards/compose.js.map +1 -1
  17. package/dist/guards/helpers.d.ts +16 -1
  18. package/dist/guards/helpers.d.ts.map +1 -1
  19. package/dist/guards/helpers.js +16 -35
  20. package/dist/guards/helpers.js.map +1 -1
  21. package/dist/guards/index.d.ts +2 -1
  22. package/dist/guards/index.d.ts.map +1 -1
  23. package/dist/guards/index.js +1 -1
  24. package/dist/guards/index.js.map +1 -1
  25. package/dist/guards/types.d.ts +1 -1
  26. package/dist/guards/types.d.ts.map +1 -1
  27. package/dist/index.d.ts +6 -10
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/player-actor.d.ts +134 -27
  32. package/dist/player-actor.d.ts.map +1 -1
  33. package/dist/player-actor.js +417 -166
  34. package/dist/player-actor.js.map +1 -1
  35. package/dist/player-actor.typecheck.js +5 -3
  36. package/dist/player-actor.typecheck.js.map +1 -1
  37. package/dist/routing/build-url.d.ts +27 -16
  38. package/dist/routing/build-url.d.ts.map +1 -1
  39. package/dist/routing/build-url.js +48 -25
  40. package/dist/routing/build-url.js.map +1 -1
  41. package/dist/routing/derive-current-route.d.ts +22 -0
  42. package/dist/routing/derive-current-route.d.ts.map +1 -0
  43. package/dist/routing/derive-current-route.js +122 -0
  44. package/dist/routing/derive-current-route.js.map +1 -0
  45. package/dist/routing/derive-initial-route.d.ts +43 -0
  46. package/dist/routing/derive-initial-route.d.ts.map +1 -0
  47. package/dist/routing/derive-initial-route.js +57 -0
  48. package/dist/routing/derive-initial-route.js.map +1 -0
  49. package/dist/routing/derive-route.d.ts +15 -1
  50. package/dist/routing/derive-route.d.ts.map +1 -1
  51. package/dist/routing/derive-route.js +25 -10
  52. package/dist/routing/derive-route.js.map +1 -1
  53. package/dist/routing/format-play-route-transitions.d.ts +29 -2
  54. package/dist/routing/format-play-route-transitions.d.ts.map +1 -1
  55. package/dist/routing/format-play-route-transitions.js +32 -8
  56. package/dist/routing/format-play-route-transitions.js.map +1 -1
  57. package/dist/routing/index.d.ts +3 -1
  58. package/dist/routing/index.d.ts.map +1 -1
  59. package/dist/routing/index.js +2 -0
  60. package/dist/routing/index.js.map +1 -1
  61. package/dist/routing/types.d.ts +11 -4
  62. package/dist/routing/types.d.ts.map +1 -1
  63. package/dist/types.d.ts +12 -16
  64. package/dist/types.d.ts.map +1 -1
  65. package/package.json +34 -11
  66. package/dist/catalog/index.d.ts +0 -13
  67. package/dist/catalog/index.d.ts.map +0 -1
  68. package/dist/catalog/index.js +0 -11
  69. package/dist/catalog/index.js.map +0 -1
  70. package/dist/catalog/types.d.ts +0 -27
  71. package/dist/catalog/types.d.ts.map +0 -1
  72. package/dist/catalog/types.js +0 -2
  73. package/dist/catalog/types.js.map +0 -1
  74. package/dist/catalog/validate-binding.d.ts +0 -22
  75. package/dist/catalog/validate-binding.d.ts.map +0 -1
  76. package/dist/catalog/validate-binding.js +0 -30
  77. package/dist/catalog/validate-binding.js.map +0 -1
  78. package/dist/catalog/validate-props.d.ts +0 -66
  79. package/dist/catalog/validate-props.d.ts.map +0 -1
  80. package/dist/catalog/validate-props.js +0 -135
  81. package/dist/catalog/validate-props.js.map +0 -1
  82. package/dist/catalog/validate-props.typecheck.d.ts +0 -2
  83. package/dist/catalog/validate-props.typecheck.d.ts.map +0 -1
  84. package/dist/catalog/validate-props.typecheck.js +0 -6
  85. package/dist/catalog/validate-props.typecheck.js.map +0 -1
  86. package/dist/signals/debounce.d.ts +0 -18
  87. package/dist/signals/debounce.d.ts.map +0 -1
  88. package/dist/signals/debounce.js +0 -35
  89. package/dist/signals/debounce.js.map +0 -1
  90. package/dist/signals/index.d.ts +0 -2
  91. package/dist/signals/index.d.ts.map +0 -1
  92. package/dist/signals/index.js +0 -2
  93. package/dist/signals/index.js.map +0 -1
  94. package/dist/signals/state-signal.d.ts +0 -33
  95. package/dist/signals/state-signal.d.ts.map +0 -1
  96. package/dist/signals/state-signal.js +0 -41
  97. package/dist/signals/state-signal.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikael Karon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,463 +1,296 @@
1
- # @xmachines/play-xstate
2
-
3
- **XState v5 adapter for Play Architecture with signal-driven reactivity and routing**
1
+ <!-- generated-by: gsd-doc-writer -->
4
2
 
5
- Transform declarative state machines into live actors with TC39 Signals and parameter-aware navigation.
6
-
7
- ## Overview
3
+ # @xmachines/play-xstate
8
4
 
9
- `@xmachines/play-xstate` provides `definePlayer()`, the primary API for binding XState v5 state machines to the Play Architecture actor base. It enables business logic to control routing and state through guard-enforced transitions with catalog binding, signal lifecycle management, and XState DevTools compatibility.
5
+ > XState v5 adapter for the XMachines Play Architecture bind state machines to the actor base with signal-driven reactivity and router integration.
10
6
 
11
- Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md), this package implements:
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![Version](https://img.shields.io/badge/version-1.0.0--beta.51-blue)](https://www.npmjs.com/package/@xmachines/play-xstate)
12
9
 
13
- - **Actor Authority (INV-01):** State machine guards decide navigation validity
14
- - **Strict Separation (INV-02):** Zero React/framework imports in business logic
15
- - **Signal-Only Reactivity (INV-05):** TC39 Signals expose all state changes
10
+ Part of the [XMachines Play](../../README.md) monorepo.
16
11
 
17
- **Routing:** Supports `meta.route` patterns, `play.route` events with parameters, and route extraction.
12
+ ---
18
13
 
19
14
  ## Installation
20
15
 
21
16
  ```bash
22
- npm install xstate@^5.0.0
23
- npm install @xmachines/play-xstate
17
+ pnpm add @xmachines/play-xstate xstate
24
18
  ```
25
19
 
26
- **Peer dependencies:**
20
+ `xstate ^5.31.0` is a peer dependency and must be installed alongside this package.
27
21
 
28
- - `xstate` ^5.0.0 — State machine runtime
29
-
30
- > `zod` is a direct dependency of `@xmachines/play-xstate` (not a peer). You do not need to install it separately unless you use it in your own catalog schemas.
22
+ ---
31
23
 
32
24
  ## Quick Start
33
25
 
34
26
  ```typescript
35
27
  import { setup } from "xstate";
36
- import { z } from "zod";
37
28
  import { definePlayer } from "@xmachines/play-xstate";
38
- import { defineCatalog } from "@xmachines/play-catalog";
39
29
 
40
- // 1. Define XState machine with meta.route
41
- const machine = setup({
42
- types: {
43
- context: {} as { userId: string },
44
- events: {} as { type: "play.route"; to: string } | { type: "auth.login"; userId: string },
45
- },
46
- guards: {
47
- isLoggedIn: ({ context }) => !!context.userId,
48
- },
49
- }).createMachine({
50
- id: "app",
51
- initial: "login",
52
- context: { userId: "" },
30
+ // 1. Define your XState v5 machine
31
+ const machine = setup({}).createMachine({
32
+ initial: "idle",
53
33
  states: {
54
- login: {
55
- id: "login",
56
- meta: {
57
- route: "/login",
58
- view: { component: "LoginForm" },
59
- },
60
- on: {
61
- "auth.login": {
62
- guard: "isLoggedIn",
63
- target: "dashboard",
64
- },
65
- },
66
- },
67
- dashboard: {
68
- id: "dashboard",
69
- meta: {
70
- route: "/dashboard",
71
- view: { component: "Dashboard", props: { userId: "" } },
72
- },
73
- },
34
+ idle: { meta: { route: "/" } },
35
+ active: { meta: { route: "/active" } },
74
36
  },
75
37
  });
76
38
 
77
- // 2. Define catalog with Zod schemas
78
- const catalog = defineCatalog({
79
- LoginForm: z.object({ error: z.string().optional() }),
80
- Dashboard: z.object({ userId: z.string() }),
81
- });
39
+ // 2. Create a player factory
40
+ const createPlayer = definePlayer({ machine });
82
41
 
83
- // 3. Create player factory
84
- const createPlayer = definePlayer({ machine, catalog });
85
-
86
- // 4. Create and start actor
87
- const actor = createPlayer({ userId: "" });
42
+ // 3. Instantiate and start an actor
43
+ const actor = createPlayer();
88
44
  actor.start();
89
45
 
90
- // 5. Send events (play.route with parameters)
91
- actor.send({ type: "play.route", to: "/login" });
46
+ // 4. Observe TC39 Signal-based reactive state
47
+ console.log(actor.currentRoute.get()); // "/"
48
+ console.log(actor.state.get().value); // "idle"
92
49
 
93
- // 6. Observe signals
94
- console.log(actor.currentRoute.get()); // "/login"
95
- console.log(actor.currentView.get()); // { component: "LoginForm", props: {...} }
50
+ // 5. Send events — machine guards decide transitions
51
+ actor.send({ type: "activate" });
96
52
 
97
- // 7. Cleanup
98
- actor.dispose();
53
+ actor.stop();
99
54
  ```
100
55
 
101
- ## API Reference
56
+ ---
102
57
 
103
- ### definePlayer()
58
+ ## API Summary
104
59
 
105
- Create a player factory from XState machine and catalog:
60
+ ### `definePlayer(config)`
106
61
 
107
- ```typescript
108
- const createPlayer = definePlayer<TMachine, TCatalog>({
109
- machine: AnyStateMachine,
110
- catalog?: Catalog,
111
- options?: PlayerOptions,
112
- }): PlayerFactory;
113
- ```
114
-
115
- **Config:**
116
-
117
- - `machine` (required) - XState v5 state machine
118
- - `catalog` (optional) - UI component catalog with Zod schemas
119
- - `options` (optional) - Lifecycle hooks
62
+ Creates a `PlayerFactory` from an XState v5 machine. The factory pattern enables multiple independent actor instances from a single configuration — useful for multi-user scenarios, SSR, or testing.
120
63
 
121
- **Returns:** Factory function `(input?) => PlayerActor`
64
+ ```typescript
65
+ import { setup } from "xstate";
66
+ import { definePlayer } from "@xmachines/play-xstate";
122
67
 
123
- **Example:**
68
+ const machine = setup({
69
+ types: {
70
+ context: {} as { userId: string },
71
+ input: {} as { userId: string },
72
+ },
73
+ }).createMachine({
74
+ context: ({ input }) => ({ userId: input.userId }),
75
+ initial: "home",
76
+ states: { home: {} },
77
+ });
124
78
 
125
- ```typescript
126
79
  const createPlayer = definePlayer({
127
- machine: authMachine,
128
- catalog: authCatalog,
80
+ machine,
129
81
  options: {
130
- onStart: (actor) => console.log("Started:", actor.id),
131
- onTransition: (actor, prev, next) => {
132
- console.log("Transition:", prev.value, "", next.value);
133
- },
82
+ onStart: (actor) => console.log("started"),
83
+ onStop: (actor) => console.log("stopped"),
84
+ onTransition: (actor, prev, next) => console.log("transitioned"),
85
+ onStateChange: (actor, state) => console.log("state changed"),
86
+ onError: (actor, err) => console.error(err),
134
87
  },
135
88
  });
136
89
 
137
- const actor1 = createPlayer({ userId: "user1" });
138
- const actor2 = createPlayer({ userId: "user2" });
139
- // Multiple independent actor instances
90
+ // Each call returns an independent PlayerActor instance
91
+ const alice = createPlayer({ userId: "alice" });
92
+ const bob = createPlayer({ userId: "bob" });
140
93
  ```
141
94
 
142
- ### PlayerActor
95
+ #### `PlayerFactory` signature
143
96
 
144
- Concrete actor implementing Play signal protocol:
145
-
146
- **Signal Properties:**
97
+ ```typescript
98
+ type PlayerFactory<TMachine> = (
99
+ input?: InputFrom<TMachine>,
100
+ options?: PlayerFactoryResumeOptions<TMachine>,
101
+ ) => PlayerActor<TMachine>;
102
+ ```
147
103
 
148
- - `state: Signal.State<AnyMachineSnapshot>` Reactive snapshot of current state
149
- - `currentRoute: Signal.Computed<string | null>` — Derived navigation path
150
- - `currentView: Signal.State<ViewMetadata | null>` — Current UI structure (updated at state entry)
104
+ #### Restoring from a snapshot
151
105
 
152
- **Actor Properties:**
106
+ ```typescript
107
+ const snapshot = actor.getSnapshot();
108
+ actor.stop();
153
109
 
154
- - `catalog: Catalog` Component catalog
110
+ // Restore to the exact saved state
111
+ const restored = createPlayer({ userId: "alice" }, { snapshot });
112
+ restored.start();
113
+ console.log(restored.currentRoute.get()); // same route as when saved
114
+ ```
155
115
 
156
- **Constructor:**
116
+ ---
157
117
 
158
- ```typescript
159
- new PlayerActor(machine, catalog, options, input?)
160
- ```
118
+ ### `PlayerActor<TMachine>`
161
119
 
162
- - `input` Typed as `InputFrom<TMachine>`. Consumers receive compile-time validation against the machine's input schema. Pass initial context values required by the machine.
120
+ Concrete actor class that wraps an XState v5 actor and exposes TC39 Signal-based reactive signals. Implements both `Routable` and `Viewable` interfaces from `@xmachines/play-actor`.
163
121
 
164
- **Methods:**
122
+ #### Signals
165
123
 
166
- - `start()` Start the actor (must call after creation)
167
- - `stop()` Stop the actor
168
- - `send(event: PlayEvent)` Send event to actor
169
- - `dispose()` Convenience cleanup (calls stop())
124
+ | Signal | Type | Description |
125
+ | -------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
126
+ | `state` | `Signal.State<SnapshotFrom<TMachine>>` | Current XState snapshot; updated on every active transition |
127
+ | `currentRoute` | `Signal.Computed<string \| null>` | Derived URL from active state's `meta.route` template and context |
128
+ | `currentView` | `Signal.State<PlaySpec \| null>` | View spec from active state's `meta.view` metadata; enriched with context params |
129
+ | `initialRoute` | `readonly string \| null` | Machine's initial-state route (fixed at construction; used by router bridges for deep-link vs restore detection) |
170
130
 
171
- **Prop validation modes** (via `PlayerOptions.propValidation`):
131
+ #### Methods
172
132
 
173
- - `"lenient"` (default) — On catalog prop validation failure, calls `onError` hook and renders with unvalidated props
174
- - `"strict"` On catalog prop validation failure, calls `onError` hook and sets `currentView` to `null` (blocks render)
133
+ | Method | Description |
134
+ | --------------- | -------------------------------------------------------------- |
135
+ | `start()` | Start the actor and fire `onStart` hook |
136
+ | `stop()` | Stop the actor, clean up subscriptions, fire `onStop` hook |
137
+ | `send(event)` | Send a typed event to the machine; fires `onTransition` hook |
138
+ | `can(event)` | Returns `true` if the current state can accept the given event |
139
+ | `getSnapshot()` | Returns the current XState snapshot |
140
+ | `dispose()` | Alias for `stop()` |
175
141
 
176
- **Example:**
142
+ #### Signal usage example
177
143
 
178
144
  ```typescript
179
- const actor = createPlayer();
180
- actor.start();
145
+ import { Signal } from "@xmachines/play-signals";
181
146
 
182
- // Observe signals with watcher
183
147
  const watcher = new Signal.subtle.Watcher(() => {
184
148
  queueMicrotask(() => {
185
- const route = actor.currentRoute.get();
186
- console.log("Route changed:", route);
149
+ console.log("Route changed:", actor.currentRoute.get());
187
150
  });
188
151
  });
152
+
189
153
  watcher.watch(actor.currentRoute);
190
- actor.currentRoute.get(); // Initial read
154
+ actor.start();
191
155
  ```
192
156
 
193
- ### Guard Composition
157
+ ---
158
+
159
+ ### Guard utilities
160
+
161
+ Composable guard helpers that wrap XState's built-in `and()`, `or()`, and `not()` for use in machine `setup({ guards })` definitions.
194
162
 
195
163
  ```typescript
164
+ import { setup } from "xstate";
196
165
  import {
197
- composeGuards,
198
- composeGuardsOr,
199
- negateGuard,
200
- hasContext,
201
- eventMatches,
202
- stateMatches,
166
+ composeGuards, // AND logic: all guards must pass
167
+ composeGuardsOr, // OR logic: at least one guard must pass
168
+ negateGuard, // NOT logic: inverts a guard
169
+ hasContext, // guard: context field is present and non-null
170
+ eventMatches, // guard: event type matches a string
171
+ contextFieldMatches, // guard: context field equals a value
203
172
  } from "@xmachines/play-xstate";
204
173
 
205
174
  const machine = setup({
206
175
  guards: {
207
- isLoggedIn: hasContext("userId"),
208
- isAdmin: ({ context }) => context.role === "admin",
176
+ isLoggedIn: ({ context }) => !!context.userId,
177
+ hasAdminRole: ({ context }) => context.role === "admin",
209
178
  },
210
179
  }).createMachine({
211
180
  on: {
212
181
  accessAdmin: {
213
- // Array means AND - all guards must pass
214
- guard: composeGuards(["isLoggedIn", "isAdmin"]),
182
+ guard: composeGuards(["isLoggedIn", "hasAdminRole"]),
215
183
  target: "adminPanel",
216
184
  },
217
- accessPublic: {
218
- // OR composition - any guard passes
219
- guard: composeGuardsOr(["isLoggedIn", ({ event }) => event.type === "guest.access"]),
220
- target: "publicArea",
221
- },
222
- logout: {
223
- // NOT composition
224
- guard: negateGuard("isLoggedIn"),
225
- target: "login",
185
+ accessDashboard: {
186
+ guard: negateGuard("isGuest"),
187
+ target: "dashboard",
226
188
  },
227
189
  },
190
+ // ...
228
191
  });
229
192
  ```
230
193
 
231
- **Helpers:**
232
-
233
- - `hasContext(path: string)` - Check if context property is truthy
234
- - `eventMatches(type: string)` - Check event type
235
- - `stateMatches(value: string)` - Check state value
236
- - `composeGuards(guards: Array)` - AND composition
237
- - `composeGuardsOr(guards: Array)` - OR composition
238
- - `negateGuard(guard)` - NOT composition
194
+ ---
239
195
 
240
- **Complete API:** See [API Documentation](../../docs/api/@xmachines/play-xstate)
196
+ ### Routing utilities
241
197
 
242
- ## Examples
198
+ Helper functions for declarative route configuration in XState machines.
243
199
 
244
- ### Guard Placement Philosophy
200
+ #### `formatPlayRouteTransitions(machineConfig)`
245
201
 
246
- **Guards check if you can BE in a state (state entry), not if you can TAKE an event (event handlers).**
202
+ Crawls machine states with `meta.route` and auto-generates `play.route` event handlers at the root level eliminating boilerplate routing transitions.
247
203
 
248
204
  ```typescript
249
205
  import { setup } from "xstate";
250
- import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
251
- import { defineCatalog } from "@xmachines/play-catalog";
206
+ import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
252
207
 
253
- // Pattern 1: RECOMMENDED - Use formatPlayRouteTransitions utility
254
- const machineConfig = {
208
+ const config = formatPlayRouteTransitions({
255
209
  id: "app",
256
- initial: "home",
257
- context: { isAuthenticated: false },
258
210
  states: {
259
211
  home: {
260
212
  id: "home",
261
- meta: { route: "/", view: { component: "Home" } },
262
- },
263
- dashboard: {
264
- id: "dashboard",
265
- meta: { route: "/dashboard", view: { component: "Dashboard" } },
266
- // Always-guard validates state entry
267
- always: [
268
- {
269
- target: "login",
270
- guard: ({ context }) => !context.isAuthenticated,
271
- },
272
- ],
213
+ meta: { route: "/home" },
273
214
  },
274
- login: {
275
- id: "login",
276
- meta: { route: "/login", view: { component: "Login" } },
215
+ profile: {
216
+ id: "profile",
217
+ meta: { route: "/users/:userId" },
277
218
  },
278
219
  },
279
- };
280
-
281
- // formatPlayRouteTransitions handles routing infrastructure
282
- const machine = setup({
283
- types: {
284
- events: {} as { type: "play.route"; to: string } | { type: "auth.login" },
285
- },
286
- }).createMachine(formatPlayRouteTransitions(machineConfig));
287
-
288
- const catalog = defineCatalog({
289
- Home,
290
- Dashboard,
291
- Login,
292
220
  });
293
221
 
294
- const createPlayer = definePlayer({ machine, catalog });
295
- const actor = createPlayer();
296
- actor.start();
297
-
298
- // Navigation via play.route event
299
- actor.send({ type: "play.route", to: "/dashboard" });
300
- // Guard validates: Can I BE in dashboard state?
301
- // If !isAuthenticated → redirects to login
222
+ // config now includes auto-generated play.route handlers:
223
+ // on: { "play.route": [ { target: ".home", guard: e => e.to === "#home" }, ... ] }
224
+ const machine = setup({}).createMachine(config);
302
225
  ```
303
226
 
304
- **Why this works:**
305
-
306
- - `formatPlayRouteTransitions` adds routing infrastructure (event.to → state mapping)
307
- - Always-guards handle business logic (authentication checks)
308
- - Clear separation: routing is infrastructure, guards are business logic
227
+ > **Note:** Every state with `meta.route` must also have an explicit `id` field; omitting it throws `MissingStateIdError` at machine-definition time.
309
228
 
310
- **Anti-pattern (DON'T DO THIS):**
229
+ #### Other routing exports
311
230
 
312
- ```typescript
313
- // WRONG - Guard on event checking event properties
314
- on: {
315
- "play.route": {
316
- guard: ({ event }) => event.to === "/dashboard",
317
- target: "dashboard"
318
- }
319
- }
320
- ```
231
+ | Export | Description |
232
+ | ---------------------------------- | ------------------------------------------------------------------------- |
233
+ | `deriveRoute(meta)` | Extract the route template string from a state's metadata object |
234
+ | `isAbsoluteRoute(route)` | Returns `true` if the route string is an absolute URL path |
235
+ | `buildRouteUrl(template, context)` | Substitute `:param` placeholders in a route template using context values |
321
236
 
322
- **Reference:** See `docs/examples/routing-patterns.md` for canonical `formatPlayRouteTransitions` usage with always-guards for authentication.
237
+ ---
323
238
 
324
- ### Lifecycle Hooks
239
+ ## Exported Types
325
240
 
326
241
  ```typescript
327
- const createPlayer = definePlayer({
328
- machine,
329
- catalog,
330
- options: {
331
- onStart: (actor) => {
332
- console.log("Actor started:", actor.id);
333
- },
334
- onStop: (actor) => {
335
- console.log("Actor stopped:", actor.id);
336
- },
337
- onTransition: (actor, prev, next) => {
338
- console.log("State change:", {
339
- from: prev.value,
340
- to: next.value,
341
- timestamp: Date.now(),
342
- });
343
- },
344
- onStateChange: (actor, state) => {
345
- // Called on every state update
346
- console.log("Snapshot updated:", state.value);
347
- },
348
- onError: (actor, error) => {
349
- console.error("Actor error:", error);
350
- // Log to monitoring service, show error UI, etc.
351
- },
352
- },
353
- });
354
- ```
355
-
356
- ### XState DevTools Integration
357
-
358
- ```typescript
359
- import { createBrowserInspector } from "@statelyai/inspect";
360
- import { definePlayer } from "@xmachines/play-xstate";
361
-
362
- const { inspect } = createBrowserInspector();
363
-
364
- const createPlayer = definePlayer({ machine, catalog });
365
- const actor = createPlayer();
366
- actor.start();
367
-
368
- // PlayerActor maintains XState Inspector compatibility
369
- // Inspector displays:
370
- // - State transitions and values
371
- // - Context data
372
- // - Events sent to actor
373
- // - Guard evaluation results
374
-
375
- // Signals accessible via actor properties, not snapshots
376
- console.log(actor.currentRoute.get()); // "/dashboard"
242
+ import type {
243
+ PlayerConfig, // definePlayer() config argument shape
244
+ PlayerOptions, // Lifecycle hooks (onStart, onStop, onTransition, onStateChange, onError)
245
+ PlayerFactory, // Factory function returned by definePlayer()
246
+ PlayerFactoryResumeOptions, // { snapshot? } for restoring actor state
247
+ Guard, // Single XState guard predicate
248
+ GuardArray, // Array of guards for compose helpers
249
+ ComposedGuard, // Return type of composeGuards / composeGuardsOr / negateGuard
250
+ RouteMachineConfig, // Minimal machine config accepted by formatPlayRouteTransitions
251
+ RouteStateNode, // Single state node shape used during route crawling
252
+ RouteContext, // Context shape expected by buildRouteUrl ({ params?, query?, basePath?, hash? })
253
+ RouteObject, // Route metadata object shape: { path: string }
254
+ RouteMetadata, // Union: string | RouteObject
255
+ } from "@xmachines/play-xstate";
377
256
  ```
378
257
 
379
- ## Metadata Conventions
258
+ ---
380
259
 
381
- ### Route Metadata
260
+ ## Error Classes
382
261
 
383
- ```typescript
384
- // meta.route marks states as routable
385
- states: {
386
- dashboard: {
387
- id: "dashboard",
388
- meta: {
389
- route: "/dashboard", // URL path - marks state as routable
390
- },
391
- },
392
- }
393
-
394
- // Parameters
395
- meta: {
396
- route: "/profile/:userId", // Required parameter
397
- route: "/settings/:section?", // Optional parameter
398
- }
399
-
400
- // Inheritance
401
- meta: {
402
- route: "/absolute", // Starts with / → doesn't inherit parent route
403
- route: "relative", // Doesn't start with / → inherits parent route
404
- }
405
- ```
406
-
407
- ### View Metadata
262
+ Error classes are exported from the `@xmachines/play-xstate/errors` sub-path to keep the main bundle lean.
408
263
 
409
264
  ```typescript
410
- meta: {
411
- view: {
412
- component: "Dashboard", // Must exist in catalog
413
- props: { userId: "user123" }, // Validated against Zod schema
414
- title: "Dashboard", // Additional metadata
415
- },
416
- }
417
-
418
- // Dynamic props from context
419
- meta: {
420
- view: {
421
- component: "Dashboard",
422
- props: (context) => ({
423
- userId: context.userId,
424
- notifications: context.unreadCount,
425
- }),
426
- },
427
- }
265
+ import {
266
+ MissingRouteParamError, // Required :param absent from context when resolving currentRoute
267
+ MissingQueryContextError, // context.params present but context.query missing
268
+ MissingStateIdError, // meta.route declared without a state id field
269
+ InvalidMachineError, // PlayerActor constructed with a non-object machine
270
+ InvalidEventError, // actor.send() called with null/undefined/non-object
271
+ InvalidRouteMetadataError, // meta.route is neither a string nor { path: string }
272
+ EmptyGuardArrayError, // composeGuards/composeGuardsOr called with empty array
273
+ } from "@xmachines/play-xstate/errors";
428
274
  ```
429
275
 
430
- ## Architecture
276
+ All error classes extend `PlayError` from `@xmachines/play` and carry typed detail fields (`param`, `template`, `combinator`, etc.) for programmatic inspection without message parsing.
431
277
 
432
- This package implements RFC Play v1 requirements:
278
+ ---
433
279
 
434
- **Architectural Invariants:**
280
+ ## Testing
435
281
 
436
- - **Actor Authority (INV-01):** Guards decide navigation validity
437
- - **Strict Separation (INV-02):** Zero framework imports
438
- - **Signal-Only Reactivity (INV-05):** All state via TC39 Signals
439
-
440
- **XState DevTools:** Maintains Inspector compatibility — snapshots remain pure XState format, signals accessible via actor properties.
441
-
442
- **Routing:**
443
-
444
- - `meta.route` property marks states as routable
445
- - `play.route` events support parameters (enhancement)
446
- - Route extraction for URL patterns
282
+ ```bash
283
+ # Run tests for this package in isolation
284
+ pnpm --filter @xmachines/play-xstate test
447
285
 
448
- **Note:** Route parameter extraction uses URLPattern API. See [@xmachines/play-tanstack-react-router browser support](../play-tanstack-react-router/README.md#browser-support) for polyfill requirements.
286
+ # Watch mode
287
+ pnpm --filter @xmachines/play-xstate run test:watch
288
+ ```
449
289
 
450
- ## Related Packages
290
+ Tests use [Vitest](https://vitest.dev/) and live in `packages/play-xstate/test/`.
451
291
 
452
- - **[@xmachines/play-actor](../play-actor)** - AbstractActor base class
453
- - **[@xmachines/play-signals](../play-signals)** - TC39 Signals polyfill
454
- - **[@xmachines/play-catalog](../play-catalog)** - UI schema validation
455
- - **[@xmachines/play-router](../play-router)** - Route extraction
456
- - **[@xmachines/play](../play)** - Protocol types (PlayEvent)
292
+ ---
457
293
 
458
294
  ## License
459
295
 
460
- Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
461
-
462
- This work is licensed under the terms of the MIT license.
463
- For a copy, see <https://opensource.org/licenses/MIT>.
296
+ MIT see [LICENSE](LICENSE) for details.