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

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 +274 -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 +113 -26
  32. package/dist/player-actor.d.ts.map +1 -1
  33. package/dist/player-actor.js +348 -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,61 @@ 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
+ - `dispose()` Convenience cleanup (calls `stop()`)
198
+ - `getSnapshot()` — Get current XState snapshot, typed as `SnapshotFrom<TMachine>`
199
+
200
+ **Routing failure behavior:**
201
+
202
+ - `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.
203
+ - Errors from `buildRouteUrl()` are typed as `MissingRouteParamError` (importable from `@xmachines/play-xstate/errors`).
204
+
205
+ **View derivation and route-param enrichment:**
206
+
207
+ - `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).
208
+ - `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
209
 
170
210
  **Example:**
171
211
 
@@ -193,7 +233,7 @@ import {
193
233
  negateGuard,
194
234
  hasContext,
195
235
  eventMatches,
196
- stateMatches,
236
+ contextFieldMatches,
197
237
  } from "@xmachines/play-xstate";
198
238
 
199
239
  const machine = setup({
@@ -226,13 +266,11 @@ const machine = setup({
226
266
 
227
267
  - `hasContext(path: string)` - Check if context property is truthy
228
268
  - `eventMatches(type: string)` - Check event type
229
- - `stateMatches(value: string)` - Check state value
269
+ - `contextFieldMatches(fieldPath: string, expectedValue: unknown)` - Check context field with strict structural equality
230
270
  - `composeGuards(guards: Array)` - AND composition
231
271
  - `composeGuardsOr(guards: Array)` - OR composition
232
272
  - `negateGuard(guard)` - NOT composition
233
273
 
234
- **Complete API:** See [API Documentation](../../docs/api/@xmachines/play-xstate)
235
-
236
274
  ## Examples
237
275
 
238
276
  ### Guard Placement Philosophy
@@ -242,7 +280,6 @@ const machine = setup({
242
280
  ```typescript
243
281
  import { setup } from "xstate";
244
282
  import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
245
- import { defineCatalog } from "@xmachines/play-catalog";
246
283
 
247
284
  // Pattern 1: RECOMMENDED - Use formatPlayRouteTransitions utility
248
285
  const machineConfig = {
@@ -275,17 +312,18 @@ const machineConfig = {
275
312
  // formatPlayRouteTransitions handles routing infrastructure
276
313
  const machine = setup({
277
314
  types: {
278
- events: {} as { type: "play.route"; to: string } | { type: "auth.login" },
315
+ context: {} as {
316
+ isAuthenticated: boolean;
317
+ params: Record<string, string>;
318
+ query: Record<string, string>;
319
+ },
320
+ events: {} as
321
+ | { type: "play.route"; to: string; params?: Record<string, string> }
322
+ | { type: "auth.login" },
279
323
  },
280
324
  }).createMachine(formatPlayRouteTransitions(machineConfig));
281
325
 
282
- const catalog = defineCatalog({
283
- Home,
284
- Dashboard,
285
- Login,
286
- });
287
-
288
- const createPlayer = definePlayer({ machine, catalog });
326
+ const createPlayer = definePlayer({ machine });
289
327
  const actor = createPlayer();
290
328
  actor.start();
291
329
 
@@ -320,7 +358,6 @@ on: {
320
358
  ```typescript
321
359
  const createPlayer = definePlayer({
322
360
  machine,
323
- catalog,
324
361
  options: {
325
362
  onStart: (actor) => {
326
363
  console.log("Actor started:", actor.id);
@@ -355,7 +392,7 @@ import { definePlayer } from "@xmachines/play-xstate";
355
392
 
356
393
  const { inspect } = createBrowserInspector();
357
394
 
358
- const createPlayer = definePlayer({ machine, catalog });
395
+ const createPlayer = definePlayer({ machine });
359
396
  const actor = createPlayer();
360
397
  actor.start();
361
398
 
@@ -400,30 +437,155 @@ meta: {
400
437
 
401
438
  ### View Metadata
402
439
 
440
+ View metadata drives the `currentView` signal. The `spec` field follows the `@json-render/core` `Spec` shape:
441
+
403
442
  ```typescript
404
443
  meta: {
405
444
  view: {
406
- component: "Dashboard", // Must exist in catalog
407
- props: { userId: "user123" }, // Validated against Zod schema
408
- title: "Dashboard", // Additional metadata
445
+ component: "Dashboard", // Component name — must exist in the registry
446
+ spec: {
447
+ root: "root", // Root element key
448
+ state: { tab: "general" }, // Optional: initial UI state (form values etc.)
449
+ elements: {
450
+ root: {
451
+ type: "Dashboard",
452
+ props: { title: "Dashboard" }, // Static props (non-undefined = cannot be overridden by route params)
453
+ children: [],
454
+ },
455
+ },
456
+ },
409
457
  },
410
458
  }
459
+ ```
411
460
 
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
- },
461
+ **Prop slots:** Declare a prop as `undefined` to allow URL params or context fields to fill
462
+ it in automatically (see [Prop Enrichment from Routing and Context](#prop-enrichment-from-routing-and-context)):
463
+
464
+ ```typescript
465
+ // Route: /settings/:section? Context: { username: "alice" }
466
+ spec: {
467
+ root: "root",
468
+ contextProps: ["username"], // expose context.username to components
469
+ elements: {
470
+ root: { type: "Settings", props: { section: undefined, username: undefined, theme: "dark" }, children: [] },
471
+ },
472
+ }
473
+ // After play.route to /settings/profile:
474
+ // Component receives: { section: "profile", username: "alice", theme: "dark" }
475
+ // section → from context.params (URL)
476
+ // username → from contextProps (context)
477
+ // theme → from spec (explicit value, untouched)
478
+ ```
479
+
480
+ ## Error Handling
481
+
482
+ All runtime errors thrown by this package extend `PlayError` from `@xmachines/play` and
483
+ are exported from the `./errors` subpath:
484
+
485
+ ```typescript
486
+ import { MissingRouteParamError, InvalidEventError } from "@xmachines/play-xstate/errors";
487
+ ```
488
+
489
+ | Class | Code | When thrown |
490
+ | ------------------------ | -------------------------- | -------------------------------------------------------- |
491
+ | `MissingRouteParamError` | `PLAY_ROUTE_PARAM_MISSING` | Required `:param` has no matching value in actor context |
492
+ | `InvalidEventError` | `PLAY_INVALID_EVENT` | `actor.send()` called with a non-object value |
493
+
494
+ `MissingRouteParamError` carries `param` and `template` fields for programmatic access.
495
+ `InvalidEventError` carries a `detail` property with the offending value.
496
+
497
+ > **Note:** `currentRoute` does NOT throw `MissingRouteParamError` — it catches the error internally and returns `null` instead, keeping signal watchers stable during transient states.
498
+
499
+ ```typescript
500
+ import { MissingRouteParamError, InvalidEventError } from "@xmachines/play-xstate/errors";
501
+
502
+ try {
503
+ actor.send(null as any);
504
+ } catch (err) {
505
+ if (err instanceof InvalidEventError) {
506
+ console.error("Non-object event passed to actor.send():", err.detail);
507
+ }
508
+ }
509
+
510
+ try {
511
+ const route = actor.currentRoute.get();
512
+ } catch (err) {
513
+ if (err instanceof MissingRouteParamError) {
514
+ console.error(`Missing "${err.param}" for template "${err.template}"`);
515
+ }
516
+ }
517
+ ```
518
+
519
+ ## Prop Enrichment from Routing and Context
520
+
521
+ When `currentView` is derived, each spec element's `props` are enriched from two sources.
522
+ Both use the **opt-in slot** pattern: declare a prop as `undefined` in the spec to allow
523
+ it to be filled in automatically.
524
+
525
+ ### Merge priority (highest → lowest)
526
+
527
+ | Source | When it applies | Wins over |
528
+ | ---------------------------------- | -------------------------------- | ------------------------- |
529
+ | Explicit non-`undefined` spec prop | Always | Everything |
530
+ | URL route param (`context.params`) | State has a `:param` URL segment | `contextProps` values |
531
+ | `contextProps` field | Listed in `spec.contextProps` | Nothing (lowest priority) |
532
+
533
+ ### URL route parameters
534
+
535
+ URL path parameters (`:section?`, `:username`) are filled automatically — declare the prop
536
+ as `undefined` to opt in:
537
+
538
+ ```typescript
539
+ // Route: /settings/:section?
540
+ elements: {
541
+ root: { type: "Settings", props: { section: undefined, user: "alice" }, children: [] }
421
542
  }
543
+
544
+ // play.route event: { to: "#settings", params: { section: "profile" } }
545
+ // Component receives: { section: "profile", user: "alice" }
546
+ ```
547
+
548
+ Extra params not declared in the spec are also passed through:
549
+
550
+ ```typescript
551
+ // params: { section: "profile", tab: "security" }
552
+ // Component receives: { section: "profile", tab: "security", user: "alice" }
553
+ ```
554
+
555
+ ### Context fields via `contextProps`
556
+
557
+ For states where context data (e.g. `context.username`) should reach a component but there
558
+ is no corresponding URL param, declare `contextProps` in the spec as an explicit allowlist:
559
+
560
+ ```typescript
561
+ // Dashboard at /dashboard — no :username URL param, but we want to show context.username
562
+ spec: {
563
+ root: "root",
564
+ contextProps: ["username"], // ← explicit allowlist
565
+ elements: {
566
+ root: { type: "Dashboard", props: { username: undefined }, children: [] },
567
+ },
568
+ }
569
+
570
+ // After auth.login sets context.username = "alice":
571
+ // Component receives: { username: "alice" }
572
+ ```
573
+
574
+ **Nothing leaks from context without an explicit `contextProps` declaration.**
575
+ Only the named fields are ever exposed. `null` and `undefined` context values are excluded.
576
+
577
+ If both a route param and a `contextProps` field share the same key, the route param wins:
578
+
579
+ ```typescript
580
+ // context.username = "alice" (logged-in user)
581
+ // play.route to /profile/demo → context.params.username = "demo"
582
+ // contextProps: ["username"], props: { username: undefined }
583
+ // Component receives: { username: "demo" } ← route param wins
422
584
  ```
423
585
 
424
586
  ## Architecture
425
587
 
426
- This package implements RFC Play v1 requirements:
588
+ This package implements Play RFC requirements:
427
589
 
428
590
  **Architectural Invariants:**
429
591
 
@@ -445,10 +607,16 @@ This package implements RFC Play v1 requirements:
445
607
 
446
608
  - **[@xmachines/play-actor](../play-actor)** - AbstractActor base class
447
609
  - **[@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
610
+ - **[@xmachines/play-router](../play-router)** - Route extraction utilities
611
+ - **[@xmachines/play-react](../play-react)** - React renderer
612
+ - **[@xmachines/play-vue](../play-vue)** - Vue renderer
613
+ - **[@xmachines/play-solid](../play-solid)** - SolidJS renderer
614
+ - **[@xmachines/play-dom](../play-dom)** - Vanilla DOM renderer
450
615
  - **[@xmachines/play](../play)** - Protocol types (PlayEvent)
451
616
 
452
617
  ## License
453
618
 
454
- MIT
619
+ Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
620
+
621
+ This work is licensed under the terms of the MIT license.
622
+ 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"}