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