@xmachines/play-xstate 1.0.0-beta.5 → 1.0.0-beta.50

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -333
  3. package/dist/define-player.d.ts +7 -57
  4. package/dist/define-player.d.ts.map +1 -1
  5. package/dist/define-player.js +17 -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 +169 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +201 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/guards/compose.d.ts +14 -3
  14. package/dist/guards/compose.d.ts.map +1 -1
  15. package/dist/guards/compose.js +29 -2
  16. package/dist/guards/compose.js.map +1 -1
  17. package/dist/guards/helpers.d.ts +15 -17
  18. package/dist/guards/helpers.d.ts.map +1 -1
  19. package/dist/guards/helpers.js +16 -26
  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 +3 -2
  26. package/dist/guards/types.d.ts.map +1 -1
  27. package/dist/index.d.ts +5 -8
  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 +129 -31
  32. package/dist/player-actor.d.ts.map +1 -1
  33. package/dist/player-actor.js +391 -131
  34. package/dist/player-actor.js.map +1 -1
  35. package/dist/player-actor.typecheck.js +9 -5
  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 +44 -26
  40. package/dist/routing/build-url.js.map +1 -1
  41. package/dist/routing/derive-route.d.ts +2 -2
  42. package/dist/routing/derive-route.d.ts.map +1 -1
  43. package/dist/routing/derive-route.js +5 -4
  44. package/dist/routing/derive-route.js.map +1 -1
  45. package/dist/routing/format-play-route-transitions.d.ts +31 -7
  46. package/dist/routing/format-play-route-transitions.d.ts.map +1 -1
  47. package/dist/routing/format-play-route-transitions.js +28 -24
  48. package/dist/routing/format-play-route-transitions.js.map +1 -1
  49. package/dist/routing/types.d.ts +15 -3
  50. package/dist/routing/types.d.ts.map +1 -1
  51. package/dist/signals/state-signal.d.ts +5 -3
  52. package/dist/signals/state-signal.d.ts.map +1 -1
  53. package/dist/signals/state-signal.js +9 -6
  54. package/dist/signals/state-signal.js.map +1 -1
  55. package/dist/types.d.ts +12 -6
  56. package/dist/types.d.ts.map +1 -1
  57. package/package.json +24 -12
  58. package/dist/catalog/index.d.ts +0 -13
  59. package/dist/catalog/index.d.ts.map +0 -1
  60. package/dist/catalog/index.js +0 -11
  61. package/dist/catalog/index.js.map +0 -1
  62. package/dist/catalog/types.d.ts +0 -36
  63. package/dist/catalog/types.d.ts.map +0 -1
  64. package/dist/catalog/types.js +0 -2
  65. package/dist/catalog/types.js.map +0 -1
  66. package/dist/catalog/validate-binding.d.ts +0 -21
  67. package/dist/catalog/validate-binding.d.ts.map +0 -1
  68. package/dist/catalog/validate-binding.js +0 -30
  69. package/dist/catalog/validate-binding.js.map +0 -1
  70. package/dist/catalog/validate-props.d.ts +0 -49
  71. package/dist/catalog/validate-props.d.ts.map +0 -1
  72. package/dist/catalog/validate-props.js +0 -103
  73. package/dist/catalog/validate-props.js.map +0 -1
  74. package/dist/catalog/validate-props.typecheck.d.ts +0 -2
  75. package/dist/catalog/validate-props.typecheck.d.ts.map +0 -1
  76. package/dist/catalog/validate-props.typecheck.js +0 -6
  77. package/dist/catalog/validate-props.typecheck.js.map +0 -1
  78. package/dist/signals/debounce.d.ts +0 -18
  79. package/dist/signals/debounce.d.ts.map +0 -1
  80. package/dist/signals/debounce.js +0 -35
  81. package/dist/signals/debounce.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,454 +1,294 @@
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.46-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 zod@^3.23.0
23
- npm install @xmachines/play-xstate
17
+ npm install @xmachines/play-xstate xstate
24
18
  ```
25
19
 
26
- ## Current Exports
20
+ `xstate ^5.30.0` is a peer dependency and must be installed alongside this package.
27
21
 
28
- - `definePlayer`, `PlayerActor`
29
- - player types: `PlayerConfig`, `PlayerOptions`, `PlayerFactory`
30
- - guard utilities: `composeGuards`, `composeGuardsOr`, `negateGuard`, `hasContext`, `eventMatches`, `stateMatches`
31
- - routing utilities: `deriveRoute`, `isAbsoluteRoute`, `buildRouteUrl`, `formatPlayRouteTransitions`
32
- - catalog utilities: `validateComponentBinding`, `validateViewProps`, `mergeViewProps`
33
-
34
- **Peer dependencies:**
35
-
36
- - `xstate` ^5.0.0 - State machine runtime
37
- - `zod` ^3.23.0 - Schema validation for component props
22
+ ---
38
23
 
39
24
  ## Quick Start
40
25
 
41
26
  ```typescript
42
27
  import { setup } from "xstate";
43
- import { z } from "zod";
44
28
  import { definePlayer } from "@xmachines/play-xstate";
45
- import { defineCatalog } from "@xmachines/play-catalog";
46
29
 
47
- // 1. Define XState machine with meta.route
48
- const machine = setup({
49
- types: {
50
- context: {} as { userId: string },
51
- events: {} as { type: "play.route"; to: string } | { type: "auth.login"; userId: string },
52
- },
53
- guards: {
54
- isLoggedIn: ({ context }) => !!context.userId,
55
- },
56
- }).createMachine({
57
- id: "app",
58
- initial: "login",
59
- context: { userId: "" },
30
+ // 1. Define your XState v5 machine
31
+ const machine = setup({}).createMachine({
32
+ initial: "idle",
60
33
  states: {
61
- login: {
62
- id: "login",
63
- meta: {
64
- route: "/login",
65
- view: { component: "LoginForm" },
66
- },
67
- on: {
68
- "auth.login": {
69
- guard: "isLoggedIn",
70
- target: "dashboard",
71
- },
72
- },
73
- },
74
- dashboard: {
75
- id: "dashboard",
76
- meta: {
77
- route: "/dashboard",
78
- view: { component: "Dashboard", props: { userId: "" } },
79
- },
80
- },
34
+ idle: { meta: { route: "/" } },
35
+ active: { meta: { route: "/active" } },
81
36
  },
82
37
  });
83
38
 
84
- // 2. Define catalog with Zod schemas
85
- const catalog = defineCatalog({
86
- LoginForm: z.object({ error: z.string().optional() }),
87
- Dashboard: z.object({ userId: z.string() }),
88
- });
89
-
90
- // 3. Create player factory
91
- const createPlayer = definePlayer({ machine, catalog });
39
+ // 2. Create a player factory
40
+ const createPlayer = definePlayer({ machine });
92
41
 
93
- // 4. Create and start actor
94
- const actor = createPlayer({ userId: "" });
42
+ // 3. Instantiate and start an actor
43
+ const actor = createPlayer();
95
44
  actor.start();
96
45
 
97
- // 5. Send events (play.route with parameters)
98
- 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"
99
49
 
100
- // 6. Observe signals
101
- console.log(actor.currentRoute.get()); // "/login"
102
- console.log(actor.currentView.get()); // { component: "LoginForm", props: {...} }
50
+ // 5. Send events — machine guards decide transitions
51
+ actor.send({ type: "activate" });
103
52
 
104
- // 7. Cleanup
105
- actor.dispose();
53
+ actor.stop();
106
54
  ```
107
55
 
108
- ## API Reference
56
+ ---
109
57
 
110
- ### definePlayer()
58
+ ## API Summary
111
59
 
112
- Create a player factory from XState machine and catalog:
60
+ ### `definePlayer(config)`
113
61
 
114
- ```typescript
115
- const createPlayer = definePlayer<TMachine, TCatalog>({
116
- machine: AnyStateMachine,
117
- catalog?: Catalog,
118
- options?: PlayerOptions,
119
- }): PlayerFactory;
120
- ```
121
-
122
- **Config:**
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.
123
63
 
124
- - `machine` (required) - XState v5 state machine
125
- - `catalog` (optional) - UI component catalog with Zod schemas
126
- - `options` (optional) - Lifecycle hooks
127
-
128
- **Returns:** Factory function `(input?) => PlayerActor`
64
+ ```typescript
65
+ import { setup } from "xstate";
66
+ import { definePlayer } from "@xmachines/play-xstate";
129
67
 
130
- **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
+ });
131
78
 
132
- ```typescript
133
79
  const createPlayer = definePlayer({
134
- machine: authMachine,
135
- catalog: authCatalog,
80
+ machine,
136
81
  options: {
137
- onStart: (actor) => console.log("Started:", actor.id),
138
- onTransition: (actor, prev, next) => {
139
- console.log("Transition:", prev.value, "", next.value);
140
- },
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),
141
87
  },
142
88
  });
143
89
 
144
- const actor1 = createPlayer({ userId: "user1" });
145
- const actor2 = createPlayer({ userId: "user2" });
146
- // Multiple independent actor instances
90
+ // Each call returns an independent PlayerActor instance
91
+ const alice = createPlayer({ userId: "alice" });
92
+ const bob = createPlayer({ userId: "bob" });
93
+ ```
94
+
95
+ #### `PlayerFactory` signature
96
+
97
+ ```typescript
98
+ type PlayerFactory<TMachine> = (
99
+ input?: InputFrom<TMachine>,
100
+ options?: PlayerFactoryResumeOptions<TMachine>,
101
+ ) => PlayerActor<TMachine>;
147
102
  ```
148
103
 
149
- ### PlayerActor
104
+ #### Restoring from a snapshot
150
105
 
151
- Concrete actor implementing Play signal protocol:
106
+ ```typescript
107
+ const snapshot = actor.getSnapshot();
108
+ actor.stop();
152
109
 
153
- **Signal Properties:**
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
+ ```
154
115
 
155
- - `state: Signal.State<Snapshot>` - Reactive snapshot of current state
156
- - `currentRoute: Signal.Computed<string | null>` - Derived navigation path
157
- - `currentView: Signal.Computed<ViewStructure | null>` - Derived UI structure
116
+ ---
158
117
 
159
- **Actor Properties:**
118
+ ### `PlayerActor<TMachine>`
160
119
 
161
- - `catalog: Catalog` - Component catalog
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`.
162
121
 
163
- **Methods:**
122
+ #### Signals
164
123
 
165
- - `start()` - Start the actor (must call after creation)
166
- - `stop()` - Stop the actor
167
- - `send(event: PlayEvent)` - Send event to actor
168
- - `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) |
169
130
 
170
- **Example:**
131
+ #### Methods
132
+
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()` |
141
+
142
+ #### Signal usage example
171
143
 
172
144
  ```typescript
173
- const actor = createPlayer();
174
- actor.start();
145
+ import { Signal } from "@xmachines/play-signals";
175
146
 
176
- // Observe signals with watcher
177
147
  const watcher = new Signal.subtle.Watcher(() => {
178
148
  queueMicrotask(() => {
179
- const route = actor.currentRoute.get();
180
- console.log("Route changed:", route);
149
+ console.log("Route changed:", actor.currentRoute.get());
181
150
  });
182
151
  });
152
+
183
153
  watcher.watch(actor.currentRoute);
184
- actor.currentRoute.get(); // Initial read
154
+ actor.start();
185
155
  ```
186
156
 
187
- ### 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.
188
162
 
189
163
  ```typescript
164
+ import { setup } from "xstate";
190
165
  import {
191
- composeGuards,
192
- composeGuardsOr,
193
- negateGuard,
194
- hasContext,
195
- eventMatches,
196
- 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
197
172
  } from "@xmachines/play-xstate";
198
173
 
199
174
  const machine = setup({
200
175
  guards: {
201
- isLoggedIn: hasContext("userId"),
202
- isAdmin: ({ context }) => context.role === "admin",
176
+ isLoggedIn: ({ context }) => !!context.userId,
177
+ hasAdminRole: ({ context }) => context.role === "admin",
203
178
  },
204
179
  }).createMachine({
205
180
  on: {
206
181
  accessAdmin: {
207
- // Array means AND - all guards must pass
208
- guard: composeGuards(["isLoggedIn", "isAdmin"]),
182
+ guard: composeGuards(["isLoggedIn", "hasAdminRole"]),
209
183
  target: "adminPanel",
210
184
  },
211
- accessPublic: {
212
- // OR composition - any guard passes
213
- guard: composeGuardsOr(["isLoggedIn", ({ event }) => event.type === "guest.access"]),
214
- target: "publicArea",
215
- },
216
- logout: {
217
- // NOT composition
218
- guard: negateGuard("isLoggedIn"),
219
- target: "login",
185
+ accessDashboard: {
186
+ guard: negateGuard("isGuest"),
187
+ target: "dashboard",
220
188
  },
221
189
  },
190
+ // ...
222
191
  });
223
192
  ```
224
193
 
225
- **Helpers:**
194
+ ---
226
195
 
227
- - `hasContext(path: string)` - Check if context property is truthy
228
- - `eventMatches(type: string)` - Check event type
229
- - `stateMatches(value: string)` - Check state value
230
- - `composeGuards(guards: Array)` - AND composition
231
- - `composeGuardsOr(guards: Array)` - OR composition
232
- - `negateGuard(guard)` - NOT composition
196
+ ### Routing utilities
233
197
 
234
- **Complete API:** See [API Documentation](../../docs/api/@xmachines/play-xstate)
198
+ Helper functions for declarative route configuration in XState machines.
235
199
 
236
- ## Examples
200
+ #### `formatPlayRouteTransitions(machineConfig)`
237
201
 
238
- ### Guard Placement Philosophy
239
-
240
- **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.
241
203
 
242
204
  ```typescript
243
205
  import { setup } from "xstate";
244
- import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
245
- import { defineCatalog } from "@xmachines/play-catalog";
206
+ import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
246
207
 
247
- // Pattern 1: RECOMMENDED - Use formatPlayRouteTransitions utility
248
- const machineConfig = {
208
+ const config = formatPlayRouteTransitions({
249
209
  id: "app",
250
- initial: "home",
251
- context: { isAuthenticated: false },
252
210
  states: {
253
211
  home: {
254
212
  id: "home",
255
- meta: { route: "/", view: { component: "Home" } },
213
+ meta: { route: "/home" },
256
214
  },
257
- dashboard: {
258
- id: "dashboard",
259
- meta: { route: "/dashboard", view: { component: "Dashboard" } },
260
- // Always-guard validates state entry
261
- always: [
262
- {
263
- target: "login",
264
- guard: ({ context }) => !context.isAuthenticated,
265
- },
266
- ],
267
- },
268
- login: {
269
- id: "login",
270
- meta: { route: "/login", view: { component: "Login" } },
215
+ profile: {
216
+ id: "profile",
217
+ meta: { route: "/users/:userId" },
271
218
  },
272
219
  },
273
- };
274
-
275
- // formatPlayRouteTransitions handles routing infrastructure
276
- const machine = setup({
277
- types: {
278
- events: {} as { type: "play.route"; to: string } | { type: "auth.login" },
279
- },
280
- }).createMachine(formatPlayRouteTransitions(machineConfig));
281
-
282
- const catalog = defineCatalog({
283
- Home,
284
- Dashboard,
285
- Login,
286
220
  });
287
221
 
288
- const createPlayer = definePlayer({ machine, catalog });
289
- const actor = createPlayer();
290
- actor.start();
291
-
292
- // Navigation via play.route event
293
- actor.send({ type: "play.route", to: "/dashboard" });
294
- // Guard validates: Can I BE in dashboard state?
295
- // 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);
296
225
  ```
297
226
 
298
- **Why this works:**
227
+ > **Note:** Every state with `meta.route` must also have an explicit `id` field; omitting it throws `MissingStateIdError` at machine-definition time.
299
228
 
300
- - `formatPlayRouteTransitions` adds routing infrastructure (event.to → state mapping)
301
- - Always-guards handle business logic (authentication checks)
302
- - Clear separation: routing is infrastructure, guards are business logic
229
+ #### Other routing exports
303
230
 
304
- **Anti-pattern (DON'T DO THIS):**
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 |
305
236
 
306
- ```typescript
307
- // ❌ WRONG - Guard on event checking event properties
308
- on: {
309
- "play.route": {
310
- guard: ({ event }) => event.to === "/dashboard",
311
- target: "dashboard"
312
- }
313
- }
314
- ```
237
+ ---
315
238
 
316
- **Reference:** See `docs/examples/routing-patterns.md` for canonical `formatPlayRouteTransitions` usage with always-guards for authentication.
317
-
318
- ### Lifecycle Hooks
239
+ ## Exported Types
319
240
 
320
241
  ```typescript
321
- const createPlayer = definePlayer({
322
- machine,
323
- catalog,
324
- options: {
325
- onStart: (actor) => {
326
- console.log("Actor started:", actor.id);
327
- },
328
- onStop: (actor) => {
329
- console.log("Actor stopped:", actor.id);
330
- },
331
- onTransition: (actor, prev, next) => {
332
- console.log("State change:", {
333
- from: prev.value,
334
- to: next.value,
335
- timestamp: Date.now(),
336
- });
337
- },
338
- onStateChange: (actor, state) => {
339
- // Called on every state update
340
- console.log("Snapshot updated:", state.value);
341
- },
342
- onError: (actor, error) => {
343
- console.error("Actor error:", error);
344
- // Log to monitoring service, show error UI, etc.
345
- },
346
- },
347
- });
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? })
253
+ } from "@xmachines/play-xstate";
348
254
  ```
349
255
 
350
- ### XState DevTools Integration
351
-
352
- ```typescript
353
- import { createBrowserInspector } from "@statelyai/inspect";
354
- import { definePlayer } from "@xmachines/play-xstate";
256
+ ---
355
257
 
356
- const { inspect } = createBrowserInspector();
357
-
358
- const createPlayer = definePlayer({ machine, catalog });
359
- const actor = createPlayer();
360
- actor.start();
258
+ ## Error Classes
361
259
 
362
- // PlayerActor maintains XState Inspector compatibility
363
- // Inspector displays:
364
- // - State transitions and values
365
- // - Context data
366
- // - Events sent to actor
367
- // - Guard evaluation results
368
-
369
- // Signals accessible via actor properties, not snapshots
370
- console.log(actor.currentRoute.get()); // "/dashboard"
371
- ```
372
-
373
- ## Metadata Conventions
374
-
375
- ### Route Metadata
260
+ Error classes are exported from the `@xmachines/play-xstate/errors` sub-path to keep the main bundle lean.
376
261
 
377
262
  ```typescript
378
- // meta.route marks states as routable
379
- states: {
380
- dashboard: {
381
- id: "dashboard",
382
- meta: {
383
- route: "/dashboard", // URL path - marks state as routable
384
- },
385
- },
386
- }
387
-
388
- // Parameters
389
- meta: {
390
- route: "/profile/:userId", // Required parameter
391
- route: "/settings/:section?", // Optional parameter
392
- }
393
-
394
- // Inheritance
395
- meta: {
396
- route: "/absolute", // Starts with / → doesn't inherit parent route
397
- route: "relative", // Doesn't start with / → inherits parent route
398
- }
399
- ```
400
-
401
- ### View Metadata
402
-
403
- ```typescript
404
- meta: {
405
- view: {
406
- component: "Dashboard", // Must exist in catalog
407
- props: { userId: "user123" }, // Validated against Zod schema
408
- title: "Dashboard", // Additional metadata
409
- },
410
- }
411
-
412
- // Dynamic props from context
413
- meta: {
414
- view: {
415
- component: "Dashboard",
416
- props: (context) => ({
417
- userId: context.userId,
418
- notifications: context.unreadCount,
419
- }),
420
- },
421
- }
263
+ import {
264
+ MissingRouteParamError, // Required :param absent from context when resolving currentRoute
265
+ MissingQueryContextError, // context.params present but context.query missing
266
+ MissingStateIdError, // meta.route declared without a state id field
267
+ InvalidMachineError, // PlayerActor constructed with a non-object machine
268
+ InvalidEventError, // actor.send() called with null/undefined/non-object
269
+ InvalidRouteMetadataError, // meta.route is neither a string nor { path: string }
270
+ EmptyGuardArrayError, // composeGuards/composeGuardsOr called with empty array
271
+ } from "@xmachines/play-xstate/errors";
422
272
  ```
423
273
 
424
- ## Architecture
425
-
426
- This package implements RFC Play v1 requirements:
427
-
428
- **Architectural Invariants:**
274
+ All error classes extend `PlayError` from `@xmachines/play` and carry typed detail fields (`param`, `template`, `combinator`, etc.) for programmatic inspection without message parsing.
429
275
 
430
- - **Actor Authority (INV-01):** Guards decide navigation validity
431
- - **Strict Separation (INV-02):** Zero framework imports
432
- - **Signal-Only Reactivity (INV-05):** All state via TC39 Signals
276
+ ---
433
277
 
434
- **XState DevTools:** Maintains Inspector compatibility — snapshots remain pure XState format, signals accessible via actor properties.
278
+ ## Testing
435
279
 
436
- **Routing:**
437
-
438
- - `meta.route` property marks states as routable
439
- - `play.route` events support parameters (enhancement)
440
- - Route extraction for URL patterns
280
+ ```bash
281
+ # Run tests for this package in isolation
282
+ npm test -w packages/play-xstate
441
283
 
442
- **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.
284
+ # Watch mode
285
+ npm run test:watch -w packages/play-xstate
286
+ ```
443
287
 
444
- ## Related Packages
288
+ Tests use [Vitest](https://vitest.dev/) and live in `packages/play-xstate/test/`.
445
289
 
446
- - **[@xmachines/play-actor](../play-actor)** - AbstractActor base class
447
- - **[@xmachines/play-signals](../play-signals)** - TC39 Signals polyfill
448
- - **[@xmachines/play-catalog](../play-catalog)** - UI schema validation
449
- - **[@xmachines/play-router](../play-router)** - Route extraction
450
- - **[@xmachines/play](../play)** - Protocol types (PlayEvent)
290
+ ---
451
291
 
452
292
  ## License
453
293
 
454
- MIT
294
+ MIT — see [LICENSE](LICENSE) for details.