@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.
Files changed (80) hide show
  1. package/README.md +190 -68
  2. package/dist/capabilities.d.ts +93 -0
  3. package/dist/capabilities.d.ts.map +1 -0
  4. package/dist/capabilities.js +4 -0
  5. package/dist/capabilities.js.map +1 -0
  6. package/dist/define-player.d.ts +7 -1
  7. package/dist/define-player.d.ts.map +1 -1
  8. package/dist/define-player.js +9 -60
  9. package/dist/define-player.js.map +1 -1
  10. package/dist/errors.d.ts +64 -23
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/errors.js +93 -31
  13. package/dist/errors.js.map +1 -1
  14. package/dist/index.d.ts +4 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +11 -6
  17. package/dist/index.js.map +1 -1
  18. package/dist/player-actor.d.ts +73 -137
  19. package/dist/player-actor.d.ts.map +1 -1
  20. package/dist/player-actor.js +108 -254
  21. package/dist/player-actor.js.map +1 -1
  22. package/dist/routing/build-url.d.ts +8 -1
  23. package/dist/routing/build-url.d.ts.map +1 -1
  24. package/dist/routing/build-url.js +34 -54
  25. package/dist/routing/build-url.js.map +1 -1
  26. package/dist/routing/derive-current-route.d.ts +12 -38
  27. package/dist/routing/derive-current-route.d.ts.map +1 -1
  28. package/dist/routing/derive-current-route.js +24 -82
  29. package/dist/routing/derive-current-route.js.map +1 -1
  30. package/dist/routing/derive-initial-route.d.ts +2 -1
  31. package/dist/routing/derive-initial-route.d.ts.map +1 -1
  32. package/dist/routing/derive-initial-route.js +13 -2
  33. package/dist/routing/derive-initial-route.js.map +1 -1
  34. package/dist/routing/derive-route.d.ts +81 -2
  35. package/dist/routing/derive-route.d.ts.map +1 -1
  36. package/dist/routing/derive-route.js +97 -3
  37. package/dist/routing/derive-route.js.map +1 -1
  38. package/dist/routing/format-play-route-transitions.d.ts +8 -2
  39. package/dist/routing/format-play-route-transitions.d.ts.map +1 -1
  40. package/dist/routing/format-play-route-transitions.js +175 -43
  41. package/dist/routing/format-play-route-transitions.js.map +1 -1
  42. package/dist/routing/index.d.ts +1 -1
  43. package/dist/routing/index.d.ts.map +1 -1
  44. package/dist/routing/types.d.ts +9 -5
  45. package/dist/routing/types.d.ts.map +1 -1
  46. package/dist/state-meta.d.ts +52 -0
  47. package/dist/state-meta.d.ts.map +1 -0
  48. package/dist/state-meta.js +77 -0
  49. package/dist/state-meta.js.map +1 -0
  50. package/dist/types.d.ts +20 -5
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/view/derive-current-view.d.ts +7 -6
  53. package/dist/view/derive-current-view.d.ts.map +1 -1
  54. package/dist/view/derive-current-view.js +7 -6
  55. package/dist/view/derive-current-view.js.map +1 -1
  56. package/dist/with-routing.d.ts +45 -0
  57. package/dist/with-routing.d.ts.map +1 -0
  58. package/dist/with-routing.js +140 -0
  59. package/dist/with-routing.js.map +1 -0
  60. package/dist/with-view.d.ts +57 -0
  61. package/dist/with-view.d.ts.map +1 -0
  62. package/dist/with-view.js +158 -0
  63. package/dist/with-view.js.map +1 -0
  64. package/package.json +40 -18
  65. package/dist/guards/compose.d.ts +0 -158
  66. package/dist/guards/compose.d.ts.map +0 -1
  67. package/dist/guards/compose.js +0 -188
  68. package/dist/guards/compose.js.map +0 -1
  69. package/dist/guards/helpers.d.ts +0 -62
  70. package/dist/guards/helpers.d.ts.map +0 -1
  71. package/dist/guards/helpers.js +0 -85
  72. package/dist/guards/helpers.js.map +0 -1
  73. package/dist/guards/index.d.ts +0 -20
  74. package/dist/guards/index.d.ts.map +0 -1
  75. package/dist/guards/index.js +0 -18
  76. package/dist/guards/index.js.map +0 -1
  77. package/dist/guards/types.d.ts +0 -22
  78. package/dist/guards/types.d.ts.map +0 -1
  79. package/dist/guards/types.js +0 -2
  80. 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 signal-driven reactivity and a router integration.
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
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Version](https://img.shields.io/badge/version-3.0.0-blue)](https://www.npmjs.com/package/@xmachines/play-xstate)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Version](https://img.shields.io/badge/version-5.0.0-blue)](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`. Vite 8 resolves its default
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
- `xstate ^5.31.0` is a peer dependency. Install it with this package.
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 TC39 Signal-based reactive state
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 also exposes reactive TC39 Signals. It implements both the `Routable` interface and the `Viewable` interface from `@xmachines/play-actor`.
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
- #### Signals
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
- | Signal | Type | Description |
141
- | -------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
142
- | `state` | `Signal.State<SnapshotFrom<TMachine>>` | The current XState snapshot. The actor updates it on every active transition |
143
- | `currentRoute` | `Signal.Computed<string \| null>` | The URL that comes from the `meta.route` template of the active state and from the context |
144
- | `currentView` | `Signal.State<PlaySpec \| null>` | The view spec from the `meta.view` metadata of the active state, with the context params added |
145
- | `initialRoute` | `readonly string \| null` | The route of the initial state of the machine. The constructor fixes it, and a router bridge uses it to detect a deep link or a restore |
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
- #### Signal usage example
246
+ #### Atom usage example
160
247
 
161
248
  ```typescript
162
- import { Signal } from "@xmachines/play-signals";
249
+ import { watchAtom } from "@xmachines/play-atom";
163
250
 
164
- const watcher = new Signal.subtle.Watcher(() => {
165
- queueMicrotask(() => {
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
- ### Guard utilities
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
- These composable guard helpers wrap the `and()`, `or()`, and `not()` functions of XState. Use them in a machine `setup({ guards })` definition.
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 }) => !!context.userId,
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: composeGuards(["isLoggedIn", "hasAdminRole"]),
278
+ guard: and(["isLoggedIn", "hasAdminRole"]),
205
279
  target: "adminPanel",
206
280
  },
207
281
  accessLogin: {
208
- guard: negateGuard("isLoggedIn"),
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
- Guard, // deprecated with the guard utilities — removed in the next major
313
- GuardArray, // deprecated with the guard utilities — removed in the next major
314
- ComposedGuard, // deprecated with the guard utilities — removed in the next major
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: string }
396
+ RouteObject, // Route metadata object shape: { path, reenter?, handler?, data? }
319
397
  RouteMetadata, // Union: string | RouteObject
320
- } from "@xmachines/play-xstate";
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
- MissingQueryContextError, // deprecated: no longer thrown
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
- EmptyGuardArrayError, // composeGuards/composeGuardsOr called with empty array
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`. Each class also carries typed detail fields, such as `param`, `template`, and `combinator`. Your code therefore reads the details of an error, and it does not parse the message.
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,4 @@
1
+ export function compose(Base, ...capabilities) {
2
+ return capabilities.reduce((actor, capability) => capability(actor), Base);
3
+ }
4
+ //# sourceMappingURL=capabilities.js.map
@@ -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"}
@@ -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 const definePlayer: <TMachine extends AnyStateMachine>(config: PlayerConfig<TMachine>) => PlayerFactory<TMachine>;
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;AAG1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,SAAS,eAAe,EAC5D,QAAQ,YAAY,CAAC,QAAQ,CAAC,KAC5B,aAAa,CAAC,QAAQ,CAKxB,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"}
@@ -1,62 +1,11 @@
1
1
  import { PlayerActor } from "./player-actor.js";
2
- /**
3
- * Creates a player factory from an XState machine
4
- *
5
- * This factory function accepts an XState v5 machine. It returns a function that
6
- * makes {@link PlayerActor} instances. One configuration can therefore make more
7
- * than one actor instance, and this helps with a test, with an application of
8
- * several instances, and with a render on the server.
9
- *
10
- * @typeParam TMachine - The type of the XState v5 state machine
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":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAC3B,MAA8B,EACJ,EAAE;IAC5B,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IAEpC,OAAO,CAAC,KAA2B,EAAE,OAA8C,EAAE,EAAE,CACtF,IAAI,WAAW,CAAC,OAAO,EAAE,OAAO,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;AACpE,CAAC,CAAC"}
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"}