@xmachines/play-xstate 1.0.0-beta.3 → 1.0.0-beta.31

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 +275 -106
  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 +138 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +159 -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 +26 -0
  16. package/dist/guards/compose.js.map +1 -1
  17. package/dist/guards/helpers.d.ts +13 -17
  18. package/dist/guards/helpers.d.ts.map +1 -1
  19. package/dist/guards/helpers.js +20 -25
  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 +125 -26
  32. package/dist/player-actor.d.ts.map +1 -1
  33. package/dist/player-actor.js +362 -101
  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 +3 -3
  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 +4 -2
  52. package/dist/signals/state-signal.d.ts.map +1 -1
  53. package/dist/signals/state-signal.js +5 -2
  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 +25 -11
  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
@@ -6,9 +6,9 @@ Transform declarative state machines into live actors with TC39 Signals and para
6
6
 
7
7
  ## Overview
8
8
 
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.
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, signal lifecycle management, and XState DevTools compatibility.
10
10
 
11
- Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md), this package implements:
11
+ Per [Play RFC](../docs/rfc/play.md), this package implements:
12
12
 
13
13
  - **Actor Authority (INV-01):** State machine guards decide navigation validity
14
14
  - **Strict Separation (INV-02):** Zero React/framework imports in business logic
@@ -19,89 +19,97 @@ Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md),
19
19
  ## Installation
20
20
 
21
21
  ```bash
22
- npm install xstate@^5.0.0 zod@^3.23.0
22
+ npm install xstate@^5.0.0
23
23
  npm install @xmachines/play-xstate
24
24
  ```
25
25
 
26
- ## Current Exports
27
-
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
26
  **Peer dependencies:**
35
27
 
36
- - `xstate` ^5.0.0 - State machine runtime
37
- - `zod` ^3.23.0 - Schema validation for component props
28
+ - `xstate` ^5.0.0 State machine runtime
38
29
 
39
30
  ## Quick Start
40
31
 
41
32
  ```typescript
42
- import { setup } from "xstate";
43
- import { z } from "zod";
44
- import { definePlayer } from "@xmachines/play-xstate";
45
- import { defineCatalog } from "@xmachines/play-catalog";
33
+ import { setup, assign } from "xstate";
34
+ import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
46
35
 
47
- // 1. Define XState machine with meta.route
36
+ // 1. Define XState machine with meta.route and view spec
48
37
  const machine = setup({
49
38
  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,
39
+ context: {} as {
40
+ isAuthenticated: boolean;
41
+ params: Record<string, string>;
42
+ query: Record<string, string>;
43
+ },
44
+ events: {} as
45
+ | { type: "play.route"; to: string; params?: Record<string, string> }
46
+ | { type: "auth.login" }
47
+ | { type: "auth.logout" },
55
48
  },
56
- }).createMachine({
57
- id: "app",
58
- initial: "login",
59
- context: { userId: "" },
60
- states: {
61
- login: {
62
- id: "login",
63
- meta: {
64
- route: "/login",
65
- view: { component: "LoginForm" },
49
+ }).createMachine(
50
+ formatPlayRouteTransitions({
51
+ id: "app",
52
+ initial: "login",
53
+ context: { isAuthenticated: false, params: {}, query: {} },
54
+ states: {
55
+ login: {
56
+ id: "login",
57
+ meta: {
58
+ route: "/login",
59
+ view: {
60
+ component: "Login",
61
+ spec: {
62
+ root: "root",
63
+ elements: {
64
+ root: { type: "Login", props: { title: "Sign In" }, children: [] },
65
+ },
66
+ },
67
+ },
68
+ },
66
69
  },
67
- on: {
68
- "auth.login": {
69
- guard: "isLoggedIn",
70
- target: "dashboard",
70
+ dashboard: {
71
+ id: "dashboard",
72
+ meta: {
73
+ route: "/dashboard",
74
+ view: {
75
+ component: "Dashboard",
76
+ spec: {
77
+ root: "root",
78
+ elements: {
79
+ root: { type: "Dashboard", props: {}, children: [] },
80
+ },
81
+ },
82
+ },
71
83
  },
84
+ always: [{ target: "login", guard: ({ context }) => !context.isAuthenticated }],
72
85
  },
73
86
  },
74
- dashboard: {
75
- id: "dashboard",
76
- meta: {
77
- route: "/dashboard",
78
- view: { component: "Dashboard", props: { userId: "" } },
87
+ on: {
88
+ "auth.login": {
89
+ target: ".dashboard",
90
+ guard: ({ context }) => !context.isAuthenticated,
91
+ actions: assign({ isAuthenticated: true }),
92
+ },
93
+ "auth.logout": {
94
+ target: ".login",
95
+ actions: assign({ isAuthenticated: false }),
79
96
  },
80
97
  },
81
- },
82
- });
83
-
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
- });
98
+ }),
99
+ );
89
100
 
90
- // 3. Create player factory
91
- const createPlayer = definePlayer({ machine, catalog });
101
+ // 2. Create player factory
102
+ const createPlayer = definePlayer({ machine });
92
103
 
93
- // 4. Create and start actor
94
- const actor = createPlayer({ userId: "" });
104
+ // 3. Create and start actor
105
+ const actor = createPlayer();
95
106
  actor.start();
96
107
 
97
- // 5. Send events (play.route with parameters)
98
- actor.send({ type: "play.route", to: "/login" });
99
-
100
- // 6. Observe signals
108
+ // 4. Observe signals
101
109
  console.log(actor.currentRoute.get()); // "/login"
102
- console.log(actor.currentView.get()); // { component: "LoginForm", props: {...} }
110
+ console.log(actor.currentView.get()); // { component: "Login", spec: {...} }
103
111
 
104
- // 7. Cleanup
112
+ // 5. Cleanup
105
113
  actor.dispose();
106
114
  ```
107
115
 
@@ -109,12 +117,11 @@ actor.dispose();
109
117
 
110
118
  ### definePlayer()
111
119
 
112
- Create a player factory from XState machine and catalog:
120
+ Create a player factory from an XState machine:
113
121
 
114
122
  ```typescript
115
- const createPlayer = definePlayer<TMachine, TCatalog>({
123
+ const createPlayer = definePlayer<TMachine>({
116
124
  machine: AnyStateMachine,
117
- catalog?: Catalog,
118
125
  options?: PlayerOptions,
119
126
  }): PlayerFactory;
120
127
  ```
@@ -122,17 +129,15 @@ const createPlayer = definePlayer<TMachine, TCatalog>({
122
129
  **Config:**
123
130
 
124
131
  - `machine` (required) - XState v5 state machine
125
- - `catalog` (optional) - UI component catalog with Zod schemas
126
132
  - `options` (optional) - Lifecycle hooks
127
133
 
128
- **Returns:** Factory function `(input?) => PlayerActor`
134
+ **Returns:** Factory function `(input?, { snapshot }?) => PlayerActor`
129
135
 
130
136
  **Example:**
131
137
 
132
138
  ```typescript
133
139
  const createPlayer = definePlayer({
134
140
  machine: authMachine,
135
- catalog: authCatalog,
136
141
  options: {
137
142
  onStart: (actor) => console.log("Started:", actor.id),
138
143
  onTransition: (actor, prev, next) => {
@@ -146,26 +151,62 @@ const actor2 = createPlayer({ userId: "user2" });
146
151
  // Multiple independent actor instances
147
152
  ```
148
153
 
154
+ **Restoring from a snapshot:**
155
+
156
+ ```typescript
157
+ const createPlayer = definePlayer({ machine });
158
+
159
+ const actor = createPlayer();
160
+ actor.start();
161
+
162
+ // ...persist the actor snapshot however your app stores it
163
+ const snapshot = actor.getSnapshot();
164
+ actor.stop();
165
+
166
+ const restoredActor = createPlayer(undefined, { snapshot });
167
+ restoredActor.start();
168
+ ```
169
+
170
+ `restoredActor.initialRoute` still reflects the machine's default initial route, not the
171
+ restored snapshot route. Router bridges rely on that invariant to distinguish a deep-link
172
+ from a restored session during `connect()`.
173
+
149
174
  ### PlayerActor
150
175
 
151
176
  Concrete actor implementing Play signal protocol:
152
177
 
153
178
  **Signal Properties:**
154
179
 
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
180
+ - `state: Signal.State<AnyMachineSnapshot>` Reactive snapshot of current state
181
+ - `currentRoute: Signal.Computed<string | null>` Derived URL from the current state's `meta.route` and `context.params`. Returns `null` when no route metadata is present or a required route parameter is missing from context.
182
+ - `currentView: Signal.State<ViewMetadata | null>` Current view spec (updated on every state transition). The spec is automatically enriched with `context.params` (URL params) and any `contextProps`-allowlisted context fields before being emitted (see [Prop Enrichment from Routing and Context](#prop-enrichment-from-routing-and-context)).
158
183
 
159
- **Actor Properties:**
184
+ **Lifecycle ordering:**
160
185
 
161
- - `catalog: Catalog` - Component catalog
186
+ - `state` and `currentRoute` update synchronously from the active XState snapshot.
187
+ - `currentView` is then validated and cached from that same snapshot.
188
+ - `onStateChange` runs after those signals are current.
189
+ - `onTransition` runs after `send()` completes, with both previous and next snapshots.
190
+ - `onStart` runs after the initial active snapshot has been published.
162
191
 
163
192
  **Methods:**
164
193
 
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())
194
+ - `start()` Start the actor (must call after creation)
195
+ - `stop()` Stop the actor
196
+ - `send(event)` Send event to actor. Typed as `EventFromLogic<TMachine>` when `TMachine` is specified.
197
+ - `can(event)` Returns `true` if the current state can accept the given event. Typed as `EventFromLogic<TMachine>` — passing an unknown event type is a compile error. Reads from the `state` signal (no allocation). Use instead of `getSnapshot().can()`.
198
+ - `dispose()` — Convenience cleanup (calls `stop()`)
199
+ - `getSnapshot()` — Get current XState snapshot, typed as `SnapshotFrom<TMachine>`
200
+
201
+ **Routing failure behavior:**
202
+
203
+ - `currentRoute` returns `null` instead of throwing when a required route parameter (e.g. `:username`) is absent from context. This handles transient states during navigation without crashing signal watchers.
204
+ - Errors from `buildRouteUrl()` are typed as `MissingRouteParamError` (importable from `@xmachines/play-xstate/errors`).
205
+
206
+ **View derivation and route-param enrichment:**
207
+
208
+ - `currentView` emits a fresh `ViewMetadata` object on every transition so TC39 Signal equality checks always detect changes (including re-entries to the same state with different params).
209
+ - `context.params` and `contextProps`-allowlisted context fields are merged into spec element props — see [Prop Enrichment from Routing and Context](#prop-enrichment-from-routing-and-context).
169
210
 
170
211
  **Example:**
171
212
 
@@ -193,7 +234,7 @@ import {
193
234
  negateGuard,
194
235
  hasContext,
195
236
  eventMatches,
196
- stateMatches,
237
+ contextFieldMatches,
197
238
  } from "@xmachines/play-xstate";
198
239
 
199
240
  const machine = setup({
@@ -226,13 +267,11 @@ const machine = setup({
226
267
 
227
268
  - `hasContext(path: string)` - Check if context property is truthy
228
269
  - `eventMatches(type: string)` - Check event type
229
- - `stateMatches(value: string)` - Check state value
270
+ - `contextFieldMatches(fieldPath: string, expectedValue: unknown)` - Check context field with strict structural equality
230
271
  - `composeGuards(guards: Array)` - AND composition
231
272
  - `composeGuardsOr(guards: Array)` - OR composition
232
273
  - `negateGuard(guard)` - NOT composition
233
274
 
234
- **Complete API:** See [API Documentation](../../docs/api/@xmachines/play-xstate)
235
-
236
275
  ## Examples
237
276
 
238
277
  ### Guard Placement Philosophy
@@ -242,7 +281,6 @@ const machine = setup({
242
281
  ```typescript
243
282
  import { setup } from "xstate";
244
283
  import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
245
- import { defineCatalog } from "@xmachines/play-catalog";
246
284
 
247
285
  // Pattern 1: RECOMMENDED - Use formatPlayRouteTransitions utility
248
286
  const machineConfig = {
@@ -275,17 +313,18 @@ const machineConfig = {
275
313
  // formatPlayRouteTransitions handles routing infrastructure
276
314
  const machine = setup({
277
315
  types: {
278
- events: {} as { type: "play.route"; to: string } | { type: "auth.login" },
316
+ context: {} as {
317
+ isAuthenticated: boolean;
318
+ params: Record<string, string>;
319
+ query: Record<string, string>;
320
+ },
321
+ events: {} as
322
+ | { type: "play.route"; to: string; params?: Record<string, string> }
323
+ | { type: "auth.login" },
279
324
  },
280
325
  }).createMachine(formatPlayRouteTransitions(machineConfig));
281
326
 
282
- const catalog = defineCatalog({
283
- Home,
284
- Dashboard,
285
- Login,
286
- });
287
-
288
- const createPlayer = definePlayer({ machine, catalog });
327
+ const createPlayer = definePlayer({ machine });
289
328
  const actor = createPlayer();
290
329
  actor.start();
291
330
 
@@ -320,7 +359,6 @@ on: {
320
359
  ```typescript
321
360
  const createPlayer = definePlayer({
322
361
  machine,
323
- catalog,
324
362
  options: {
325
363
  onStart: (actor) => {
326
364
  console.log("Actor started:", actor.id);
@@ -355,7 +393,7 @@ import { definePlayer } from "@xmachines/play-xstate";
355
393
 
356
394
  const { inspect } = createBrowserInspector();
357
395
 
358
- const createPlayer = definePlayer({ machine, catalog });
396
+ const createPlayer = definePlayer({ machine });
359
397
  const actor = createPlayer();
360
398
  actor.start();
361
399
 
@@ -400,30 +438,155 @@ meta: {
400
438
 
401
439
  ### View Metadata
402
440
 
441
+ View metadata drives the `currentView` signal. The `spec` field follows the `@json-render/core` `Spec` shape:
442
+
403
443
  ```typescript
404
444
  meta: {
405
445
  view: {
406
- component: "Dashboard", // Must exist in catalog
407
- props: { userId: "user123" }, // Validated against Zod schema
408
- title: "Dashboard", // Additional metadata
446
+ component: "Dashboard", // Component name — must exist in the registry
447
+ spec: {
448
+ root: "root", // Root element key
449
+ state: { tab: "general" }, // Optional: initial UI state (form values etc.)
450
+ elements: {
451
+ root: {
452
+ type: "Dashboard",
453
+ props: { title: "Dashboard" }, // Static props (non-undefined = cannot be overridden by route params)
454
+ children: [],
455
+ },
456
+ },
457
+ },
409
458
  },
410
459
  }
460
+ ```
411
461
 
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
- },
462
+ **Prop slots:** Declare a prop as `undefined` to allow URL params or context fields to fill
463
+ it in automatically (see [Prop Enrichment from Routing and Context](#prop-enrichment-from-routing-and-context)):
464
+
465
+ ```typescript
466
+ // Route: /settings/:section? Context: { username: "alice" }
467
+ spec: {
468
+ root: "root",
469
+ contextProps: ["username"], // expose context.username to components
470
+ elements: {
471
+ root: { type: "Settings", props: { section: undefined, username: undefined, theme: "dark" }, children: [] },
472
+ },
473
+ }
474
+ // After play.route to /settings/profile:
475
+ // Component receives: { section: "profile", username: "alice", theme: "dark" }
476
+ // section → from context.params (URL)
477
+ // username → from contextProps (context)
478
+ // theme → from spec (explicit value, untouched)
479
+ ```
480
+
481
+ ## Error Handling
482
+
483
+ All runtime errors thrown by this package extend `PlayError` from `@xmachines/play` and
484
+ are exported from the `./errors` subpath:
485
+
486
+ ```typescript
487
+ import { MissingRouteParamError, InvalidEventError } from "@xmachines/play-xstate/errors";
488
+ ```
489
+
490
+ | Class | Code | When thrown |
491
+ | ------------------------ | -------------------------- | -------------------------------------------------------- |
492
+ | `MissingRouteParamError` | `PLAY_ROUTE_PARAM_MISSING` | Required `:param` has no matching value in actor context |
493
+ | `InvalidEventError` | `PLAY_INVALID_EVENT` | `actor.send()` called with a non-object value |
494
+
495
+ `MissingRouteParamError` carries `param` and `template` fields for programmatic access.
496
+ `InvalidEventError` carries a `detail` property with the offending value.
497
+
498
+ > **Note:** `currentRoute` does NOT throw `MissingRouteParamError` — it catches the error internally and returns `null` instead, keeping signal watchers stable during transient states.
499
+
500
+ ```typescript
501
+ import { MissingRouteParamError, InvalidEventError } from "@xmachines/play-xstate/errors";
502
+
503
+ try {
504
+ actor.send(null as any);
505
+ } catch (err) {
506
+ if (err instanceof InvalidEventError) {
507
+ console.error("Non-object event passed to actor.send():", err.detail);
508
+ }
509
+ }
510
+
511
+ try {
512
+ const route = actor.currentRoute.get();
513
+ } catch (err) {
514
+ if (err instanceof MissingRouteParamError) {
515
+ console.error(`Missing "${err.param}" for template "${err.template}"`);
516
+ }
517
+ }
518
+ ```
519
+
520
+ ## Prop Enrichment from Routing and Context
521
+
522
+ When `currentView` is derived, each spec element's `props` are enriched from two sources.
523
+ Both use the **opt-in slot** pattern: declare a prop as `undefined` in the spec to allow
524
+ it to be filled in automatically.
525
+
526
+ ### Merge priority (highest → lowest)
527
+
528
+ | Source | When it applies | Wins over |
529
+ | ---------------------------------- | -------------------------------- | ------------------------- |
530
+ | Explicit non-`undefined` spec prop | Always | Everything |
531
+ | URL route param (`context.params`) | State has a `:param` URL segment | `contextProps` values |
532
+ | `contextProps` field | Listed in `spec.contextProps` | Nothing (lowest priority) |
533
+
534
+ ### URL route parameters
535
+
536
+ URL path parameters (`:section?`, `:username`) are filled automatically — declare the prop
537
+ as `undefined` to opt in:
538
+
539
+ ```typescript
540
+ // Route: /settings/:section?
541
+ elements: {
542
+ root: { type: "Settings", props: { section: undefined, user: "alice" }, children: [] }
421
543
  }
544
+
545
+ // play.route event: { to: "#settings", params: { section: "profile" } }
546
+ // Component receives: { section: "profile", user: "alice" }
547
+ ```
548
+
549
+ Extra params not declared in the spec are also passed through:
550
+
551
+ ```typescript
552
+ // params: { section: "profile", tab: "security" }
553
+ // Component receives: { section: "profile", tab: "security", user: "alice" }
554
+ ```
555
+
556
+ ### Context fields via `contextProps`
557
+
558
+ For states where context data (e.g. `context.username`) should reach a component but there
559
+ is no corresponding URL param, declare `contextProps` in the spec as an explicit allowlist:
560
+
561
+ ```typescript
562
+ // Dashboard at /dashboard — no :username URL param, but we want to show context.username
563
+ spec: {
564
+ root: "root",
565
+ contextProps: ["username"], // ← explicit allowlist
566
+ elements: {
567
+ root: { type: "Dashboard", props: { username: undefined }, children: [] },
568
+ },
569
+ }
570
+
571
+ // After auth.login sets context.username = "alice":
572
+ // Component receives: { username: "alice" }
573
+ ```
574
+
575
+ **Nothing leaks from context without an explicit `contextProps` declaration.**
576
+ Only the named fields are ever exposed. `null` and `undefined` context values are excluded.
577
+
578
+ If both a route param and a `contextProps` field share the same key, the route param wins:
579
+
580
+ ```typescript
581
+ // context.username = "alice" (logged-in user)
582
+ // play.route to /profile/demo → context.params.username = "demo"
583
+ // contextProps: ["username"], props: { username: undefined }
584
+ // Component receives: { username: "demo" } ← route param wins
422
585
  ```
423
586
 
424
587
  ## Architecture
425
588
 
426
- This package implements RFC Play v1 requirements:
589
+ This package implements Play RFC requirements:
427
590
 
428
591
  **Architectural Invariants:**
429
592
 
@@ -445,10 +608,16 @@ This package implements RFC Play v1 requirements:
445
608
 
446
609
  - **[@xmachines/play-actor](../play-actor)** - AbstractActor base class
447
610
  - **[@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
611
+ - **[@xmachines/play-router](../play-router)** - Route extraction utilities
612
+ - **[@xmachines/play-react](../play-react)** - React renderer
613
+ - **[@xmachines/play-vue](../play-vue)** - Vue renderer
614
+ - **[@xmachines/play-solid](../play-solid)** - SolidJS renderer
615
+ - **[@xmachines/play-dom](../play-dom)** - Vanilla DOM renderer
450
616
  - **[@xmachines/play](../play)** - Protocol types (PlayEvent)
451
617
 
452
618
  ## License
453
619
 
454
- MIT
620
+ Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
621
+
622
+ This work is licensed under the terms of the MIT license.
623
+ For a copy, see <https://opensource.org/licenses/MIT>.
@@ -1,29 +1,23 @@
1
- import type { AnyStateMachine } from "xstate";
1
+ import { type AnyStateMachine } from "xstate";
2
2
  import type { PlayerConfig, PlayerFactory } from "./types.js";
3
3
  /**
4
- * Create a player factory from XState machine and catalog
4
+ * Create a player factory from an XState machine
5
5
  *
6
- * Factory pattern that accepts an XState v5 machine and optional UI catalog,
6
+ * Factory pattern that accepts an XState v5 machine,
7
7
  * returning a function that creates {@link PlayerActor} instances. This enables
8
8
  * creating multiple actor instances from a single configuration, useful for
9
9
  * testing, multi-instance scenarios, or server-side rendering.
10
10
  *
11
- * **Architectural Context:** Implements **Strict Separation (INV-02)** by accepting
12
- * the machine definition (pure business logic) separately from the catalog (UI vocabulary).
13
- * The machine references component names in `meta.view` without importing React/framework code.
14
- *
15
11
  * @typeParam TMachine - XState v5 state machine type
16
- * @typeParam TCatalog - Optional UI component catalog type
17
12
  *
18
13
  * @param config - Player configuration object
19
14
  * @param config.machine - XState v5 state machine
20
- * @param config.catalog - Optional UI component catalog (allows machines without UI)
21
15
  * @param config.options - Optional lifecycle hooks (onStart, onTransition, etc.)
22
16
  *
23
17
  * @returns Factory function that creates actor instances with optional input context
24
18
  *
25
19
  * @example
26
- * Basic player factory without catalog
20
+ * Basic player factory
27
21
  * ```typescript
28
22
  * import { setup } from "xstate";
29
23
  * import { definePlayer } from "@xmachines/play-xstate";
@@ -42,53 +36,9 @@ import type { PlayerConfig, PlayerFactory } from "./types.js";
42
36
  * ```
43
37
  *
44
38
  * @example
45
- * Player factory with catalog and parameter-aware routing
46
- * ```typescript
47
- * import { setup } from "xstate";
48
- * import { defineCatalog } from "@xmachines/play-catalog";
49
- * import { definePlayer } from "@xmachines/play-xstate";
50
- * import { z } from "zod";
51
- *
52
- * const catalog = defineCatalog({
53
- * HomePage: z.object({}),
54
- * ProfilePage: z.object({ userId: z.string() })
55
- * });
56
- *
57
- * const machine = setup({
58
- * types: {
59
- * context: {} as { userId: string },
60
- * events: {} as { type: 'play.route'; to: string; params?: Record<string, string> }
61
- * }
62
- * }).createMachine({
63
- * initial: 'home',
64
- * context: { userId: '' },
65
- * states: {
66
- * home: {
67
- * route: {},
68
- * meta: {
69
- * route: '/',
70
- * view: { component: 'HomePage' }
71
- * }
72
- * },
73
- * profile: {
74
- * route: {},
75
- * meta: {
76
- * route: '/profile/:userId',
77
- * view: { component: 'ProfilePage', userId: (ctx) => ctx.userId }
78
- * }
79
- * }
80
- * }
81
- * });
82
- *
83
- * const createPlayer = definePlayer({ machine, catalog });
84
- * const actor = createPlayer({ userId: 'user123' });
85
- * actor.start();
86
- * ```
87
- *
88
- * @example
89
39
  * Multiple actor instances from single factory
90
40
  * ```typescript
91
- * const createPlayer = definePlayer({ machine, catalog });
41
+ * const createPlayer = definePlayer({ machine });
92
42
  *
93
43
  * // Create actors for different users
94
44
  * const alice = createPlayer({ userId: 'alice' });
@@ -101,10 +51,10 @@ import type { PlayerConfig, PlayerFactory } from "./types.js";
101
51
  * console.log(alice.state.get() !== bob.state.get());
102
52
  * ```
103
53
  *
104
- * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md | RFC Play v1}
54
+ * @see [Play RFC](../../docs/rfc/play.md)
105
55
  * @see {@link PlayerActor} for the concrete actor implementation
106
56
  * @see {@link PlayerConfig} for configuration options
107
57
  * @see {@link PlayerFactory} for factory function signature
108
58
  */
109
- export declare const definePlayer: <TMachine extends AnyStateMachine, TCatalog = Record<string, unknown>>(config: PlayerConfig<TMachine, TCatalog>) => PlayerFactory<TMachine>;
59
+ export declare const definePlayer: <TMachine extends AnyStateMachine>(config: PlayerConfig<TMachine>) => PlayerFactory<TMachine>;
110
60
  //# sourceMappingURL=define-player.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-player.d.ts","sourceRoot":"","sources":["../src/define-player.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAa,MAAM,QAAQ,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyGG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,SAAS,eAAe,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChG,QAAQ,YAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,KACtC,aAAa,CAAC,QAAQ,CAQxB,CAAC"}
1
+ {"version":3,"file":"define-player.d.ts","sourceRoot":"","sources":["../src/define-player.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,eAAe,EAA2C,MAAM,QAAQ,CAAC;AACpG,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAA8B,MAAM,YAAY,CAAC;AAG1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,SAAS,eAAe,EAC5D,QAAQ,YAAY,CAAC,QAAQ,CAAC,KAC5B,aAAa,CAAC,QAAQ,CAsBxB,CAAC"}