@xmachines/play-xstate 2.0.0 → 2.1.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 (56) hide show
  1. package/README.md +66 -65
  2. package/dist/define-player.d.ts +16 -16
  3. package/dist/define-player.js +16 -16
  4. package/dist/errors.d.ts +57 -50
  5. package/dist/errors.d.ts.map +1 -1
  6. package/dist/errors.js +63 -55
  7. package/dist/errors.js.map +1 -1
  8. package/dist/guards/compose.d.ts +53 -50
  9. package/dist/guards/compose.d.ts.map +1 -1
  10. package/dist/guards/compose.js +67 -63
  11. package/dist/guards/compose.js.map +1 -1
  12. package/dist/guards/helpers.d.ts +22 -22
  13. package/dist/guards/helpers.js +23 -23
  14. package/dist/guards/index.d.ts +9 -9
  15. package/dist/guards/index.js +9 -9
  16. package/dist/guards/types.d.ts +9 -8
  17. package/dist/guards/types.d.ts.map +1 -1
  18. package/dist/index.d.ts +6 -5
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +8 -7
  21. package/dist/index.js.map +1 -1
  22. package/dist/player-actor.d.ts +162 -146
  23. package/dist/player-actor.d.ts.map +1 -1
  24. package/dist/player-actor.js +269 -241
  25. package/dist/player-actor.js.map +1 -1
  26. package/dist/routing/build-url.d.ts +19 -16
  27. package/dist/routing/build-url.d.ts.map +1 -1
  28. package/dist/routing/build-url.js +62 -59
  29. package/dist/routing/build-url.js.map +1 -1
  30. package/dist/routing/derive-current-route.d.ts +42 -36
  31. package/dist/routing/derive-current-route.d.ts.map +1 -1
  32. package/dist/routing/derive-current-route.js +57 -49
  33. package/dist/routing/derive-current-route.js.map +1 -1
  34. package/dist/routing/derive-initial-route.d.ts +23 -20
  35. package/dist/routing/derive-initial-route.d.ts.map +1 -1
  36. package/dist/routing/derive-initial-route.js +27 -24
  37. package/dist/routing/derive-initial-route.js.map +1 -1
  38. package/dist/routing/derive-route.d.ts +38 -37
  39. package/dist/routing/derive-route.d.ts.map +1 -1
  40. package/dist/routing/derive-route.js +45 -42
  41. package/dist/routing/derive-route.js.map +1 -1
  42. package/dist/routing/format-play-route-transitions.d.ts +34 -28
  43. package/dist/routing/format-play-route-transitions.d.ts.map +1 -1
  44. package/dist/routing/format-play-route-transitions.js +32 -28
  45. package/dist/routing/format-play-route-transitions.js.map +1 -1
  46. package/dist/routing/index.d.ts +3 -3
  47. package/dist/routing/index.js +3 -3
  48. package/dist/routing/types.d.ts +12 -11
  49. package/dist/routing/types.d.ts.map +1 -1
  50. package/dist/types.d.ts +64 -60
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/view/derive-current-view.d.ts +42 -40
  53. package/dist/view/derive-current-view.d.ts.map +1 -1
  54. package/dist/view/derive-current-view.js +51 -47
  55. package/dist/view/derive-current-view.js.map +1 -1
  56. package/package.json +7 -6
@@ -5,11 +5,13 @@ import { ActorThrewNonErrorError, InvalidEventError, InvalidMachineError } from
5
5
  import { deriveCurrentRoute, deriveInitialRoute } from "./routing/index.js";
6
6
  import { deriveCurrentView } from "./view/derive-current-view.js";
7
7
  /**
8
- * An `Error` by identity or by brand: `instanceof` misses errors constructed
9
- * in another realm (an iframe, `node:vm`). Prefer `Error.isError` where the
10
- * runtime has it (Node >= 24, Baseline-2025 browsers) — it rejects
11
- * `Symbol.toStringTag` spoofs and fall back to the brand check elsewhere.
12
- * Typed structurally: the repo's lib target predates the API.
8
+ * Tells you if a value is an `Error`, by its identity or by its brand: `instanceof`
9
+ * misses an error from another realm, such as an iframe or `node:vm`. The function
10
+ * prefers `Error.isError` where the runtime has it (Node >= 24, and a
11
+ * Baseline-2025 browser), because that function refuses a false
12
+ * `Symbol.toStringTag`. In every other runtime the function tests the brand. The
13
+ * type is structural, because the lib target of this repository is older than the
14
+ * API.
13
15
  */
14
16
  const isRealError = (value) => {
15
17
  if (value instanceof Error)
@@ -20,12 +22,13 @@ const isRealError = (value) => {
20
22
  return Object.prototype.toString.call(value) === "[object Error]";
21
23
  };
22
24
  /**
23
- * Normalize an actor failure for `onError`.
25
+ * Normalizes a failure of the actor for `onError`.
24
26
  *
25
- * An `Error` is handed over unchanged so the machine's own error keeps its
26
- * identity (a consumer's `instanceof` check still works, and the no-`onError`
27
- * path rethrows that same object). Anything else is ours to construct, so it
28
- * becomes a coded `PlayError` carrying the thrown value as `cause`.
27
+ * The function gives an `Error` to the handler without a change. The error of the
28
+ * machine therefore keeps its identity: an `instanceof` test of a consumer still
29
+ * works, and the path without an `onError` throws that same object again. Every
30
+ * other value is ours to build, and it becomes a `PlayError` with a code. That
31
+ * error carries the value from the throw as its `cause`.
29
32
  */
30
33
  const toError = (value) => {
31
34
  try {
@@ -34,20 +37,22 @@ const toError = (value) => {
34
37
  }
35
38
  }
36
39
  catch {
37
- // Classification itself can throw for hostile values (a revoked Proxy
38
- // traps both instanceof and brand inspection) fall through and wrap.
40
+ // The classification itself can throw for a hostile value, because a revoked
41
+ // Proxy traps instanceof and also the inspection of the brand. Continue, and wrap
42
+ // the value.
39
43
  }
40
44
  return new ActorThrewNonErrorError(value);
41
45
  };
42
46
  /**
43
- * Bounded structural equality for derived view specs.
47
+ * The structural equality of two derived view specs, with a limit on its depth.
44
48
  *
45
- * Walks exactly the shape `deriveCurrentView` builds spec fields, then
46
- * `elements`, then each element's `props` comparing every leaf with
47
- * `Object.is`. Prop VALUES are never entered: a changed reference re-emits
48
- * even when contents are equal, which keeps container props (Map/Set/class
49
- * instances, invisible to structural comparison) and cyclic values (unbounded
50
- * recursion for it) correct by construction.
49
+ * The function walks exactly the shape that `deriveCurrentView` builds: the spec
50
+ * fields, then `elements`, then the `props` object of each element. It compares
51
+ * each leaf with `Object.is`. It never enters the VALUE of a prop: a new reference
52
+ * therefore emits the view again, also when the contents are equal. This design
53
+ * keeps two things correct: a prop of a container (a Map, a Set, or an instance of a
54
+ * class, which a structural comparison cannot see), and a cyclic value, which gives
55
+ * a structural comparison a recursion without an end.
51
56
  */
52
57
  const viewSpecsEquivalent = (a, b) => {
53
58
  if (a === b)
@@ -58,8 +63,9 @@ const viewSpecsEquivalent = (a, b) => {
58
63
  return false;
59
64
  const aElements = a.elements ?? {};
60
65
  const bElements = b.elements ?? {};
61
- // Derived specs spread the same static meta.view, so elements is usually
62
- // reference-identical skip the per-element walk when it is.
66
+ // A derived spec spreads the same static meta.view. Therefore the elements
67
+ // usually have the same reference, and the walk over each element is then not
68
+ // necessary.
63
69
  if (aElements === bElements)
64
70
  return true;
65
71
  const elementKeys = Object.keys(aElements);
@@ -80,35 +86,39 @@ const viewSpecsEquivalent = (a, b) => {
80
86
  return true;
81
87
  };
82
88
  /**
83
- * A snapshot worth propagating to signals: an active snapshot or a "done"
84
- * snapshot (top-level final state reached). Error and stopped snapshots are
85
- * skipped so signals keep the last observable state instead of surfacing
86
- * teardown/error artifacts.
89
+ * Tells you if a snapshot is worth a propagation to the signals: an active
90
+ * snapshot, or a "done" snapshot, which means that the machine reached a final state
91
+ * at the top level. The code skips an error snapshot and a stopped snapshot.
92
+ * Therefore the signals keep the last observable state, and they show no artifact of
93
+ * a teardown or of an error.
87
94
  */
88
95
  const isObservableSnapshot = (snapshot) => snapshot.status === "active" || snapshot.status === "done";
89
96
  /**
90
- * Concrete XState actor implementing Play Architecture signal protocol
97
+ * The concrete XState actor. It implements the signal protocol of the Play Architecture
91
98
  *
92
- * Extends {@link @xmachines/play-actor!AbstractActor} and so XState's own `Actor` —
93
- * to provide XState v5 integration while maintaining ecosystem compatibility (XState
94
- * inspection, devtools). The machine is handed to the base constructor, so a
95
- * `PlayerActor` **is** the XState actor rather than a wrapper around one: everything
96
- * XState's `Actor` exposes operates on this instance's own state, and the class adds
97
- * TC39 Signal-based reactive state for Infrastructure observation on top.
99
+ * The class extends {@link @xmachines/play-actor!AbstractActor}, and therefore the
100
+ * `Actor` class of XState. It gives you the XState v5 integration, and it keeps the
101
+ * compatibility with the ecosystem, such as the XState inspection and the devtools.
102
+ * The constructor of the base class receives the machine. Therefore a `PlayerActor`
103
+ * **is** the XState actor, and it is no wrapper around one: every member of the
104
+ * XState `Actor` class works on the state of this instance, and this class adds the
105
+ * reactive state on the TC39 Signals for the observation by the infrastructure.
98
106
  *
99
- * **Capabilities:** Implements both {@link @xmachines/play-actor!Routable} and
100
- * {@link @xmachines/play-actor!Viewable} interfaces, providing routing and view
101
- * rendering support.
107
+ * **Capabilities:** the class implements both the
108
+ * {@link @xmachines/play-actor!Routable} interface and the
109
+ * {@link @xmachines/play-actor!Viewable} interface. It therefore supports the
110
+ * routing and the view rendering.
102
111
  *
103
- * **Architectural Context:** Implements **Actor Authority (INV-01)** by ensuring the
104
- * XState machine's guards control all navigation decisions. Infrastructure observes
105
- * the actor's signals (`state`, `currentRoute`, `currentView`) but cannot directly
106
- * manipulate state—all mutations flow through the state machine's event handlers.
112
+ * **Architectural context:** the class implements **Actor Authority (INV-01)**,
113
+ * because the guards of the XState machine control every decision of the
114
+ * navigation. The infrastructure observes the signals of the actor (`state`,
115
+ * `currentRoute`, and `currentView`), but it changes no state directly: every
116
+ * change goes through the event handlers of the state machine.
107
117
  *
108
- * @typeParam TMachine - XState v5 state machine type
118
+ * @typeParam TMachine - The type of the XState v5 state machine
109
119
  *
110
120
  * @example
111
- * Basic actor creation and lifecycle
121
+ * The creation of an actor, and its lifecycle
112
122
  * ```typescript
113
123
  * import { setup } from "xstate";
114
124
  * import { definePlayer } from "@xmachines/play-xstate";
@@ -119,7 +129,7 @@ const isObservableSnapshot = (snapshot) => snapshot.status === "active" || snaps
119
129
  * idle: {
120
130
  * meta: {
121
131
  * route: '/',
122
- * // A view spec needs `root` and `elements` other shapes derive null.
132
+ * // A view spec needs `root` and `elements`. Every other shape derives null.
123
133
  * view: {
124
134
  * root: 'home',
125
135
  * elements: { home: { type: 'HomePage', props: {}, children: [] } },
@@ -133,13 +143,13 @@ const isObservableSnapshot = (snapshot) => snapshot.status === "active" || snaps
133
143
  * const actor = createPlayer();
134
144
  * actor.start();
135
145
  *
136
- * // Observe signals
146
+ * // Observe the signals
137
147
  * console.log(actor.currentRoute.get()); // '/'
138
148
  * console.log(actor.currentView.get()?.root); // 'home'
139
149
  * ```
140
150
  *
141
151
  * @example
142
- * Signal lifecycle with watchers
152
+ * The signal lifecycle with a watcher
143
153
  * ```typescript
144
154
  * import { Signal } from "@xmachines/play-signals";
145
155
  *
@@ -152,61 +162,63 @@ const isObservableSnapshot = (snapshot) => snapshot.status === "active" || snaps
152
162
  *
153
163
  * watcher.watch(actor.state);
154
164
  * actor.send({ type: 'play.route', to: '#about' });
155
- * // Watcher notification scheduled via microtask by the watcher itself
165
+ * // The watcher schedules its own notification in a microtask
156
166
  * ```
157
167
  *
158
168
  * @see [Play RFC](../../docs/rfc/play.md)
159
- * @see {@link definePlayer} for factory creation
160
- * @see {@link @xmachines/play-actor!AbstractActor} for signal protocol
161
- * @see {@link @xmachines/play-actor!Routable} for routing capability
162
- * @see {@link @xmachines/play-actor!Viewable} for view rendering capability
169
+ * @see {@link definePlayer} for the creation through a factory
170
+ * @see {@link @xmachines/play-actor!AbstractActor} for the signal protocol
171
+ * @see {@link @xmachines/play-actor!Routable} for the routing capability
172
+ * @see {@link @xmachines/play-actor!Viewable} for the view rendering capability
163
173
  *
164
174
  * @remarks
165
- * **Routing:** This actor supports both XState's `route: {}` config pattern
166
- * and `play.route` events with parameters. The `deriveRoute()` function checks
167
- * `meta.route` (Stately pattern) for URL templates with parameter substitution support.
175
+ * **The routing:** this actor supports the `route: {}` config pattern of XState and
176
+ * also a `play.route` event with parameters. The `deriveRoute()` function reads
177
+ * `meta.route`, which is the Stately pattern, for a URL template, and it substitutes
178
+ * each parameter.
168
179
  *
169
- * **View Signal Pattern:** The `currentView` signal is a direct `Signal.State` (not
170
- * `Signal.Computed`) to ensure proper watcher propagation in PlayRenderer. Views are
171
- * cached and updated at state entry, not computed on every read.
180
+ * **The pattern of the view signal:** the `currentView` signal is a direct
181
+ * `Signal.State`, and not a `Signal.Computed`. The propagation to a watcher in
182
+ * PlayRenderer is therefore correct. The class derives each view at the entry of a
183
+ * state and keeps it, and it computes no view on a read.
172
184
  */
173
185
  export class PlayerActor extends AbstractActor {
174
186
  playerOptions;
175
187
  /**
176
- * The caller's live options bag, or an empty one during construction.
188
+ * The live options object of the caller, or an empty object during the construction.
177
189
  *
178
- * XState hands this actor to a context factory as `self` and to an
179
- * `inspect` observer as `actorRef` from inside its own constructor, before
180
- * any field here is assigned. Reading hooks through this accessor keeps
181
- * that window from throwing a throw would be caught by XState's
182
- * initialization and parked as an error snapshot while preserving the
183
- * read-at-delivery-time behaviour the bag's own docs promise.
190
+ * XState gives this actor to a context factory as `self`, and to an `inspect`
191
+ * observer as `actorRef`, from inside its own constructor, before the code assigns
192
+ * any field here. A read of a hook through this accessor therefore does not throw
193
+ * in that window. A throw goes to the initialization of XState, which parks it as
194
+ * an error snapshot. The accessor also keeps the behavior that the documentation of
195
+ * the object promises: a read at the moment of the delivery.
184
196
  *
185
- * The window-sensitive fields below are `declare`d for the same reason.
186
- * This target compiles class fields to `Object.defineProperty`, which runs
187
- * after `super()` returns: a plain declaration initializer or not — would
188
- * reset anything the window had written back to `undefined`. `declare`
189
- * emits nothing, so those writes survive.
197
+ * The fields below that this window touches have a `declare` modifier for the same
198
+ * reason. This target compiles a class field into `Object.defineProperty`, which
199
+ * runs after `super()` returns. A plain declaration, with an initializer or without
200
+ * one, therefore resets each value of the window to `undefined`. A `declare`
201
+ * modifier emits nothing, and those values survive.
190
202
  */
191
203
  get hooks() {
192
204
  return this.playerOptions ?? {};
193
205
  }
194
206
  /**
195
- * The last snapshot the view pipeline processed. XState notifies observers
196
- * on EVERY processed event an ignored event redelivers the identical
197
- * snapshot and deriveCurrentView is pure in the snapshot, so an identical
198
- * reference cannot change the outcome. Seeded with undefined (never a
199
- * snapshot): the construction-time snapshot is reference-identical to the
200
- * one start() replays, and seeding with it would suppress the initial view.
207
+ * The last snapshot of the view pipeline. XState notifies each observer on EVERY
208
+ * event that it processes, and an event that it ignores delivers the identical
209
+ * snapshot again. deriveCurrentView is pure in the snapshot. Therefore an identical
210
+ * reference can change no result. The first value is undefined, and never a
211
+ * snapshot: the snapshot of the construction has the same reference as the snapshot
212
+ * that start() replays, and that value therefore stops the first view.
201
213
  */
202
214
  lastViewSnapshot = undefined;
203
- // AbstractActor protocol requirements
215
+ // The requirements of the AbstractActor protocol
204
216
  state;
205
217
  /**
206
- * Returns whether the actor's current state can accept the given event.
218
+ * Tells you if the current state of the actor accepts the given event.
207
219
  *
208
- * Typed to the machine's event union passing an unknown event type is a
209
- * compile error. Evaluated against the current snapshot signal.
220
+ * The type is the event union of the machine. An unknown event type is therefore a
221
+ * compile error. The method evaluates the event against the snapshot signal.
210
222
  *
211
223
  * @example
212
224
  * ```typescript
@@ -214,186 +226,195 @@ export class PlayerActor extends AbstractActor {
214
226
  * ```
215
227
  */
216
228
  can(event) {
217
- // Reading the signal keeps can() reactive: a Signal.Computed over it
218
- // recomputes on transitions. Two states have no answer to give: the
219
- // construction window, where no snapshot is readable yet, and an actor
220
- // whose initialization failed XState parks an error snapshot there,
221
- // which is a truthy object with no `can` on it.
229
+ // A read of the signal keeps can() reactive: a Signal.Computed over the signal
230
+ // computes its value again on each transition. Two states have no answer: the
231
+ // construction window, where the code can read no snapshot, and an actor with a
232
+ // failed initialization, where XState parks an error snapshot. That snapshot is
233
+ // a truthy object, and it has no `can` method.
222
234
  const snapshot = this.state?.get();
223
235
  return typeof snapshot?.can === "function" ? snapshot.can(event) : false;
224
236
  }
225
237
  /**
226
- * A TC39 `Signal.Computed` that derives the current URL path from the active
227
- * machine state's `meta.route` template and the actor's context.
238
+ * A TC39 `Signal.Computed`. It derives the current URL path from the `meta.route`
239
+ * template of the active machine state and from the context of the actor.
228
240
  *
229
- * Returns `null` when the current state has no `meta.route`, or when the route
230
- * template cannot be fully resolved a required `:param` absent from context
231
- * is caught internally (`MissingRouteParamError` never escapes `get()`): the
232
- * condition is transient mid-transition and the signal recomputes on the next
241
+ * It returns `null` when the current state has no `meta.route` field, and also when
242
+ * it cannot resolve the complete route template. A necessary `:param` that the
243
+ * context does not hold is caught inside the signal, and a
244
+ * `MissingRouteParamError` therefore never leaves `get()`: that condition is
245
+ * temporary during a transition, and the signal computes the value again on the next
233
246
  * snapshot.
234
247
  *
235
248
  * @example
236
249
  * ```typescript
237
- * // Returns "/profile/alice" when context.params.userId === "alice",
238
- * // and null while the param is still missing.
250
+ * // It returns "/profile/alice" when context.params.userId === "alice",
251
+ * // and null while the param is still absent.
239
252
  * const route = actor.currentRoute.get();
240
253
  * ```
241
254
  */
242
255
  currentRoute;
243
256
  /**
244
- * The route derived from the machine's initial state fixed at construction,
245
- * never changes even when the actor is restored from a snapshot.
257
+ * The route of the initial state of the machine. The constructor fixes it, and it
258
+ * never changes, also when the code restores the actor from a snapshot.
246
259
  *
247
- * Router bridges compare this against the browser URL to distinguish a deep-link
248
- * (non-initial URL → router wins) from a restore (initial URL + actor at a
249
- * different restored route → actor wins).
260
+ * A router bridge compares it with the browser URL, and it therefore separates a
261
+ * deep link (a URL that is not the initial one the router wins) from a restore
262
+ * (the initial URL, and the actor at a different route from the restore the actor
263
+ * wins).
250
264
  *
251
- * Derived statically from the machine definition via `deriveInitialRoute`
252
- * (XState's pure `initialTransition` helper): the initial state chain and its
253
- * `meta.route` templates are fixed at machine definition time, while `:param`
254
- * substitution uses the machine's real initial context for this actor's `input`.
255
- * No extra actor is ever created, and a restored snapshot never influences the
256
- * value it is always the machine's **default** initial route.
265
+ * `deriveInitialRoute` derives the value statically from the machine definition,
266
+ * with the pure `initialTransition` helper of XState: the chain of the initial states
267
+ * and their `meta.route` templates are fixed at the moment of the machine
268
+ * definition, and the substitution of a `:param` uses the real initial context of
269
+ * the machine for the `input` of this actor. The code makes no second actor, and a
270
+ * snapshot of a restore changes the value never: it is always the **default**
271
+ * initial route of the machine.
257
272
  */
258
273
  initialRoute;
259
274
  /**
260
- * Reactive signal containing the current view spec derived from the active state's
261
- * `meta.view` metadata.
275
+ * The reactive signal of the current view spec. The signal derives the spec from
276
+ * the `meta.view` metadata of the active state.
262
277
  *
263
- * Emits a **fresh object reference** whenever the rendered view actually changes —
264
- * a different state's view, or a param/context change that alters the resolved
265
- * spec (including `reenter: true` re-entries with new params). Snapshots that do
266
- * not change the rendered view (e.g. context-only assigns) keep the previous
267
- * reference so downstream providers do not remount the UI on every event.
278
+ * It emits a **new object reference** on each real change of the view on the
279
+ * screen: the view of a different state, or a change of a param or of the context
280
+ * that changes the resolved spec. A re-entry with `reenter: true` and new params
281
+ * also changes the spec. A snapshot that changes no view on the screen, such as an
282
+ * assign of the context alone, keeps the previous reference. A provider below the
283
+ * signal therefore mounts the UI again not on every event.
268
284
  *
269
- * The emitted `PlaySpec` carries the machine's context in its composed
270
- * `state` under the read-only `/context` subtree, so specs read context
271
- * URL params included through the ordinary state grammar
272
- * (`{ $state: "/context/params/section" }`). See `@xmachines/play-actor`'s
273
- * context-projection module for the full contract.
285
+ * The `PlaySpec` of the emission carries the context of the machine in its composed
286
+ * `state` field, under the read-only `/context` subtree. A spec therefore reads the
287
+ * context, and also each URL param, through the ordinary state grammar
288
+ * (`{ $state: "/context/params/section" }`). The context-projection module of
289
+ * `@xmachines/play-actor` holds the complete contract.
274
290
  *
275
- * Returns `null` when the current state has no `meta.view` metadata.
291
+ * The signal returns `null` when the current state has no `meta.view` metadata.
276
292
  *
277
- * Two states declaring separate but structurally identical `meta.view`
278
- * literals emit distinct references on a transition between them (a
279
- * provider remount); hoist the shared literal into one `typedSpec` constant
280
- * to deduplicate by identity.
293
+ * Two states can declare two separate `meta.view` literals with an identical
294
+ * structure. A transition between those two states then emits two different
295
+ * references, and a provider mounts the UI again. Move the shared literal into one
296
+ * `typedSpec` constant, and the identity then removes the duplicate.
281
297
  *
282
298
  * @example
283
299
  * ```typescript
284
300
  * const view = actor.currentView.get();
285
301
  * if (view) {
286
- * console.log(view.root); // e.g. "root"
287
- * console.log(view.elements); // @xmachines/json-render-core Spec elements
302
+ * console.log(view.root); // for example "root"
303
+ * console.log(view.elements); // the Spec elements of @xmachines/json-render-core
288
304
  * }
289
305
  * ```
290
306
  */
291
307
  currentView = new Signal.State(null);
292
308
  constructor(machine, options, input, restoredSnapshot) {
293
- // Defensive check before super(): a non-object machine fails deep inside
294
- // XState's constructor with an opaque TypeError instead of a coded error.
309
+ // A defensive check before super(): a machine that is not an object fails deep
310
+ // inside the constructor of XState, with an opaque TypeError, and not with a coded
311
+ // error.
295
312
  if (!machine || typeof machine !== "object") {
296
313
  throw new InvalidMachineError();
297
314
  }
298
- // THIS is the actor. The machine and its runtime options go straight to
299
- // XState's Actor constructor the same arguments `createActor` forwards
300
- // so there is no second instance to answer for the real one, and every
301
- // Actor member we do not override operates on real state.
315
+ // THIS is the actor. The machine and its runtime options go directly to the Actor
316
+ // constructor of XState, which receives the same arguments as `createActor`
317
+ // forwards. Therefore no second instance answers for the real one, and every
318
+ // Actor member without an override here works on the real state.
302
319
  //
303
- // XState 5.28.0: the options bag has a conditional type constraint on
304
- // `input` that TypeScript cannot resolve against an unbound generic
305
- // TMachine. The cast stays inside the XState type system, and `input` is
306
- // typed on this constructor's own signature, so callers keep their
307
- // compile-time validation.
308
- // Track XState typing improvements: https://github.com/statelyai/xstate/issues
320
+ // XState 5.28.0: the options object has a conditional type constraint on `input`,
321
+ // and TypeScript cannot resolve that constraint against an unbound generic
322
+ // TMachine. The cast stays inside the type system of XState, and the signature of
323
+ // this constructor gives `input` its type. Therefore each caller keeps the check
324
+ // at the compile time.
325
+ // Follow the improvements of the XState types: https://github.com/statelyai/xstate/issues
309
326
  super(machine, {
310
327
  input,
311
328
  snapshot: restoredSnapshot,
312
329
  inspect: options?.inspect,
313
330
  });
314
- // Derive the machine's initial route for restore-vs-deeplink detection
315
- // always the machine's DEFAULT initial route, never the restored state's.
316
- // Without a restored snapshot this actor's own pre-start snapshot IS that
317
- // default initial state, so derive from it directly; only a restore needs
318
- // XState's pure `initialTransition` helper, whose inert actor scope runs
319
- // the machine's initial transition twice more (once with `input`
320
- // undefined) an XState quirk worth paying only when required.
331
+ // Derive the initial route of the machine, for the detection of a restore or a
332
+ // deep link. The value is always the DEFAULT initial route of the machine, and
333
+ // never the route of the restored state. Without a snapshot of a restore, the
334
+ // pre-start snapshot of this actor IS that default initial state, and the code
335
+ // derives the route from it directly. A restore alone needs the pure
336
+ // `initialTransition` helper of XState. The inert actor scope of that helper runs
337
+ // the initial transition of the machine two more times, and one of them has an
338
+ // undefined `input`. This is a quirk of XState, and it costs too much for each
339
+ // other case.
321
340
  this.initialRoute =
322
341
  restoredSnapshot === undefined
323
342
  ? deriveCurrentRoute(this.getSnapshot())
324
343
  : deriveInitialRoute(machine, input);
325
344
  this.playerOptions = options || {};
326
- // Initialize state signal. Updates are synchronous (no microtask batching):
327
- // XState already coalesces multiple transitions within a single send() into
328
- // one subscription callback, and synchronous updates ensure guard-triggered
329
- // redirects are immediately visible to router bridges.
345
+ // Initialize the state signal. Each update is synchronous, with no batching in a
346
+ // microtask: XState groups the transitions of one send() call into one
347
+ // subscription callback already, and a synchronous update shows each guard
348
+ // redirect to a router bridge at once.
330
349
  this.state = new Signal.State(this.getSnapshot());
331
- // Initialize currentRoute computed signal
350
+ // Initialize the currentRoute computed signal
332
351
  this.currentRoute = new Signal.Computed(() => {
333
352
  const snapshot = this.state.get();
334
353
  return deriveCurrentRoute(snapshot);
335
354
  });
336
- // Observe our own transitions. `super` rather than `this` so the
337
- // next-only bookkeeping in the subscribe override stays about userland
338
- // subscriptions only.
355
+ // Observe the transitions of this actor. The code uses `super`, and not `this`, so
356
+ // that the bookkeeping of the next-only subscriptions in the subscribe override
357
+ // stays about the subscriptions of the user code.
339
358
  super.subscribe({
340
359
  next: (snapshot) => {
341
- // Only update on stable states: active snapshots and the
342
- // "done" snapshot from a top-level final state. Error/stopped snapshots
343
- // are skipped so signals never freeze on teardown artifacts.
360
+ // Update on a stable state only: an active snapshot, and the "done" snapshot of a
361
+ // final state at the top level. The code skips an error snapshot and a stopped
362
+ // snapshot. Therefore a signal never freezes on an artifact of a teardown.
344
363
  if (isObservableSnapshot(snapshot)) {
345
- // State updates are synchronous so router bridges see guard redirects immediately.
364
+ // Each state update is synchronous. Therefore a router bridge sees a guard redirect at once.
346
365
  this.state.set(snapshot);
347
- // Validate and cache the view after state/currentRoute are current, before hooks.
348
- // Hook ordering is intentional:
349
- // 1. state/currentRoute updated
350
- // 2. currentView cached
351
- // 3. onStateChange hook
352
- // 4. send() then invokes onTransition
366
+ // Check the view and keep it after state and currentRoute hold their new values,
367
+ // and before the hooks.
368
+ // The order of the hooks is deliberate:
369
+ // 1. state and currentRoute receive their new values
370
+ // 2. currentView holds the new view
371
+ // 3. the onStateChange hook runs
372
+ // 4. send() then calls onTransition
353
373
  this.validateAndCacheView(snapshot);
354
- // Call onStateChange hook
374
+ // Call the onStateChange hook
355
375
  const onStateChange = this.hooks.onStateChange;
356
376
  if (onStateChange) {
357
377
  onStateChange(this, snapshot);
358
378
  }
359
379
  }
360
380
  },
361
- // Always registered, handler read at DELIVERY time: the options bag is
362
- // shared by reference, so an onError attached after construction still
363
- // receives actor errors, and one removed later stops swallowing them.
364
- // Without a handler the listener rethrows, which XState's observer
365
- // dispatch routes to its global unhandled rethrow the same loud
366
- // default as not registering an error listener at all.
381
+ // The code registers the listener always, and it reads the handler at the moment
382
+ // of the DELIVERY: the options object is shared by its reference. Therefore an
383
+ // onError handler on that object after the construction still receives each actor
384
+ // error, and the removal of a handler stops the silence again.
385
+ // Without a handler, the listener throws the error again. The observer dispatch of
386
+ // XState then sends it to its global unhandled rethrow, which is the same loud
387
+ // default as a listener that the code registers not.
367
388
  error: (error) => {
368
389
  const handler = this.hooks.onError;
369
390
  if (handler) {
370
391
  handler(this, toError(error));
371
392
  return;
372
393
  }
373
- // A userland next-only subscription already makes XState report
374
- // this delivery globally; rethrowing here too would double it.
394
+ // A next-only subscription of the user code makes XState report this delivery
395
+ // globally already. A second throw here reports it two times.
375
396
  if ((this.nextOnlySubscriptions ?? 0) > 0) {
376
397
  return;
377
398
  }
378
399
  throw error;
379
400
  },
380
401
  });
381
- // Everything above is reachable from XState's own constructor callbacks;
382
- // past this point the instance is whole.
402
+ // The callbacks of the constructor of XState can reach everything above. After this
403
+ // point the instance is complete.
383
404
  this.constructed = true;
384
405
  }
385
406
  /**
386
- * Start the actor.
407
+ * Starts the actor.
387
408
  *
388
- * Fires `onStart` on each real start every transition from not-running to
389
- * running, including a start after a stop, which XState allows (its own
390
- * `start()` bails only while the actor is already RUNNING). A repeated call
391
- * while running does not re-fire it, so a defensive double mount does not
392
- * re-run `onStart` side effects for one actual start.
409
+ * The method fires `onStart` on each real start, which is every transition from
410
+ * "not running" to "running". A start after a stop is such a transition, and XState
411
+ * permits it: its own `start()` stops only while the actor RUNS already. A second
412
+ * call while the actor runs fires no hook. Therefore a defensive double mount runs
413
+ * the side effects of `onStart` one time for one real start.
393
414
  */
394
415
  start() {
395
- // See stop(): a call reaching in through the construction window would
396
- // run a half-built actor.
416
+ // See stop(): a call that reaches in through the construction window runs an actor
417
+ // that is not complete.
397
418
  if (!this.constructed) {
398
419
  return this;
399
420
  }
@@ -408,21 +429,21 @@ export class PlayerActor extends AbstractActor {
408
429
  return this;
409
430
  }
410
431
  /**
411
- * Stop the actor and clean up.
432
+ * Stops the actor and cleans up.
412
433
  *
413
- * Fires `onStop` only when the actor was actually running mirroring
414
- * XState, where stopping a never-started or already-stopped actor is a
415
- * no-op with zero teardown so paired cleanup never runs twice, nor
416
- * against resources `onStart` never acquired. Stopping does not close the
417
- * actor for good: a later `start()` is a fresh lifecycle and fires
418
- * `onStart` again.
434
+ * The method fires `onStop` only when the actor ran. This matches XState, where a
435
+ * stop of an actor that never started, or of an actor that stopped already, does
436
+ * nothing and tears nothing down. Therefore the paired cleanup runs never two
437
+ * times, and it runs never against a resource that `onStart` did not take. A stop
438
+ * does not close the actor for ever: a later `start()` is a new lifecycle, and it
439
+ * fires `onStart` again.
419
440
  */
420
441
  stop() {
421
- // A call reaching in through the construction window (a context factory
422
- // stopping its own `self`) would mark the actor stopped before its
423
- // internal subscription is registered XState drops observers added to
424
- // a stopped actor, so the signals would never move again. There is
425
- // nothing to tear down mid-construction, so ignore it.
442
+ // A call that reaches in through the construction window, for example a context
443
+ // factory that stops its own `self`, marks the actor as stopped before the code
444
+ // registers its internal subscription. XState drops each observer of a stopped
445
+ // actor. The signals therefore move never again. There is nothing to tear down
446
+ // during the construction, and the code ignores such a call.
426
447
  if (!this.constructed) {
427
448
  return this;
428
449
  }
@@ -436,49 +457,52 @@ export class PlayerActor extends AbstractActor {
436
457
  return this;
437
458
  }
438
459
  /**
439
- * Send an event to this actor.
460
+ * Sends an event to this actor.
440
461
  *
441
- * The actor's state machine guards decide whether the event causes a transition.
442
- * Pass any event from the machine's event union domain events, routing events, etc.
462
+ * The guards of the state machine of the actor decide if the event causes a
463
+ * transition. Give any event of the event union of the machine: a domain event, a
464
+ * routing event, and so on.
443
465
  *
444
- * @param event - An event from the machine's `EventFromLogic<TMachine>` union.
466
+ * @param event - An event of the `EventFromLogic<TMachine>` union of the machine.
445
467
  *
446
- * @throws {InvalidEventError} When `event` is not a plain object (`null`, `undefined`,
447
- * a string, number, etc.). Import the class from `@xmachines/play-xstate/errors`.
468
+ * @throws {InvalidEventError} When `event` is not a plain object, for example
469
+ * `null`, `undefined`, a string, or a number. Import the class from
470
+ * `@xmachines/play-xstate/errors`.
448
471
  *
449
472
  * @example
450
473
  * ```typescript
451
- * // Domain event (typed to machine's event union)
474
+ * // A domain event, with the type of the event union of the machine
452
475
  * actor.send({ type: "auth.login", userId: "123" });
453
476
  *
454
- * // Routing event
477
+ * // A routing event
455
478
  * actor.send({ type: "play.route", to: "#home" });
456
479
  * ```
457
480
  */
458
481
  send(event) {
459
- // Defensive check: Validate event is not null/undefined
482
+ // A defensive check: the event must not be null and not undefined
460
483
  if (!event || typeof event !== "object") {
461
484
  throw new InvalidEventError(event);
462
485
  }
463
- // Inside the construction window there is no readable snapshot, and no
464
- // hook can have been registered yet: deliver and return.
486
+ // Inside the construction window there is no readable snapshot, and no hook can
487
+ // exist yet: deliver the event and return.
465
488
  if (!this.constructed) {
466
489
  Actor.prototype.send.call(this, event);
467
490
  return;
468
491
  }
469
- // Captured unconditionally getSnapshot() is a single property read, and
470
- // the options bag is live: an onTransition installed while this event is
471
- // being processed (e.g. from onStateChange) must still fire for it with
472
- // the correct pre-send snapshot.
492
+ // The code captures the snapshot always, because getSnapshot() is one property
493
+ // read, and because the options object is live: an onTransition handler that
494
+ // arrives during the processing of this event, for example from onStateChange,
495
+ // must still run for it, with the correct snapshot from before the send.
473
496
  const prevSnapshot = this.getSnapshot();
474
- // Send to XState actor
475
- // `AbstractActor` re-declares send() as abstract purely to narrow the
476
- // event type, and TypeScript forbids super calls to an abstract member,
477
- // so reach XState's implementation directly. `this` IS the actor, so this
478
- // is exactly the call `super.send(event)` would make: the relay that
479
- // emits the @xstate.event inspection event and enqueues on our mailbox.
497
+ // Send the event to the XState actor.
498
+ // `AbstractActor` declares send() as abstract for one reason only, to narrow the
499
+ // event type, and TypeScript forbids a super call to an abstract member.
500
+ // Therefore the code reaches the implementation of XState directly. `this` IS the
501
+ // actor. This call is therefore exactly the call of `super.send(event)`: the relay
502
+ // that emits the @xstate.event inspection event and puts the event in the mailbox
503
+ // of this actor.
480
504
  Actor.prototype.send.call(this, event);
481
- // Call onTransition hook
505
+ // Call the onTransition hook
482
506
  const onTransition = this.hooks.onTransition;
483
507
  if (onTransition) {
484
508
  const nextSnapshot = this.getSnapshot();
@@ -486,19 +510,19 @@ export class PlayerActor extends AbstractActor {
486
510
  }
487
511
  }
488
512
  /**
489
- * Get current snapshot
513
+ * Returns the current snapshot
490
514
  */
491
515
  getSnapshot() {
492
516
  return super.getSnapshot();
493
517
  }
494
518
  subscribe(nextListenerOrObserver, errorListener, completeListener) {
495
- // XState's subscribe() normalizes function-or-observer internally; the cast
496
- // only reconciles the overload signatures.
519
+ // The subscribe() method of XState accepts a function and also an observer, and it
520
+ // normalizes them internally. The cast joins the two overload signatures only.
497
521
  const subscription = super.subscribe(nextListenerOrObserver, errorListener, completeListener);
498
- // Observers without an error listener make XState rethrow actor errors
499
- // globally on their behalf; count them so the internal listener's own
500
- // loud-default rethrow stands down while one is active (see the
501
- // constructor's error listener).
522
+ // An observer without an error listener makes XState throw each actor error again,
523
+ // globally, for that observer. The code counts those observers. Therefore the loud
524
+ // default of the internal listener, which also throws again, stands down while one
525
+ // of them is active. See the error listener of the constructor.
502
526
  const hasErrorListener = typeof nextListenerOrObserver === "object" && nextListenerOrObserver !== null
503
527
  ? typeof nextListenerOrObserver.error === "function"
504
528
  : typeof errorListener === "function";
@@ -518,30 +542,31 @@ export class PlayerActor extends AbstractActor {
518
542
  };
519
543
  }
520
544
  /**
521
- * Listen for events this actor emits via the `emit` action.
545
+ * Listens for the events that this actor emits with the `emit` action.
522
546
  *
523
- * @param type - Emitted event type to listen for, or `"*"` for all.
524
- * @param handler - Called with each matching emitted event.
525
- * @returns Subscription with an `unsubscribe()` method.
547
+ * @param type - The type of the emitted event to listen for, or `"*"` for every event.
548
+ * @param handler - The actor calls it with each emitted event that matches.
549
+ * @returns The subscription, with an `unsubscribe()` method.
526
550
  */
527
551
  on(type, handler) {
528
552
  return super.on(type, handler);
529
553
  }
530
554
  /**
531
- * Get this actor's persisted snapshot.
555
+ * Returns the persisted snapshot of this actor.
532
556
  *
533
- * Suitable for serialization and later restoration via the factory's
534
- * `restore.snapshot` option.
557
+ * Use it to serialize the state, and to restore it later with the
558
+ * `restore.snapshot` option of the factory.
535
559
  */
536
560
  getPersistedSnapshot(options) {
537
561
  const forward = super.getPersistedSnapshot;
538
562
  return forward.call(this, options);
539
563
  }
540
564
  /**
541
- * Derive and cache the view at state entry once per transition, stored in
542
- * the signal rather than recomputed per read.
565
+ * Derives the view at the entry of a state, and keeps it. This happens one time for
566
+ * each transition. The signal holds the view, and the code computes it not on each
567
+ * read.
543
568
  *
544
- * @param snapshot - Current XState snapshot
569
+ * @param snapshot - The current XState snapshot
545
570
  */
546
571
  validateAndCacheView(snapshot) {
547
572
  if (snapshot === this.lastViewSnapshot) {
@@ -550,19 +575,20 @@ export class PlayerActor extends AbstractActor {
550
575
  this.lastViewSnapshot = snapshot;
551
576
  try {
552
577
  const view = deriveCurrentView(snapshot);
553
- // Emit only when the rendered view actually changed: deriveCurrentView
554
- // returns a fresh object per call, so reference identity cannot tell,
555
- // and re-emitting a fresh reference for a snapshot that does not alter
556
- // the view (e.g. a context-only assign) would make downstream providers
557
- // remount the UI, wiping in-view state. Deep equality is deliberately
558
- // NOT used here it cannot see inside Maps/Sets (suppressing genuine
559
- // changes) and recurses forever on cyclic props. The last emitted spec
560
- // IS the signal's current value; read it untracked so the gate never
561
- // registers currentView as a dependency of a surrounding computation.
578
+ // Emit only after a real change of the view on the screen: deriveCurrentView
579
+ // returns a fresh object on each call, and the identity of the reference therefore
580
+ // tells nothing. A new reference for a snapshot that changes the view not, for
581
+ // example a context-only assign, makes a provider below mount the UI again, and
582
+ // that removes the state of the view. A deep equality test is deliberately NOT
583
+ // here: it sees nothing inside a Map or a Set, and it therefore stops a real
584
+ // change, and it recurses without an end on a cyclic prop. The last spec of an
585
+ // emission IS the current value of the signal. Read it without a track, so that
586
+ // the gate registers currentView never as a dependency of a computation around
587
+ // it.
562
588
  const lastEmittedView = Signal.subtle.untrack(() => this.currentView.get());
563
- // Reuse the previous composed state reference when the /context
564
- // projection is value-unchanged, so a context-only assign that does
565
- // not alter projected values cannot churn state identity.
589
+ // Use the reference of the previous composed state again when the value of the
590
+ // /context projection did not change. A context-only assign that changes no
591
+ // projected value therefore changes the identity of the state not.
566
592
  const nextView = reuseComposedState(lastEmittedView, view);
567
593
  if (viewSpecsEquivalent(lastEmittedView, nextView)) {
568
594
  return;
@@ -574,11 +600,13 @@ export class PlayerActor extends AbstractActor {
574
600
  if (onError) {
575
601
  onError(this, toError(error));
576
602
  }
577
- // On error: keep the last valid view (don't clear)
603
+ // On an error: keep the last valid view, and clear it not
578
604
  }
579
605
  }
580
606
  /**
581
- * Convenience dispose method for cleanup an alias for {@link stop}.
607
+ * The dispose method, for the cleanup. It is the alias of {@link stop}.
608
+ *
609
+ * @deprecated Use {@link stop}. Will be removed in the next major.
582
610
  */
583
611
  dispose() {
584
612
  this.stop();