@xmachines/play-xstate 1.0.0-beta.2 → 1.0.0-beta.21
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.
- package/README.md +263 -107
- package/dist/define-player.d.ts +6 -56
- package/dist/define-player.d.ts.map +1 -1
- package/dist/define-player.js +8 -60
- package/dist/define-player.js.map +1 -1
- package/dist/define-player.typecheck.d.ts +2 -0
- package/dist/define-player.typecheck.d.ts.map +1 -0
- package/dist/define-player.typecheck.js +48 -0
- package/dist/define-player.typecheck.js.map +1 -0
- package/dist/errors.d.ts +66 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +76 -0
- package/dist/errors.js.map +1 -0
- package/dist/guards/compose.d.ts +14 -3
- package/dist/guards/compose.d.ts.map +1 -1
- package/dist/guards/compose.js +26 -0
- package/dist/guards/compose.js.map +1 -1
- package/dist/guards/helpers.d.ts +13 -17
- package/dist/guards/helpers.d.ts.map +1 -1
- package/dist/guards/helpers.js +20 -25
- package/dist/guards/helpers.js.map +1 -1
- package/dist/guards/index.d.ts +2 -1
- package/dist/guards/index.d.ts.map +1 -1
- package/dist/guards/index.js +1 -1
- package/dist/guards/index.js.map +1 -1
- package/dist/guards/types.d.ts +3 -2
- package/dist/guards/types.d.ts.map +1 -1
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -5
- package/dist/index.js.map +1 -1
- package/dist/player-actor.d.ts +70 -22
- package/dist/player-actor.d.ts.map +1 -1
- package/dist/player-actor.js +290 -88
- package/dist/player-actor.js.map +1 -1
- package/dist/player-actor.typecheck.d.ts +2 -0
- package/dist/player-actor.typecheck.d.ts.map +1 -0
- package/dist/player-actor.typecheck.js +27 -0
- package/dist/player-actor.typecheck.js.map +1 -0
- package/dist/routing/build-url.d.ts +22 -16
- package/dist/routing/build-url.d.ts.map +1 -1
- package/dist/routing/build-url.js +27 -20
- package/dist/routing/build-url.js.map +1 -1
- package/dist/routing/derive-route.d.ts +2 -2
- package/dist/routing/derive-route.d.ts.map +1 -1
- package/dist/routing/derive-route.js +3 -3
- package/dist/routing/derive-route.js.map +1 -1
- package/dist/routing/format-play-route-transitions.d.ts +41 -4
- package/dist/routing/format-play-route-transitions.d.ts.map +1 -1
- package/dist/routing/format-play-route-transitions.js +22 -19
- package/dist/routing/format-play-route-transitions.js.map +1 -1
- package/dist/routing/index.d.ts +2 -1
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/types.d.ts +8 -13
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/signals/index.d.ts +0 -1
- package/dist/signals/index.d.ts.map +1 -1
- package/dist/signals/index.js +0 -1
- package/dist/signals/index.js.map +1 -1
- package/dist/signals/state-signal.d.ts +1 -1
- package/dist/signals/state-signal.d.ts.map +1 -1
- package/dist/types.d.ts +20 -14
- package/dist/types.d.ts.map +1 -1
- package/package.json +26 -19
- package/dist/catalog/index.d.ts +0 -12
- package/dist/catalog/index.d.ts.map +0 -1
- package/dist/catalog/index.js +0 -11
- package/dist/catalog/index.js.map +0 -1
- package/dist/catalog/types.d.ts +0 -36
- package/dist/catalog/types.d.ts.map +0 -1
- package/dist/catalog/types.js +0 -2
- package/dist/catalog/types.js.map +0 -1
- package/dist/catalog/validate-binding.d.ts +0 -21
- package/dist/catalog/validate-binding.d.ts.map +0 -1
- package/dist/catalog/validate-binding.js +0 -30
- package/dist/catalog/validate-binding.js.map +0 -1
- package/dist/catalog/validate-props.d.ts +0 -41
- package/dist/catalog/validate-props.d.ts.map +0 -1
- package/dist/catalog/validate-props.js +0 -95
- package/dist/catalog/validate-props.js.map +0 -1
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
|
|
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 [
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
context: {} as {
|
|
40
|
+
isAuthenticated: boolean;
|
|
41
|
+
routeParams: Record<string, string>;
|
|
42
|
+
queryParams: 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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
49
|
+
}).createMachine(
|
|
50
|
+
formatPlayRouteTransitions({
|
|
51
|
+
id: "app",
|
|
52
|
+
initial: "login",
|
|
53
|
+
context: { isAuthenticated: false, routeParams: {}, queryParams: {} },
|
|
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
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
//
|
|
91
|
-
const createPlayer = definePlayer({ machine
|
|
101
|
+
// 2. Create player factory
|
|
102
|
+
const createPlayer = definePlayer({ machine });
|
|
92
103
|
|
|
93
|
-
//
|
|
94
|
-
const actor = createPlayer(
|
|
104
|
+
// 3. Create and start actor
|
|
105
|
+
const actor = createPlayer();
|
|
95
106
|
actor.start();
|
|
96
107
|
|
|
97
|
-
//
|
|
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: "
|
|
110
|
+
console.log(actor.currentView.get()); // { component: "Login", spec: {...} }
|
|
103
111
|
|
|
104
|
-
//
|
|
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
|
|
120
|
+
Create a player factory from an XState machine:
|
|
113
121
|
|
|
114
122
|
```typescript
|
|
115
|
-
const createPlayer = definePlayer<TMachine
|
|
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,49 @@ 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
|
+
|
|
149
170
|
### PlayerActor
|
|
150
171
|
|
|
151
172
|
Concrete actor implementing Play signal protocol:
|
|
152
173
|
|
|
153
174
|
**Signal Properties:**
|
|
154
175
|
|
|
155
|
-
- `state: Signal.State<
|
|
156
|
-
- `currentRoute: Signal.Computed<string | null>`
|
|
157
|
-
- `currentView: Signal.
|
|
176
|
+
- `state: Signal.State<AnyMachineSnapshot>` — Reactive snapshot of current state
|
|
177
|
+
- `currentRoute: Signal.Computed<string | null>` — Derived URL from the current state's `meta.route` and `context.routeParams`. Returns `null` when no route metadata is present or a required route parameter is missing from context.
|
|
178
|
+
- `currentView: Signal.State<ViewMetadata | null>` — Current view spec (updated on every state transition). The spec is automatically enriched with `context.routeParams` (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
179
|
|
|
159
|
-
**
|
|
180
|
+
**Methods:**
|
|
160
181
|
|
|
161
|
-
- `
|
|
182
|
+
- `start()` — Start the actor (must call after creation)
|
|
183
|
+
- `stop()` — Stop the actor
|
|
184
|
+
- `send(event)` — Send event to actor. Typed as `EventFromLogic<TMachine>` when `TMachine` is specified.
|
|
185
|
+
- `dispose()` — Convenience cleanup (calls `stop()`)
|
|
186
|
+
- `getSnapshot()` — Get current XState snapshot, typed as `SnapshotFrom<TMachine>`
|
|
162
187
|
|
|
163
|
-
**
|
|
188
|
+
**Routing failure behavior:**
|
|
189
|
+
|
|
190
|
+
- `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.
|
|
191
|
+
- Errors from `buildRouteUrl()` are typed as `MissingRouteParamError` (importable from `@xmachines/play-xstate/errors`).
|
|
164
192
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
- `
|
|
168
|
-
- `
|
|
193
|
+
**View derivation and route-param enrichment:**
|
|
194
|
+
|
|
195
|
+
- `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).
|
|
196
|
+
- `context.routeParams` 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
197
|
|
|
170
198
|
**Example:**
|
|
171
199
|
|
|
@@ -193,7 +221,7 @@ import {
|
|
|
193
221
|
negateGuard,
|
|
194
222
|
hasContext,
|
|
195
223
|
eventMatches,
|
|
196
|
-
|
|
224
|
+
contextFieldMatches,
|
|
197
225
|
} from "@xmachines/play-xstate";
|
|
198
226
|
|
|
199
227
|
const machine = setup({
|
|
@@ -226,13 +254,11 @@ const machine = setup({
|
|
|
226
254
|
|
|
227
255
|
- `hasContext(path: string)` - Check if context property is truthy
|
|
228
256
|
- `eventMatches(type: string)` - Check event type
|
|
229
|
-
- `
|
|
257
|
+
- `contextFieldMatches(fieldPath: string, expectedValue: unknown)` - Check context field with proper equality (no substring false-matches; use this instead of the removed `stateMatches`)
|
|
230
258
|
- `composeGuards(guards: Array)` - AND composition
|
|
231
259
|
- `composeGuardsOr(guards: Array)` - OR composition
|
|
232
260
|
- `negateGuard(guard)` - NOT composition
|
|
233
261
|
|
|
234
|
-
**Complete API:** See [API Documentation](../../docs/api/@xmachines/play-xstate)
|
|
235
|
-
|
|
236
262
|
## Examples
|
|
237
263
|
|
|
238
264
|
### Guard Placement Philosophy
|
|
@@ -242,7 +268,6 @@ const machine = setup({
|
|
|
242
268
|
```typescript
|
|
243
269
|
import { setup } from "xstate";
|
|
244
270
|
import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
|
|
245
|
-
import { defineCatalog } from "@xmachines/play-catalog";
|
|
246
271
|
|
|
247
272
|
// Pattern 1: RECOMMENDED - Use formatPlayRouteTransitions utility
|
|
248
273
|
const machineConfig = {
|
|
@@ -275,17 +300,18 @@ const machineConfig = {
|
|
|
275
300
|
// formatPlayRouteTransitions handles routing infrastructure
|
|
276
301
|
const machine = setup({
|
|
277
302
|
types: {
|
|
278
|
-
|
|
303
|
+
context: {} as {
|
|
304
|
+
isAuthenticated: boolean;
|
|
305
|
+
routeParams: Record<string, string>;
|
|
306
|
+
queryParams: Record<string, string>;
|
|
307
|
+
},
|
|
308
|
+
events: {} as
|
|
309
|
+
| { type: "play.route"; to: string; params?: Record<string, string> }
|
|
310
|
+
| { type: "auth.login" },
|
|
279
311
|
},
|
|
280
312
|
}).createMachine(formatPlayRouteTransitions(machineConfig));
|
|
281
313
|
|
|
282
|
-
const
|
|
283
|
-
Home,
|
|
284
|
-
Dashboard,
|
|
285
|
-
Login,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const createPlayer = definePlayer({ machine, catalog });
|
|
314
|
+
const createPlayer = definePlayer({ machine });
|
|
289
315
|
const actor = createPlayer();
|
|
290
316
|
actor.start();
|
|
291
317
|
|
|
@@ -320,7 +346,6 @@ on: {
|
|
|
320
346
|
```typescript
|
|
321
347
|
const createPlayer = definePlayer({
|
|
322
348
|
machine,
|
|
323
|
-
catalog,
|
|
324
349
|
options: {
|
|
325
350
|
onStart: (actor) => {
|
|
326
351
|
console.log("Actor started:", actor.id);
|
|
@@ -355,7 +380,7 @@ import { definePlayer } from "@xmachines/play-xstate";
|
|
|
355
380
|
|
|
356
381
|
const { inspect } = createBrowserInspector();
|
|
357
382
|
|
|
358
|
-
const createPlayer = definePlayer({ machine
|
|
383
|
+
const createPlayer = definePlayer({ machine });
|
|
359
384
|
const actor = createPlayer();
|
|
360
385
|
actor.start();
|
|
361
386
|
|
|
@@ -400,30 +425,155 @@ meta: {
|
|
|
400
425
|
|
|
401
426
|
### View Metadata
|
|
402
427
|
|
|
428
|
+
View metadata drives the `currentView` signal. The `spec` field follows the `@json-render/core` `Spec` shape:
|
|
429
|
+
|
|
403
430
|
```typescript
|
|
404
431
|
meta: {
|
|
405
432
|
view: {
|
|
406
|
-
component: "Dashboard",
|
|
407
|
-
|
|
408
|
-
|
|
433
|
+
component: "Dashboard", // Component name — must exist in the registry
|
|
434
|
+
spec: {
|
|
435
|
+
root: "root", // Root element key
|
|
436
|
+
state: { tab: "general" }, // Optional: initial UI state (form values etc.)
|
|
437
|
+
elements: {
|
|
438
|
+
root: {
|
|
439
|
+
type: "Dashboard",
|
|
440
|
+
props: { title: "Dashboard" }, // Static props (non-undefined = cannot be overridden by route params)
|
|
441
|
+
children: [],
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
409
445
|
},
|
|
410
446
|
}
|
|
447
|
+
```
|
|
411
448
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
449
|
+
**Prop slots:** Declare a prop as `undefined` to allow URL params or context fields to fill
|
|
450
|
+
it in automatically (see [Prop Enrichment from Routing and Context](#prop-enrichment-from-routing-and-context)):
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// Route: /settings/:section? Context: { username: "alice" }
|
|
454
|
+
spec: {
|
|
455
|
+
root: "root",
|
|
456
|
+
contextProps: ["username"], // expose context.username to components
|
|
457
|
+
elements: {
|
|
458
|
+
root: { type: "Settings", props: { section: undefined, username: undefined, theme: "dark" }, children: [] },
|
|
459
|
+
},
|
|
460
|
+
}
|
|
461
|
+
// After play.route to /settings/profile:
|
|
462
|
+
// Component receives: { section: "profile", username: "alice", theme: "dark" }
|
|
463
|
+
// section → from routeParams (URL)
|
|
464
|
+
// username → from contextProps (context)
|
|
465
|
+
// theme → from spec (explicit value, untouched)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Error Handling
|
|
469
|
+
|
|
470
|
+
All runtime errors thrown by this package extend `PlayError` from `@xmachines/play` and
|
|
471
|
+
are exported from the `./errors` subpath:
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
import { MissingRouteParamError, InvalidEventError } from "@xmachines/play-xstate/errors";
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
| Class | Code | When thrown |
|
|
478
|
+
| ------------------------ | -------------------------- | -------------------------------------------------------- |
|
|
479
|
+
| `MissingRouteParamError` | `PLAY_ROUTE_PARAM_MISSING` | Required `:param` has no matching value in actor context |
|
|
480
|
+
| `InvalidEventError` | `PLAY_INVALID_EVENT` | `actor.send()` called with a non-object value |
|
|
481
|
+
|
|
482
|
+
`MissingRouteParamError` carries `param` and `template` fields for programmatic access.
|
|
483
|
+
`InvalidEventError` carries a `detail` property with the offending value.
|
|
484
|
+
|
|
485
|
+
> **Note:** `currentRoute` does NOT throw `MissingRouteParamError` — it catches the error internally and returns `null` instead, keeping signal watchers stable during transient states.
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { MissingRouteParamError, InvalidEventError } from "@xmachines/play-xstate/errors";
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
actor.send(null as any);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
if (err instanceof InvalidEventError) {
|
|
494
|
+
console.error("Non-object event passed to actor.send():", err.detail);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const route = actor.currentRoute.get();
|
|
500
|
+
} catch (err) {
|
|
501
|
+
if (err instanceof MissingRouteParamError) {
|
|
502
|
+
console.error(`Missing "${err.param}" for template "${err.template}"`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## Prop Enrichment from Routing and Context
|
|
508
|
+
|
|
509
|
+
When `currentView` is derived, each spec element's `props` are enriched from two sources.
|
|
510
|
+
Both use the **opt-in slot** pattern: declare a prop as `undefined` in the spec to allow
|
|
511
|
+
it to be filled in automatically.
|
|
512
|
+
|
|
513
|
+
### Merge priority (highest → lowest)
|
|
514
|
+
|
|
515
|
+
| Source | When it applies | Wins over |
|
|
516
|
+
| --------------------------------------- | -------------------------------- | ------------------------- |
|
|
517
|
+
| Explicit non-`undefined` spec prop | Always | Everything |
|
|
518
|
+
| URL route param (`context.routeParams`) | State has a `:param` URL segment | `contextProps` values |
|
|
519
|
+
| `contextProps` field | Listed in `spec.contextProps` | Nothing (lowest priority) |
|
|
520
|
+
|
|
521
|
+
### URL route parameters
|
|
522
|
+
|
|
523
|
+
URL path parameters (`:section?`, `:username`) are filled automatically — declare the prop
|
|
524
|
+
as `undefined` to opt in:
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
// Route: /settings/:section?
|
|
528
|
+
elements: {
|
|
529
|
+
root: { type: "Settings", props: { section: undefined, user: "alice" }, children: [] }
|
|
421
530
|
}
|
|
531
|
+
|
|
532
|
+
// play.route event: { to: "#settings", params: { section: "profile" } }
|
|
533
|
+
// Component receives: { section: "profile", user: "alice" }
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Extra params not declared in the spec are also passed through:
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// params: { section: "profile", tab: "security" }
|
|
540
|
+
// Component receives: { section: "profile", tab: "security", user: "alice" }
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Context fields via `contextProps`
|
|
544
|
+
|
|
545
|
+
For states where context data (e.g. `context.username`) should reach a component but there
|
|
546
|
+
is no corresponding URL param, declare `contextProps` in the spec as an explicit allowlist:
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
// Dashboard at /dashboard — no :username URL param, but we want to show context.username
|
|
550
|
+
spec: {
|
|
551
|
+
root: "root",
|
|
552
|
+
contextProps: ["username"], // ← explicit allowlist
|
|
553
|
+
elements: {
|
|
554
|
+
root: { type: "Dashboard", props: { username: undefined }, children: [] },
|
|
555
|
+
},
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// After auth.login sets context.username = "alice":
|
|
559
|
+
// Component receives: { username: "alice" }
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Nothing leaks from context without an explicit `contextProps` declaration.**
|
|
563
|
+
Only the named fields are ever exposed. `null` and `undefined` context values are excluded.
|
|
564
|
+
|
|
565
|
+
If both a route param and a `contextProps` field share the same key, the route param wins:
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// context.username = "alice" (logged-in user)
|
|
569
|
+
// play.route to /profile/demo → routeParams.username = "demo"
|
|
570
|
+
// contextProps: ["username"], props: { username: undefined }
|
|
571
|
+
// Component receives: { username: "demo" } ← route param wins
|
|
422
572
|
```
|
|
423
573
|
|
|
424
574
|
## Architecture
|
|
425
575
|
|
|
426
|
-
This package implements
|
|
576
|
+
This package implements Play RFC requirements:
|
|
427
577
|
|
|
428
578
|
**Architectural Invariants:**
|
|
429
579
|
|
|
@@ -445,10 +595,16 @@ This package implements RFC Play v1 requirements:
|
|
|
445
595
|
|
|
446
596
|
- **[@xmachines/play-actor](../play-actor)** - AbstractActor base class
|
|
447
597
|
- **[@xmachines/play-signals](../play-signals)** - TC39 Signals polyfill
|
|
448
|
-
- **[@xmachines/play-
|
|
449
|
-
- **[@xmachines/play-
|
|
598
|
+
- **[@xmachines/play-router](../play-router)** - Route extraction utilities
|
|
599
|
+
- **[@xmachines/play-react](../play-react)** - React renderer
|
|
600
|
+
- **[@xmachines/play-vue](../play-vue)** - Vue renderer
|
|
601
|
+
- **[@xmachines/play-solid](../play-solid)** - SolidJS renderer
|
|
602
|
+
- **[@xmachines/play-dom](../play-dom)** - Vanilla DOM renderer
|
|
450
603
|
- **[@xmachines/play](../play)** - Protocol types (PlayEvent)
|
|
451
604
|
|
|
452
605
|
## License
|
|
453
606
|
|
|
454
|
-
|
|
607
|
+
Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
|
|
608
|
+
|
|
609
|
+
This work is licensed under the terms of the MIT license.
|
|
610
|
+
For a copy, see <https://opensource.org/licenses/MIT>.
|
package/dist/define-player.d.ts
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
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
|
|
4
|
+
* Create a player factory from an XState machine
|
|
5
5
|
*
|
|
6
|
-
* Factory pattern that accepts an XState v5 machine
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,EAA8B,MAAM,YAAY,CAAC;AAG1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,SAAS,eAAe,EAC5D,QAAQ,YAAY,CAAC,QAAQ,CAAC,KAC5B,aAAa,CAAC,QAAQ,CAMxB,CAAC"}
|