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