@xmachines/play-xstate 3.0.0 → 5.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/README.md +190 -68
- package/dist/capabilities.d.ts +93 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +4 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/define-player.d.ts +7 -1
- package/dist/define-player.d.ts.map +1 -1
- package/dist/define-player.js +9 -60
- package/dist/define-player.js.map +1 -1
- package/dist/errors.d.ts +64 -23
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +93 -31
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -6
- package/dist/index.js.map +1 -1
- package/dist/player-actor.d.ts +73 -137
- package/dist/player-actor.d.ts.map +1 -1
- package/dist/player-actor.js +108 -254
- package/dist/player-actor.js.map +1 -1
- package/dist/routing/build-url.d.ts +8 -1
- package/dist/routing/build-url.d.ts.map +1 -1
- package/dist/routing/build-url.js +34 -54
- package/dist/routing/build-url.js.map +1 -1
- package/dist/routing/derive-current-route.d.ts +12 -38
- package/dist/routing/derive-current-route.d.ts.map +1 -1
- package/dist/routing/derive-current-route.js +24 -82
- package/dist/routing/derive-current-route.js.map +1 -1
- package/dist/routing/derive-initial-route.d.ts +2 -1
- package/dist/routing/derive-initial-route.d.ts.map +1 -1
- package/dist/routing/derive-initial-route.js +13 -2
- package/dist/routing/derive-initial-route.js.map +1 -1
- package/dist/routing/derive-route.d.ts +81 -2
- package/dist/routing/derive-route.d.ts.map +1 -1
- package/dist/routing/derive-route.js +97 -3
- package/dist/routing/derive-route.js.map +1 -1
- package/dist/routing/format-play-route-transitions.d.ts +8 -2
- package/dist/routing/format-play-route-transitions.d.ts.map +1 -1
- package/dist/routing/format-play-route-transitions.js +175 -43
- package/dist/routing/format-play-route-transitions.js.map +1 -1
- package/dist/routing/index.d.ts +1 -1
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/types.d.ts +9 -5
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/state-meta.d.ts +52 -0
- package/dist/state-meta.d.ts.map +1 -0
- package/dist/state-meta.js +77 -0
- package/dist/state-meta.js.map +1 -0
- package/dist/types.d.ts +20 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/view/derive-current-view.d.ts +7 -6
- package/dist/view/derive-current-view.d.ts.map +1 -1
- package/dist/view/derive-current-view.js +7 -6
- package/dist/view/derive-current-view.js.map +1 -1
- package/dist/with-routing.d.ts +45 -0
- package/dist/with-routing.d.ts.map +1 -0
- package/dist/with-routing.js +140 -0
- package/dist/with-routing.js.map +1 -0
- package/dist/with-view.d.ts +57 -0
- package/dist/with-view.d.ts.map +1 -0
- package/dist/with-view.js +158 -0
- package/dist/with-view.js.map +1 -0
- package/package.json +40 -18
- package/dist/guards/compose.d.ts +0 -158
- package/dist/guards/compose.d.ts.map +0 -1
- package/dist/guards/compose.js +0 -188
- package/dist/guards/compose.js.map +0 -1
- package/dist/guards/helpers.d.ts +0 -62
- package/dist/guards/helpers.d.ts.map +0 -1
- package/dist/guards/helpers.js +0 -85
- package/dist/guards/helpers.js.map +0 -1
- package/dist/guards/index.d.ts +0 -20
- package/dist/guards/index.d.ts.map +0 -1
- package/dist/guards/index.js +0 -18
- package/dist/guards/index.js.map +0 -1
- package/dist/guards/types.d.ts +0 -22
- package/dist/guards/types.d.ts.map +0 -1
- package/dist/guards/types.js +0 -2
- package/dist/guards/types.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
# @xmachines/play-xstate
|
|
2
2
|
|
|
3
|
-
> XState v5 adapter for the XMachines Play Architecture. It binds a state machine to the actor base, with
|
|
3
|
+
> XState v5 adapter for the XMachines Play Architecture. It binds a state machine to the actor base, with atom-driven reactivity and a router integration.
|
|
4
4
|
|
|
5
|
-
[](https://opensource.org/licenses/MIT) [](https://opensource.org/licenses/MIT) [](https://www.npmjs.com/package/@xmachines/play-xstate)
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
> **Browser floor: Chrome 110, Firefox 115, Safari 16.4.** This package calls the ES2023
|
|
10
10
|
> change-by-copy array methods, so a browser below that floor throws
|
|
11
|
-
> `TypeError: ... is not a function`.
|
|
12
|
-
> `baseline-widely-available` target to Firefox 114, which is below it — raise
|
|
13
|
-
> `build.target` when you bundle for the browser. The root README carries the table.
|
|
11
|
+
> `TypeError: ... is not a function`. The root README carries the table.
|
|
14
12
|
|
|
15
13
|
## Installation
|
|
16
14
|
|
|
17
15
|
```bash
|
|
18
|
-
pnpm add @xmachines/play-xstate xstate
|
|
16
|
+
pnpm add @xmachines/play-xstate @xmachines/play @xmachines/play-atom xstate
|
|
19
17
|
```
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
**Peer dependencies.** Install them with the package:
|
|
20
|
+
|
|
21
|
+
- [`@xmachines/play`](../play/README.md) — the core protocol. This package reads `shallowEqualExcept` and `DISPOSE` from it.
|
|
22
|
+
- [`@xmachines/play-atom`](../play-atom/README.md) — the atom primitives. `PlayerActor` holds `currentView` and `currentRoute` as atoms.
|
|
23
|
+
- `xstate` ^5.33.0 — the XState v5 runtime.
|
|
24
|
+
|
|
25
|
+
**Optional peers.** Each capability subpath carries one. Install it when you read that subpath, and not before:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add @xmachines/play-router # @xmachines/play-xstate/routing
|
|
29
|
+
pnpm add @xmachines/play-view # @xmachines/play-xstate/view
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- [`@xmachines/play-router`](../play-router/README.md) — the routing capability reads `Routable`, `PlayRouteEvent` and `RouteData` from it.
|
|
33
|
+
- [`@xmachines/play-view`](../play-view/README.md) — the view capability reads `composePlayState`, `PlaySpec` and `Viewable` from it.
|
|
22
34
|
|
|
23
35
|
---
|
|
24
36
|
|
|
@@ -26,7 +38,8 @@ pnpm add @xmachines/play-xstate xstate
|
|
|
26
38
|
|
|
27
39
|
```typescript
|
|
28
40
|
import { setup } from "xstate";
|
|
29
|
-
import { definePlayer } from "@xmachines/play-xstate";
|
|
41
|
+
import { definePlayer, compose, PlayerActor } from "@xmachines/play-xstate";
|
|
42
|
+
import { withRouting } from "@xmachines/play-xstate/routing";
|
|
30
43
|
|
|
31
44
|
// 1. Define your XState v5 machine
|
|
32
45
|
const machine = setup({}).createMachine({
|
|
@@ -38,13 +51,13 @@ const machine = setup({}).createMachine({
|
|
|
38
51
|
});
|
|
39
52
|
|
|
40
53
|
// 2. Create a player factory
|
|
41
|
-
const createPlayer = definePlayer({ machine });
|
|
54
|
+
const createPlayer = definePlayer({ machine, actor: compose(PlayerActor, withRouting) });
|
|
42
55
|
|
|
43
56
|
// 3. Instantiate and start an actor
|
|
44
57
|
const actor = createPlayer();
|
|
45
58
|
actor.start();
|
|
46
59
|
|
|
47
|
-
// 4. Observe
|
|
60
|
+
// 4. Observe atom-based reactive state
|
|
48
61
|
console.log(actor.currentRoute.get()); // "/"
|
|
49
62
|
console.log(actor.state.get().value); // "idle"
|
|
50
63
|
|
|
@@ -133,16 +146,91 @@ console.log(restored.currentRoute.get()); // same route as when saved
|
|
|
133
146
|
|
|
134
147
|
### `PlayerActor<TMachine>`
|
|
135
148
|
|
|
136
|
-
This concrete actor class is an XState v5 actor that
|
|
149
|
+
This concrete actor class is an XState v5 actor that exposes reactive atoms. It holds the protocol of `PlayActor` and nothing else: `state` and `send`.
|
|
150
|
+
|
|
151
|
+
#### Atoms
|
|
152
|
+
|
|
153
|
+
| Atom | Type | Description |
|
|
154
|
+
| ------- | ------------------------------ | ---------------------------------------------------------------------------- |
|
|
155
|
+
| `state` | `Atom<SnapshotFrom<TMachine>>` | The current XState snapshot. The actor updates it on every active transition |
|
|
156
|
+
|
|
157
|
+
### The capabilities
|
|
158
|
+
|
|
159
|
+
Routing and the view are OPTIONAL, and each one is a mixin behind its own entry point. An application that declares no route loads no routing code, and an application that renders no view loads no view code and installs no json-render.
|
|
160
|
+
|
|
161
|
+
Each capability package is an OPTIONAL peer dependency. Install the one that you compose:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pnpm add @xmachines/play-router # for @xmachines/play-xstate/routing
|
|
165
|
+
pnpm add @xmachines/play-view # for @xmachines/play-xstate/view
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The main entry point of this package names neither. `@xmachines/play-xstate/routing` also
|
|
169
|
+
carries the route utilities — `deriveRoute`, `isAbsoluteRoute`, `buildRouteUrl`,
|
|
170
|
+
`formatPlayRouteTransitions` and the route types — because each one names
|
|
171
|
+
[`@xmachines/play-router`](../play-router/README.md) in its own types.
|
|
172
|
+
|
|
173
|
+
| Capability | Entry point | Adds | Interface |
|
|
174
|
+
| ---------- | -------------------------------- | ------------------------------ | ------------------------------------------------------------------ |
|
|
175
|
+
| Routing | `@xmachines/play-xstate/routing` | `currentRoute`, `initialRoute` | `Routable` of [`@xmachines/play-router`](../play-router/README.md) |
|
|
176
|
+
| View | `@xmachines/play-xstate/view` | `currentView` | `Viewable` of [`@xmachines/play-view`](../play-view/README.md) |
|
|
137
177
|
|
|
138
|
-
|
|
178
|
+
`compose` applies each capability from left to right:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { definePlayer, PlayerActor, compose } from "@xmachines/play-xstate";
|
|
182
|
+
import { withRouting } from "@xmachines/play-xstate/routing";
|
|
183
|
+
import { withView } from "@xmachines/play-xstate/view";
|
|
184
|
+
|
|
185
|
+
// Both capabilities
|
|
186
|
+
const createPlayer = definePlayer({
|
|
187
|
+
machine,
|
|
188
|
+
actor: compose(PlayerActor, withRouting, withView),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Routing alone: `currentView` is a compile error on this actor
|
|
192
|
+
const createRouted = definePlayer({ machine, actor: compose(PlayerActor, withRouting) });
|
|
193
|
+
|
|
194
|
+
// Neither: `state` and `send`
|
|
195
|
+
const createBare = definePlayer({ machine });
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**The composition ORDER decides nothing.** `currentRoute` and `currentView` are computed atoms over `state`, and neither reads the other, so one write for each transition reaches both and the engine evaluates them in topological order. A router bridge therefore sees a guard redirect and a renderer sees the view of the same snapshot, whatever order `compose` applied. A release before this one wrote `currentView` from an `onSnapshot` override, and the order was load-bearing there.
|
|
199
|
+
|
|
200
|
+
**A lifecycle hook receives the bare actor.** `PlayerOptions` is typed before the class is composed, so it cannot name the capabilities, and a narrower hook parameter is refused under `strictFunctionTypes`. A hook that reads `currentRoute` or `currentView` reads them through a binding of the composed type:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
let composed: PlayerActor<typeof machine> & Routable & Viewable;
|
|
204
|
+
|
|
205
|
+
const actor = definePlayer({
|
|
206
|
+
machine,
|
|
207
|
+
options: { onStateChange: () => console.log(composed.currentRoute.get()) },
|
|
208
|
+
actor: compose(PlayerActor, withRouting, withView),
|
|
209
|
+
})();
|
|
210
|
+
composed = actor;
|
|
211
|
+
```
|
|
139
212
|
|
|
140
|
-
|
|
|
141
|
-
| -------------- |
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `currentView` | `
|
|
145
|
-
|
|
213
|
+
| Atom | Type | Capability | Description |
|
|
214
|
+
| -------------- | -------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
215
|
+
| `currentRoute` | `ReadonlyAtom<string \| null>` | routing | The URL that comes from the `meta.route` template of the active state and from the context |
|
|
216
|
+
| `initialRoute` | `readonly string \| null` | routing | The route of the initial state of the machine. The construction fixes it, and a router bridge uses it to detect a deep link or a restore |
|
|
217
|
+
| `currentView` | `ReadonlyAtom<PlaySpec \| null>` | view | The view spec from the `meta.view` metadata of the active state, with the context params added |
|
|
218
|
+
|
|
219
|
+
`state` is the ONE atom that the actor writes. `currentRoute` and `currentView` are
|
|
220
|
+
COMPUTED over it, so one write for each transition reaches both in one propagation and no
|
|
221
|
+
observer reads the new state beside an old derivation. Each derivation reports a failure
|
|
222
|
+
through the `onError` option and keeps its last good value; it throws never, because it
|
|
223
|
+
runs inside the write.
|
|
224
|
+
|
|
225
|
+
**`currentRoute` notifies on every transition, and `currentView` notifies on a change of
|
|
226
|
+
the view alone.** The two atoms answer two different questions. A router bridge needs the
|
|
227
|
+
answer of the actor to each event that it proposed, and a guard that refuses an inbound
|
|
228
|
+
location and holds the machine where it was derives the SAME path — so `currentRoute`
|
|
229
|
+
compares "different" always, and the bridge learns that the address bar must go back.
|
|
230
|
+
`currentView` gates its emission on the value, and a snapshot that changes no view on the
|
|
231
|
+
screen keeps the previous reference, so a provider below it mounts the UI again not on
|
|
232
|
+
every event. Debounce your own `watchAtom(actor.currentRoute, ...)` callback, or compare
|
|
233
|
+
the path yourself, when you run work that belongs to a change of the route alone.
|
|
146
234
|
|
|
147
235
|
#### Methods
|
|
148
236
|
|
|
@@ -153,59 +241,45 @@ This concrete actor class is an XState v5 actor that also exposes reactive TC39
|
|
|
153
241
|
| `send(event)` | Sends a typed event to the machine and calls the `onTransition` hook |
|
|
154
242
|
| `can(event)` | Returns `true` when the current state accepts the given event |
|
|
155
243
|
| `getSnapshot()` | Returns the current XState snapshot |
|
|
156
|
-
| `dispose()` | The alias of `stop()`. Deprecated — call `stop()`, or write `using` |
|
|
157
244
|
| `[DISPOSE]()` | Stops the actor at the end of a `using` scope. The release is `stop()` |
|
|
158
245
|
|
|
159
|
-
####
|
|
246
|
+
#### Atom usage example
|
|
160
247
|
|
|
161
248
|
```typescript
|
|
162
|
-
import {
|
|
249
|
+
import { watchAtom } from "@xmachines/play-atom";
|
|
163
250
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
console.log("Route changed:", actor.currentRoute.get());
|
|
167
|
-
});
|
|
251
|
+
const stop = watchAtom(actor.currentRoute, (route) => {
|
|
252
|
+
console.log("Route changed:", route);
|
|
168
253
|
});
|
|
169
254
|
|
|
170
|
-
watcher.watch(actor.currentRoute);
|
|
171
255
|
actor.start();
|
|
256
|
+
|
|
257
|
+
// Later, on teardown
|
|
258
|
+
stop();
|
|
172
259
|
```
|
|
173
260
|
|
|
174
261
|
---
|
|
175
262
|
|
|
176
|
-
###
|
|
177
|
-
|
|
178
|
-
> **Deprecated:** these guard helpers wrap the `and()`, `or()`, and `not()`
|
|
179
|
-
> combinators of XState, but they do not compose with a guard slot that
|
|
180
|
-
> `setup()` types. Use the combinators of XState directly. The next major
|
|
181
|
-
> version removes this module.
|
|
263
|
+
### Guards
|
|
182
264
|
|
|
183
|
-
|
|
265
|
+
This package gives no guard utility. Compose a guard with the `and()`, `or()` and `not()` combinators of XState:
|
|
184
266
|
|
|
185
267
|
```typescript
|
|
186
|
-
import { setup } from "xstate";
|
|
187
|
-
import {
|
|
188
|
-
composeGuards, // AND logic: all guards must pass
|
|
189
|
-
composeGuardsOr, // OR logic: at least one guard must pass
|
|
190
|
-
negateGuard, // NOT logic: inverts a guard
|
|
191
|
-
hasContext, // guard: context field is present and non-null
|
|
192
|
-
eventMatches, // guard: event type matches a string
|
|
193
|
-
contextFieldMatches, // guard: context field equals a value
|
|
194
|
-
} from "@xmachines/play-xstate";
|
|
268
|
+
import { and, not, setup } from "xstate";
|
|
195
269
|
|
|
196
270
|
const machine = setup({
|
|
197
271
|
guards: {
|
|
198
|
-
isLoggedIn: ({ context }) =>
|
|
272
|
+
isLoggedIn: ({ context }) => context.userId !== "",
|
|
199
273
|
hasAdminRole: ({ context }) => context.role === "admin",
|
|
200
274
|
},
|
|
201
275
|
}).createMachine({
|
|
202
276
|
on: {
|
|
203
277
|
accessAdmin: {
|
|
204
|
-
guard:
|
|
278
|
+
guard: and(["isLoggedIn", "hasAdminRole"]),
|
|
205
279
|
target: "adminPanel",
|
|
206
280
|
},
|
|
207
281
|
accessLogin: {
|
|
208
|
-
guard:
|
|
282
|
+
guard: not("isLoggedIn"),
|
|
209
283
|
target: "login",
|
|
210
284
|
},
|
|
211
285
|
},
|
|
@@ -213,6 +287,10 @@ const machine = setup({
|
|
|
213
287
|
});
|
|
214
288
|
```
|
|
215
289
|
|
|
290
|
+
A combinator resolves each name against the `guards` of `setup()`. A name that the map does not hold therefore fails to compile, and it names the fault: `Type '"typoGuardName"' is not assignable to type '"isLoggedIn"'`.
|
|
291
|
+
|
|
292
|
+
`composeGuards`, `composeGuardsOr`, `negateGuard`, `hasContext`, `eventMatches` and `contextFieldMatches` stood here before. They closed two gaps of the types of XState 5.28. The peer floor of this package is `^5.33.0`, and 5.33 holds neither gap. The helpers also fitted a guard slot that `setup()` types never: the example of this package reached them through an `as never` cast. Write the combinator instead, and write a plain predicate in place of each helper.
|
|
293
|
+
|
|
216
294
|
---
|
|
217
295
|
|
|
218
296
|
### Routing utilities
|
|
@@ -225,7 +303,7 @@ This function reads each machine state that has a `meta.route` field. It then ge
|
|
|
225
303
|
|
|
226
304
|
```typescript
|
|
227
305
|
import { setup } from "xstate";
|
|
228
|
-
import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
|
|
306
|
+
import { formatPlayRouteTransitions } from "@xmachines/play-xstate/routing";
|
|
229
307
|
|
|
230
308
|
const config = formatPlayRouteTransitions({
|
|
231
309
|
id: "app",
|
|
@@ -309,17 +387,75 @@ import type {
|
|
|
309
387
|
PlayerOptions, // Lifecycle hooks (onStart, onStop, onTransition, onStateChange, onError) + inspect
|
|
310
388
|
PlayerFactory, // Factory function returned by definePlayer()
|
|
311
389
|
PlayerFactoryResumeOptions, // { snapshot? } for restoring actor state
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
390
|
+
} from "@xmachines/play-xstate";
|
|
391
|
+
|
|
392
|
+
import type {
|
|
315
393
|
RouteMachineConfig, // Minimal machine config accepted by formatPlayRouteTransitions
|
|
316
394
|
RouteStateNode, // Single state node shape used during route crawling
|
|
317
395
|
RouteContext, // Context shape expected by buildRouteUrl ({ params?, query?, basePath?, hash? })
|
|
318
|
-
RouteObject, // Route metadata object shape: { path
|
|
396
|
+
RouteObject, // Route metadata object shape: { path, reenter?, handler?, data? }
|
|
319
397
|
RouteMetadata, // Union: string | RouteObject
|
|
320
|
-
|
|
398
|
+
RouteData, // The resolved extra data of a route: Record<string, unknown>
|
|
399
|
+
RouteDataResolver, // The function form of RouteObject.data
|
|
400
|
+
} from "@xmachines/play-xstate/routing";
|
|
321
401
|
```
|
|
322
402
|
|
|
403
|
+
`RouteObject`, `RouteMetadata`, `RouteData`, and `RouteDataResolver` have one
|
|
404
|
+
definition, and it lives in [`@xmachines/play-router`](../play-router/README.md). `@xmachines/play-xstate/routing`
|
|
405
|
+
re-exports them, so either import path gives you the same type. The MAIN entry point of
|
|
406
|
+
this package re-exports them no longer: it would name an optional peer in the types of
|
|
407
|
+
every consumer, including one that composes no routing.
|
|
408
|
+
|
|
409
|
+
### The object form of `meta.route`
|
|
410
|
+
|
|
411
|
+
The string form declares the path only. The object form declares the path and the
|
|
412
|
+
behaviour of the generated `play.route` transition:
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
meta: {
|
|
416
|
+
route: {
|
|
417
|
+
path: "/doc/:docId",
|
|
418
|
+
// Where the generated transition sits. The default is "root", which is what
|
|
419
|
+
// XState does: every route transition sits on the root of the machine.
|
|
420
|
+
// "root" — one transition on the root. A route from any state arrives.
|
|
421
|
+
// "local" — one transition on the PARENT. The parent keeps its entry action,
|
|
422
|
+
// and a route from outside the parent arrives NOWHERE.
|
|
423
|
+
// "both" — one in each place. The parent keeps its entry action, and a route
|
|
424
|
+
// from outside the parent still arrives.
|
|
425
|
+
handler: "both",
|
|
426
|
+
// Whether the transition re-enters its OWN DOMAIN. The default is false, which
|
|
427
|
+
// is the default of XState. Under `handler: "root"` the domain is the root of
|
|
428
|
+
// the machine, so `false` spares the root alone: each ancestor between the root
|
|
429
|
+
// and the target still runs its exit and its entry actions. `handler` is the
|
|
430
|
+
// field that spares those intermediate ancestors.
|
|
431
|
+
reenter: false,
|
|
432
|
+
// The extra data of the route. The generated transition assigns it to
|
|
433
|
+
// `context.data`. A literal value, or a function of { context, event }.
|
|
434
|
+
data: { titleKey: "doc.view" },
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
An unknown `handler` value throws `InvalidRouteHandlerError` at the format time.
|
|
440
|
+
|
|
441
|
+
> **Upgrading from 3.x: the `reenter` default changed, and a machine that relies on it
|
|
442
|
+
> goes quiet.** An earlier release wrote `reenter: true` on EVERY generated transition,
|
|
443
|
+
> so every routed state re-entered its own domain on each navigation and ran its `entry`
|
|
444
|
+
> actions again. The flag is now what `meta.route` declares, and the default is `false` —
|
|
445
|
+
> which is the default of XState. A state whose `entry` action must run on each
|
|
446
|
+
> navigation has to declare `reenter: true` for itself. Nothing throws, and the action
|
|
447
|
+
> simply stops running.
|
|
448
|
+
|
|
449
|
+
The `data` field follows the `WithDynamicParams` shape of XState, so the function form
|
|
450
|
+
reads the context and the event:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
data: ({ context, event }) => ({ title: `Document ${event.params?.docId}` });
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
A function does not survive `JSON.stringify`. Use the literal form for a machine that
|
|
457
|
+
Stately Studio reads, or that a process sends over a wire.
|
|
458
|
+
|
|
323
459
|
---
|
|
324
460
|
|
|
325
461
|
## Error Classes
|
|
@@ -329,31 +465,17 @@ The `@xmachines/play-xstate/errors` subpath exports the error classes. The main
|
|
|
329
465
|
```typescript
|
|
330
466
|
import {
|
|
331
467
|
MissingRouteParamError, // Required :param absent from context when resolving currentRoute
|
|
332
|
-
|
|
468
|
+
InvalidRouteParamError, // A :param carries a dot segment, which a URL resolves away
|
|
333
469
|
MissingStateIdError, // meta.route declared without a state id field
|
|
334
470
|
InvalidMachineError, // PlayerActor constructed with a non-object machine
|
|
335
471
|
InvalidEventError, // actor.send() called with null/undefined/non-object
|
|
336
472
|
ActorThrewNonErrorError, // actor failed with a thrown value that is not an Error
|
|
337
473
|
InvalidRouteMetadataError, // meta.route is neither a string nor { path: string }
|
|
338
|
-
|
|
474
|
+
InvalidRouteHandlerError, // meta.route.handler is not "root", "local", or "both"
|
|
339
475
|
} from "@xmachines/play-xstate/errors";
|
|
340
476
|
```
|
|
341
477
|
|
|
342
|
-
Every error class extends `PlayError` from `@xmachines/play
|
|
343
|
-
|
|
344
|
-
---
|
|
345
|
-
|
|
346
|
-
## Testing
|
|
347
|
-
|
|
348
|
-
```bash
|
|
349
|
-
# Run tests for this package in isolation
|
|
350
|
-
pnpm --filter @xmachines/play-xstate test
|
|
351
|
-
|
|
352
|
-
# Watch mode
|
|
353
|
-
pnpm --filter @xmachines/play-xstate run test:watch
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
The tests use [Vitest](https://vitest.dev/). They are in `packages/play-xstate/test/`.
|
|
478
|
+
Every error class extends `PlayError` from [`@xmachines/play/errors`](../play/README.md). Each class also carries typed detail fields, such as `param`, `template`, and `handler`. Your code therefore reads the details of an error, and it does not parse the message.
|
|
357
479
|
|
|
358
480
|
---
|
|
359
481
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composition of the optional capabilities of an actor.
|
|
3
|
+
*
|
|
4
|
+
* `PlayerActor` gives the protocol of `PlayActor` and nothing else: `state` and
|
|
5
|
+
* `send`. Routing and the view are capabilities, each one a MIXIN in a module that the
|
|
6
|
+
* core imports never. An application that renders no view therefore loads no view code,
|
|
7
|
+
* and an application that declares no route loads no routing code.
|
|
8
|
+
*
|
|
9
|
+
* {@link compose} applies each capability from left to right, so the LAST one is the
|
|
10
|
+
* outermost class. The ORDER decides no value: a capability that publishes one derives it
|
|
11
|
+
* with a computed atom over `state`, and the engine evaluates every derivation from the
|
|
12
|
+
* one write of a transition, in topological order. `onSnapshot` remains for a capability
|
|
13
|
+
* that must run an EFFECT, and an override of it still calls `super.onSnapshot()` first.
|
|
14
|
+
*
|
|
15
|
+
* ## Why the types read this way
|
|
16
|
+
*
|
|
17
|
+
* A capability is typed by the INSTANCE that it extends, and not by a type parameter for
|
|
18
|
+
* the class. That one decision removes three problems at once:
|
|
19
|
+
*
|
|
20
|
+
* - A class that extends a TYPE PARAMETER must constrain it to `new (...args: any[])`, and
|
|
21
|
+
* TypeScript reports TS2545 for every other spelling. A concrete constructor type needs
|
|
22
|
+
* no `any`.
|
|
23
|
+
* - A mixin that INFERS its return type emits no declaration: `Actor` of XState carries
|
|
24
|
+
* private members such as `_actorScope`, and an anonymous class that holds one is not
|
|
25
|
+
* nameable (TS4094).
|
|
26
|
+
* - A conditional return type cannot be checked against the class that a mixin builds, so
|
|
27
|
+
* it forces `as unknown as` at the return. Nothing here asserts anything.
|
|
28
|
+
*
|
|
29
|
+
* `TCapabilities` carries what the base already added, so a composed constructor names
|
|
30
|
+
* every capability on its instance. {@link compose} is what accumulates them:
|
|
31
|
+
* `compose(PlayerActor, withRouting, withView)` answers an actor that is `Routable` AND
|
|
32
|
+
* `Viewable`. Each capability declares a FIXED return type, so the nested form
|
|
33
|
+
* `withView(withRouting(PlayerActor))` answers `Viewable` alone.
|
|
34
|
+
*/
|
|
35
|
+
import type { ActorOptions, AnyStateMachine, InputFrom } from "xstate";
|
|
36
|
+
import type { PlayerActor } from "./player-actor.js";
|
|
37
|
+
import type { PlayerOptions } from "./types.js";
|
|
38
|
+
/**
|
|
39
|
+
* The constructor of a `PlayerActor`, with every capability that it carries already.
|
|
40
|
+
*
|
|
41
|
+
* The argument list is the one that `PlayerActor` declares, so `definePlayer` builds an
|
|
42
|
+
* actor with the arguments checked, whatever the composition added to the instance.
|
|
43
|
+
*
|
|
44
|
+
* @typeParam TMachine - The machine of the actor.
|
|
45
|
+
* @typeParam TCapabilities - What the capabilities added, for example `Routable & Viewable`.
|
|
46
|
+
*/
|
|
47
|
+
export type PlayerConstructor<TMachine extends AnyStateMachine, TCapabilities extends object = object> = new (machine: TMachine, options: PlayerOptions<TMachine>, input?: InputFrom<TMachine>, restoredSnapshot?: ActorOptions<TMachine>["snapshot"]) => PlayerActor<TMachine> & TCapabilities;
|
|
48
|
+
/**
|
|
49
|
+
* One capability of an actor: a function that takes an actor class and answers the same
|
|
50
|
+
* class with `TAdded` on the instance.
|
|
51
|
+
*
|
|
52
|
+
* `withRouting` of `@xmachines/play-xstate/routing` and `withView` of
|
|
53
|
+
* `@xmachines/play-xstate/view` are the two that this package ships. Write your own with
|
|
54
|
+
* the same shape, and {@link compose} takes it beside them.
|
|
55
|
+
*
|
|
56
|
+
* @typeParam TMachine - The machine of the actor.
|
|
57
|
+
* @typeParam TAdded - What the capability adds to the instance, for example `Routable`.
|
|
58
|
+
*/
|
|
59
|
+
export type Capability<TMachine extends AnyStateMachine, TAdded extends object> = (Base: PlayerConstructor<TMachine>) => PlayerConstructor<TMachine, TAdded>;
|
|
60
|
+
/**
|
|
61
|
+
* Builds an actor class from a base and a list of capabilities.
|
|
62
|
+
*
|
|
63
|
+
* It applies each capability from left to right, so the LAST one is the outermost class,
|
|
64
|
+
* and the return type accumulates what each one adds:
|
|
65
|
+
* `compose(PlayerActor, withRouting, withView)` is `Routable` AND `Viewable`.
|
|
66
|
+
*
|
|
67
|
+
* Use this form, and not the nested one. Each capability declares a FIXED return type, so
|
|
68
|
+
* `withView(withRouting(PlayerActor))` builds the same class at run time and answers
|
|
69
|
+
* `Viewable` alone at the type level — a router bridge then refuses the actor with
|
|
70
|
+
* TS2739. The overloads below are what carries `Routable` through.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* import { definePlayer, PlayerActor, compose } from "@xmachines/play-xstate";
|
|
75
|
+
* import { withRouting } from "@xmachines/play-xstate/routing";
|
|
76
|
+
* import { withView } from "@xmachines/play-xstate/view";
|
|
77
|
+
*
|
|
78
|
+
* const createPlayer = definePlayer({
|
|
79
|
+
* machine,
|
|
80
|
+
* actor: compose(PlayerActor, withRouting, withView),
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* Each overload takes the capabilities as separate arguments, `c1` to `c3`, and it
|
|
85
|
+
* applies them from left to right.
|
|
86
|
+
*
|
|
87
|
+
* @param Base - The actor class to build on, normally `PlayerActor`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function compose<TMachine extends AnyStateMachine>(Base: PlayerConstructor<TMachine>): PlayerConstructor<TMachine>;
|
|
90
|
+
export declare function compose<TMachine extends AnyStateMachine, TC1 extends object>(Base: PlayerConstructor<TMachine>, c1: Capability<TMachine, TC1>): PlayerConstructor<TMachine, TC1>;
|
|
91
|
+
export declare function compose<TMachine extends AnyStateMachine, TC1 extends object, TC2 extends object>(Base: PlayerConstructor<TMachine>, c1: Capability<TMachine, TC1>, c2: Capability<TMachine, TC2>): PlayerConstructor<TMachine, TC1 & TC2>;
|
|
92
|
+
export declare function compose<TMachine extends AnyStateMachine, TC1 extends object, TC2 extends object, TC3 extends object>(Base: PlayerConstructor<TMachine>, c1: Capability<TMachine, TC1>, c2: Capability<TMachine, TC2>, c3: Capability<TMachine, TC3>): PlayerConstructor<TMachine, TC1 & TC2 & TC3>;
|
|
93
|
+
//# sourceMappingURL=capabilities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,CAC5B,QAAQ,SAAS,eAAe,EAChC,aAAa,SAAS,MAAM,GAAG,MAAM,IAClC,KACH,OAAO,EAAE,QAAQ,EACjB,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,EAChC,KAAK,CAAC,EAAE,SAAS,CAAC,QAAQ,CAAC,EAC3B,gBAAgB,CAAC,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,KACjD,WAAW,CAAC,QAAQ,CAAC,GAAG,aAAa,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,MAAM,MAAM,UAAU,CAAC,QAAQ,SAAS,eAAe,EAAE,MAAM,SAAS,MAAM,IAAI,CACjF,IAAI,EAAE,iBAAiB,CAAC,QAAQ,CAAC,KAC7B,iBAAiB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,OAAO,CAAC,QAAQ,SAAS,eAAe,EACvD,IAAI,EAAE,iBAAiB,CAAC,QAAQ,CAAC,GAC/B,iBAAiB,CAAC,QAAQ,CAAC,CAAC;AAC/B,wBAAgB,OAAO,CAAC,QAAQ,SAAS,eAAe,EAAE,GAAG,SAAS,MAAM,EAC3E,IAAI,EAAE,iBAAiB,CAAC,QAAQ,CAAC,EACjC,EAAE,EAAE,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,GAC3B,iBAAiB,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AACpC,wBAAgB,OAAO,CAAC,QAAQ,SAAS,eAAe,EAAE,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAC/F,IAAI,EAAE,iBAAiB,CAAC,QAAQ,CAAC,EACjC,EAAE,EAAE,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,EAC7B,EAAE,EAAE,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,GAC3B,iBAAiB,CAAC,QAAQ,EAAE,GAAG,GAAG,GAAG,CAAC,CAAC;AAC1C,wBAAgB,OAAO,CACtB,QAAQ,SAAS,eAAe,EAChC,GAAG,SAAS,MAAM,EAClB,GAAG,SAAS,MAAM,EAClB,GAAG,SAAS,MAAM,EAElB,IAAI,EAAE,iBAAiB,CAAC,QAAQ,CAAC,EACjC,EAAE,EAAE,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,EAC7B,EAAE,EAAE,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,EAC7B,EAAE,EAAE,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,GAC3B,iBAAiB,CAAC,QAAQ,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AA6HA,MAAM,UAAU,OAAO,CACtB,IAAiC,EACjC,GAAG,YAEF;IAED,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;AAC5E,CAAC"}
|
package/dist/define-player.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AnyStateMachine } from "xstate";
|
|
2
2
|
import type { PlayerConfig, PlayerFactory } from "./types.js";
|
|
3
|
+
import type { PlayerConstructor } from "./capabilities.js";
|
|
3
4
|
/**
|
|
4
5
|
* Creates a player factory from an XState machine
|
|
5
6
|
*
|
|
@@ -56,5 +57,10 @@ import type { PlayerConfig, PlayerFactory } from "./types.js";
|
|
|
56
57
|
* @see {@link PlayerConfig} for the configuration options
|
|
57
58
|
* @see {@link PlayerFactory} for the signature of the factory function
|
|
58
59
|
*/
|
|
59
|
-
export declare
|
|
60
|
+
export declare function definePlayer<TMachine extends AnyStateMachine>(config: PlayerConfig<TMachine> & {
|
|
61
|
+
actor?: undefined;
|
|
62
|
+
}): PlayerFactory<TMachine>;
|
|
63
|
+
export declare function definePlayer<TMachine extends AnyStateMachine, TCapabilities extends object>(config: PlayerConfig<TMachine, TCapabilities> & {
|
|
64
|
+
actor: PlayerConstructor<TMachine, TCapabilities>;
|
|
65
|
+
}): PlayerFactory<TMachine, TCapabilities>;
|
|
60
66
|
//# 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,EAA8B,MAAM,YAAY,CAAC;
|
|
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;AAC1F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,YAAY,CAAC,QAAQ,SAAS,eAAe,EAC5D,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,GAAG;IAAE,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,GACpD,aAAa,CAAC,QAAQ,CAAC,CAAC;AAC3B,wBAAgB,YAAY,CAAC,QAAQ,SAAS,eAAe,EAAE,aAAa,SAAS,MAAM,EAC1F,MAAM,EAAE,YAAY,CAAC,QAAQ,EAAE,aAAa,CAAC,GAAG;IAC/C,KAAK,EAAE,iBAAiB,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;CAClD,GACC,aAAa,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC"}
|
package/dist/define-player.js
CHANGED
|
@@ -1,62 +1,11 @@
|
|
|
1
1
|
import { PlayerActor } from "./player-actor.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
* @param config - The configuration object of the player
|
|
13
|
-
* @param config.machine - The XState v5 state machine
|
|
14
|
-
* @param config.options - The optional lifecycle hooks: onStart, onTransition, and the others
|
|
15
|
-
*
|
|
16
|
-
* @returns The factory function. It makes an actor instance, with an optional input context
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* A basic player factory
|
|
20
|
-
* ```typescript
|
|
21
|
-
* import { setup } from "xstate";
|
|
22
|
-
* import { definePlayer } from "@xmachines/play-xstate";
|
|
23
|
-
*
|
|
24
|
-
* const machine = setup({}).createMachine({
|
|
25
|
-
* initial: 'idle',
|
|
26
|
-
* states: {
|
|
27
|
-
* idle: { meta: { route: '/' } },
|
|
28
|
-
* active: { meta: { route: '/active' } }
|
|
29
|
-
* }
|
|
30
|
-
* });
|
|
31
|
-
*
|
|
32
|
-
* const createPlayer = definePlayer({ machine });
|
|
33
|
-
* const actor = createPlayer();
|
|
34
|
-
* actor.start();
|
|
35
|
-
* ```
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* More than one actor instance from one factory
|
|
39
|
-
* ```typescript
|
|
40
|
-
* const createPlayer = definePlayer({ machine });
|
|
41
|
-
*
|
|
42
|
-
* // Create an actor for each user
|
|
43
|
-
* const alice = createPlayer({ userId: 'alice' });
|
|
44
|
-
* const bob = createPlayer({ userId: 'bob' });
|
|
45
|
-
*
|
|
46
|
-
* alice.start();
|
|
47
|
-
* bob.start();
|
|
48
|
-
*
|
|
49
|
-
* // The two state machines are independent
|
|
50
|
-
* console.log(alice.state.get() !== bob.state.get());
|
|
51
|
-
* ```
|
|
52
|
-
*
|
|
53
|
-
* @see [Play RFC](../../docs/rfc/play.md)
|
|
54
|
-
* @see {@link PlayerActor} for the concrete actor implementation
|
|
55
|
-
* @see {@link PlayerConfig} for the configuration options
|
|
56
|
-
* @see {@link PlayerFactory} for the signature of the factory function
|
|
57
|
-
*/
|
|
58
|
-
export const definePlayer = (config) => {
|
|
59
|
-
const { machine, options } = config;
|
|
60
|
-
return (input, restore) => new PlayerActor(machine, options ?? {}, input, restore?.snapshot);
|
|
61
|
-
};
|
|
2
|
+
export function definePlayer(config) {
|
|
3
|
+
const { machine, options, actor } = config;
|
|
4
|
+
// `PlayerActor` is the default, and it carries no capability. A consumer that routes or
|
|
5
|
+
// renders passes a composed class, and this module imports neither capability. The two
|
|
6
|
+
// overloads above are what keeps the answer exact: the default one answers a bare
|
|
7
|
+
// player, and the other answers the player with everything that the composition added.
|
|
8
|
+
const ActorClass = actor ?? PlayerActor;
|
|
9
|
+
return (input, restore) => new ActorClass(machine, options ?? {}, input, restore?.snapshot);
|
|
10
|
+
}
|
|
62
11
|
//# sourceMappingURL=define-player.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-player.js","sourceRoot":"","sources":["../src/define-player.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"define-player.js","sourceRoot":"","sources":["../src/define-player.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAkEhD,MAAM,UAAU,YAAY,CAC3B,MAA6C;IAE7C,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;IAC3C,wFAAwF;IACxF,uFAAuF;IACvF,kFAAkF;IAClF,uFAAuF;IACvF,MAAM,UAAU,GAAG,KAAK,IAAI,WAAW,CAAC;IAExC,OAAO,CAAC,KAA2B,EAAE,OAA8C,EAAE,EAAE,CACtF,IAAI,UAAU,CAAC,OAAO,EAAE,OAAO,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;AACnE,CAAC"}
|