@xmachines/play-xstate 6.1.0 → 7.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 (42) hide show
  1. package/README.md +64 -12
  2. package/dist/capabilities.js +6 -2
  3. package/dist/capabilities.js.map +1 -1
  4. package/dist/define-player.js +8 -8
  5. package/dist/define-player.js.map +1 -1
  6. package/dist/errors.js +262 -259
  7. package/dist/errors.js.map +1 -1
  8. package/dist/index.js +4 -25
  9. package/dist/player-actor.js +318 -448
  10. package/dist/player-actor.js.map +1 -1
  11. package/dist/routing/build-url.js +74 -108
  12. package/dist/routing/build-url.js.map +1 -1
  13. package/dist/routing/derive-current-route.d.ts.map +1 -1
  14. package/dist/routing/derive-current-route.js +46 -67
  15. package/dist/routing/derive-current-route.js.map +1 -1
  16. package/dist/routing/derive-initial-route.js +53 -64
  17. package/dist/routing/derive-initial-route.js.map +1 -1
  18. package/dist/routing/derive-route.js +225 -242
  19. package/dist/routing/derive-route.js.map +1 -1
  20. package/dist/routing/format-play-route-transitions.js +192 -258
  21. package/dist/routing/format-play-route-transitions.js.map +1 -1
  22. package/dist/state-meta.d.ts +56 -25
  23. package/dist/state-meta.d.ts.map +1 -1
  24. package/dist/state-meta.js +164 -105
  25. package/dist/state-meta.js.map +1 -1
  26. package/dist/view/derive-current-view.d.ts +18 -10
  27. package/dist/view/derive-current-view.d.ts.map +1 -1
  28. package/dist/view/derive-current-view.js +243 -221
  29. package/dist/view/derive-current-view.js.map +1 -1
  30. package/dist/with-routing.js +94 -136
  31. package/dist/with-routing.js.map +1 -1
  32. package/dist/with-view.d.ts.map +1 -1
  33. package/dist/with-view.js +82 -152
  34. package/dist/with-view.js.map +1 -1
  35. package/package.json +19 -19
  36. package/dist/index.js.map +0 -1
  37. package/dist/routing/index.js +0 -14
  38. package/dist/routing/index.js.map +0 -1
  39. package/dist/routing/types.js +0 -2
  40. package/dist/routing/types.js.map +0 -1
  41. package/dist/types.js +0 -2
  42. package/dist/types.js.map +0 -1
@@ -1,454 +1,324 @@
1
- import { Actor, } from "xstate";
2
- import { createWritableAtom } from "@xmachines/play-atom";
3
- import { isError } from "@xmachines/core/utils";
4
1
  import { ActorThrewNonErrorError, InvalidEventError, InvalidMachineError } from "./errors.js";
2
+ import { isError } from "@xmachines/core/utils";
3
+ import { Actor } from "xstate";
4
+ import { createWritableAtom } from "@xmachines/play-atom";
5
+ //#region packages/play-xstate/src/player-actor.ts
5
6
  /**
6
- * Normalizes a failure of the actor for `onError`.
7
- *
8
- * @internal
9
- *
10
- * The function gives an `Error` to the handler without a change. The error of the
11
- * machine therefore keeps its identity: an `instanceof` test of a consumer still
12
- * works, and the path without an `onError` throws that same object again. Every
13
- * other value is ours to build, and it becomes a `PlayError` with a code. That
14
- * error carries the value from the throw as its `cause`.
15
- */
16
- export const toError = (value) => {
17
- try {
18
- if (isError(value)) {
19
- return value;
20
- }
21
- }
22
- catch {
23
- // The classification itself can throw for a hostile value, because a revoked
24
- // Proxy traps instanceof and also the inspection of the brand. Continue, and wrap
25
- // the value.
26
- }
27
- return new ActorThrewNonErrorError(value);
7
+ * Normalizes a failure of the actor for `onError`.
8
+ *
9
+ * @internal
10
+ *
11
+ * The function gives an `Error` to the handler without a change. The error of the
12
+ * machine therefore keeps its identity: an `instanceof` test of a consumer still
13
+ * works, and the path without an `onError` throws that same object again. Every
14
+ * other value is ours to build, and it becomes a `PlayError` with a code. That
15
+ * error carries the value from the throw as its `cause`.
16
+ */
17
+ var toError = (value) => {
18
+ try {
19
+ if (isError(value)) return value;
20
+ } catch {}
21
+ return new ActorThrewNonErrorError(value);
28
22
  };
29
23
  /**
30
- * Tells you if a snapshot is worth a propagation to the atoms: an active
31
- * snapshot, or a "done" snapshot, which means that the machine reached a final state
32
- * at the top level. The code skips an error snapshot and a stopped snapshot.
33
- * Therefore the atoms keep the last observable state, and they show no artifact of
34
- * a teardown or of an error.
35
- */
36
- const isObservableSnapshot = (snapshot) => snapshot.status === "active" || snapshot.status === "done";
24
+ * Tells you if a snapshot is worth a propagation to the atoms: an active
25
+ * snapshot, or a "done" snapshot, which means that the machine reached a final state
26
+ * at the top level. The code skips an error snapshot and a stopped snapshot.
27
+ * Therefore the atoms keep the last observable state, and they show no artifact of
28
+ * a teardown or of an error.
29
+ */
30
+ var isObservableSnapshot = (snapshot) => snapshot.status === "active" || snapshot.status === "done";
37
31
  /**
38
- * The concrete XState actor. It implements the atom protocol of the Play Architecture
39
- *
40
- * The class extends the `Actor` class of XState directly, and it implements
41
- * {@link @xmachines/play-actor!PlayActor}. It gives you the XState v5 integration, and it keeps the
42
- * compatibility with the ecosystem, such as the XState inspection and the devtools.
43
- * The constructor of the base class receives the machine. Therefore a `PlayerActor`
44
- * **is** the XState actor, and it is no wrapper around one: every member of the
45
- * XState `Actor` class works on the state of this instance, and this class adds the
46
- * reactive state on the atoms for the observation by the infrastructure.
47
- *
48
- * **Capabilities:** the class implements both the
49
- * {@link @xmachines/play-router!index.Routable} interface and the
50
- * {@link @xmachines/play-view!index.Viewable} interface. It therefore supports the
51
- * routing and the view rendering.
52
- *
53
- * **Architectural context:** the class implements **Actor Authority (INV-01)**,
54
- * because the guards of the XState machine control every decision of the
55
- * navigation. The infrastructure observes the atoms of the actor (`state`,
56
- * `currentRoute`, and `currentView`), but it changes no state directly: every
57
- * change goes through the event handlers of the state machine.
58
- *
59
- * @typeParam TMachine - The type of the XState v5 state machine
60
- *
61
- * @example
62
- * The creation of an actor, and its lifecycle
63
- * ```typescript
64
- * import { setup } from "xstate";
65
- * import { definePlayer, compose, PlayerActor } from "@xmachines/play-xstate";
66
- *
67
- * const machine = setup({}).createMachine({
68
- * initial: 'idle',
69
- * states: {
70
- * idle: {
71
- * meta: {
72
- * route: '/',
73
- * // A view spec needs `root` and `elements`. Every other shape derives null.
74
- * view: {
75
- * root: 'home',
76
- * elements: { home: { type: 'HomePage', props: {}, children: [] } },
77
- * },
78
- * },
79
- * }
80
- * }
81
- * });
82
- *
83
- * const createPlayer = definePlayer({ machine });
84
- * const actor = createPlayer();
85
- * actor.start();
86
- *
87
- * // Observe the atoms
88
- * console.log(actor.currentRoute.get()); // '/'
89
- * console.log(actor.currentView.get()?.root); // 'home'
90
- * ```
91
- *
92
- * @example
93
- * The atom lifecycle with a watch
94
- * ```typescript
95
- * import { watchAtom } from "@xmachines/play-atom";
96
- *
97
- * const stop = watchAtom(actor.state, (snapshot) => {
98
- * console.log('State changed:', snapshot.value);
99
- * });
100
- *
101
- * actor.send({ type: 'play.route', to: '#about' });
102
- * // watchAtom delivers the new value from a microtask
103
- *
104
- * stop();
105
- * ```
106
- *
107
- * @see [Play RFC](../../docs/rfc/play.md)
108
- * @see {@link definePlayer} for the creation through a factory
109
- * @see {@link @xmachines/play-actor!PlayActor} for the atom protocol
110
- * @see {@link @xmachines/play-router!index.Routable} for the routing capability
111
- * @see {@link @xmachines/play-view!index.Viewable} for the view rendering capability
112
- *
113
- * @remarks
114
- * **The routing:** this actor supports the `route: {}` config pattern of XState and
115
- * also a `play.route` event with parameters. The `deriveRoute()` function reads
116
- * `meta.route`, which is the Stately pattern, for a URL template, and it substitutes
117
- * each parameter.
118
- *
119
- * **The pattern of the view atom:** `withView` of `@xmachines/play-xstate/view` adds
120
- * `currentView`, and that atom is a COMPUTED atom over `state`. One write for each
121
- * transition — `state.set(snapshot)` — therefore reaches the view and the route in one
122
- * flush, and no subscriber reads the new state beside an old derivation. The atom
123
- * memoizes the result, so a repeated read computes no view again.
124
- */
125
- export class PlayerActor extends Actor {
126
- playerOptions;
127
- /**
128
- * The three inputs of the constructor that a CAPABILITY needs.
129
- *
130
- * A mixin over a generic base cannot declare a constructor: TypeScript then demands the
131
- * signature `...args: any[]` (TS2545), which this workspace does not write. A field
132
- * initializer of a subclass runs after `super()` instead, and it reads these. The
133
- * routing capability derives `initialRoute` from all three.
134
- */
135
- playerMachine;
136
- /** @see {@link playerMachine} */
137
- playerInput;
138
- /** @see {@link playerMachine} */
139
- playerRestoredSnapshot;
140
- /**
141
- * The live options object of the caller, or an empty object during the construction.
142
- *
143
- * XState gives this actor to a context factory as `self`, and to an `inspect`
144
- * observer as `actorRef`, from inside its own constructor, before the code assigns
145
- * any field here. A read of a hook through this accessor therefore does not throw
146
- * in that window. A throw goes to the initialization of XState, which parks it as
147
- * an error snapshot. The accessor also keeps the behavior that the documentation of
148
- * the object promises: a read at the moment of the delivery.
149
- *
150
- * The fields below that this window touches have a `declare` modifier for the same
151
- * reason. This target compiles a class field into `Object.defineProperty`, which
152
- * runs after `super()` returns. A plain declaration, with an initializer or without
153
- * one, therefore resets each value of the window to `undefined`. A `declare`
154
- * modifier emits nothing, and those values survive.
155
- */
156
- get hooks() {
157
- return this.playerOptions ?? {};
158
- }
159
- // The requirements of the PlayActor contract
160
- state;
161
- /**
162
- * Tells you if the current state of the actor accepts the given event.
163
- *
164
- * The type is the event union of the machine. An unknown event type is therefore a
165
- * compile error. The method evaluates the event against the snapshot atom.
166
- *
167
- * @example
168
- * ```typescript
169
- * if (actor.can({ type: "auth.logout" })) { ... }
170
- * ```
171
- */
172
- can(event) {
173
- // A read of the atom keeps can() reactive: a computed atom over the atom
174
- // computes its value again on each transition.
175
- const snapshot = this.state?.get();
176
- // The STATUS decides first, and the presence of the method decides second.
177
- //
178
- // An actor answers an event only while it is active. XState 5.32 made that test
179
- // unnecessary by accident — the error snapshot of a failed initialization carried no
180
- // `can` method, so a test of the method alone answered `false` for it — and 5.33
181
- // gives that snapshot a working `can`, which then answered `true` for an actor that
182
- // processes an event never. The status is what the question is really about, and a
183
- // `done` actor answers `false` for the same reason.
184
- //
185
- // A STOPPED actor is the exception, and it is deliberate: the subscription below
186
- // updates this atom on a stable state alone, so the atom keeps the last ACTIVE
187
- // snapshot after a teardown rather than freezing on an artifact of it. `can()`
188
- // therefore answers for the last live state of a stopped actor.
189
- if (snapshot?.status !== "active")
190
- return false;
191
- return typeof snapshot.can === "function" ? snapshot.can(event) : false;
192
- }
193
- constructor(machine, options, input, restoredSnapshot) {
194
- // A defensive check before super(): a machine that is not an object fails deep
195
- // inside the constructor of XState, with an opaque TypeError, and not with a coded
196
- // error.
197
- if (!machine || typeof machine !== "object") {
198
- throw new InvalidMachineError();
199
- }
200
- // THIS is the actor. The machine and its runtime options go directly to the Actor
201
- // constructor of XState, which receives the same arguments as `createActor`
202
- // forwards. Therefore no second instance answers for the real one, and every
203
- // Actor member without an override here works on the real state.
204
- //
205
- // XState 5.28.0: the options object has a conditional type constraint on `input`,
206
- // and TypeScript cannot resolve that constraint against an unbound generic
207
- // TMachine. The cast stays inside the type system of XState, and the signature of
208
- // this constructor gives `input` its type. Therefore each caller keeps the check
209
- // at the compile time.
210
- // Follow the improvements of the XState types: https://github.com/statelyai/xstate/issues
211
- super(machine, {
212
- input,
213
- snapshot: restoredSnapshot,
214
- inspect: options?.inspect,
215
- });
216
- this.playerMachine = machine;
217
- this.playerInput = input;
218
- this.playerRestoredSnapshot = restoredSnapshot;
219
- this.playerOptions = options || {};
220
- // Initialize the state atom. Each update is synchronous, with no batching in a
221
- // microtask: XState groups the transitions of one send() call into one
222
- // subscription callback already, and a synchronous update shows each guard
223
- // redirect to a router bridge at once.
224
- // `createWritableAtom`, and not `createAtom`. The overload pair of `createAtom` reads
225
- // a function argument as a computed atom, and `ReturnType<TMachine["transition"]>` is
226
- // an unresolved conditional type here, so TypeScript cannot rule a function out. It
227
- // picks the computed signature, which returns a `ReadonlyAtom` that carries no `set`.
228
- this.state = createWritableAtom(this.getSnapshot());
229
- // Observe the transitions of this actor. The code uses `super`, and not `this`, so
230
- // that the bookkeeping of the next-only subscriptions in the subscribe override
231
- // stays about the subscriptions of the user code.
232
- super.subscribe({
233
- next: (snapshot) => {
234
- // Update on a stable state only: an active snapshot, and the "done" snapshot of a
235
- // final state at the top level. The code skips an error snapshot and a stopped
236
- // snapshot. Therefore an atom never freezes on an artifact of a teardown.
237
- if (isObservableSnapshot(snapshot)) {
238
- // Each state update is synchronous. Therefore a router bridge sees a guard redirect at once.
239
- this.state.set(snapshot);
240
- // ONE write for each transition, and the line above is it. `currentRoute` and
241
- // `currentView` are computed atoms over `state`, so that write propagates to
242
- // both in ONE flush, in topological order. No observer therefore reads the new
243
- // state beside an old derivation, and the composition order of the capabilities
244
- // decides nothing.
245
- //
246
- // A capability that must run an EFFECT on a transition overrides
247
- // `onSnapshot`. A capability that publishes a value derives it instead.
248
- //
249
- // The whole order of one transition:
250
- // 1. state receives its new value, and every computed atom over it follows
251
- // 2. onSnapshot runs each capability, in the composition order
252
- // 3. the onStateChange hook runs
253
- // 4. send() then calls onTransition
254
- this.onSnapshot(snapshot);
255
- // Call the onStateChange hook
256
- const onStateChange = this.hooks.onStateChange;
257
- if (onStateChange) {
258
- onStateChange(this, snapshot);
259
- }
260
- }
261
- },
262
- // The code registers the listener always, and it reads the handler at the moment
263
- // of the DELIVERY: the options object is shared by its reference. Therefore an
264
- // onError handler on that object after the construction still receives each actor
265
- // error, and the removal of a handler stops the silence again.
266
- // Without a handler, the listener throws the error again. The observer dispatch of
267
- // XState then sends it to its global unhandled rethrow, which is the same loud
268
- // default as a listener that the code registers not.
269
- error: (error) => {
270
- const handler = this.hooks.onError;
271
- if (handler) {
272
- handler(this, toError(error));
273
- return;
274
- }
275
- // A next-only subscription of the user code makes XState report this delivery
276
- // globally already. A second throw here reports it two times.
277
- if ((this.nextOnlySubscriptions ?? 0) > 0) {
278
- return;
279
- }
280
- throw error;
281
- },
282
- });
283
- // The callbacks of the constructor of XState can reach everything above. After this
284
- // point the instance is complete.
285
- this.constructed = true;
286
- }
287
- /**
288
- * Starts the actor.
289
- *
290
- * The method fires `onStart` on each real start, which is every transition from
291
- * "not running" to "running". A start after a stop is such a transition, and XState
292
- * permits it: its own `start()` stops only while the actor RUNS already. A second
293
- * call while the actor runs fires no hook. Therefore a defensive double mount runs
294
- * the side effects of `onStart` one time for one real start.
295
- */
296
- start() {
297
- // See stop(): a call that reaches in through the construction window runs an actor
298
- // that is not complete.
299
- if (!this.constructed) {
300
- return this;
301
- }
302
- super.start();
303
- if (this.lifecycle !== "running") {
304
- this.lifecycle = "running";
305
- const onStart = this.hooks.onStart;
306
- if (onStart) {
307
- onStart(this);
308
- }
309
- }
310
- return this;
311
- }
312
- /**
313
- * Stops the actor and cleans up.
314
- *
315
- * The method fires `onStop` only when the actor ran. This matches XState, where a
316
- * stop of an actor that never started, or of an actor that stopped already, does
317
- * nothing and tears nothing down. Therefore the paired cleanup runs never two
318
- * times, and it runs never against a resource that `onStart` did not take. A stop
319
- * does not close the actor for ever: a later `start()` is a new lifecycle, and it
320
- * fires `onStart` again.
321
- */
322
- stop() {
323
- // A call that reaches in through the construction window, for example a context
324
- // factory that stops its own `self`, marks the actor as stopped before the code
325
- // registers its internal subscription. XState drops each observer of a stopped
326
- // actor. The atoms therefore move never again. There is nothing to tear down
327
- // during the construction, and the code ignores such a call.
328
- if (!this.constructed) {
329
- return this;
330
- }
331
- super.stop();
332
- const wasRunning = this.lifecycle === "running";
333
- this.lifecycle = "stopped";
334
- const onStop = this.hooks.onStop;
335
- if (wasRunning && onStop) {
336
- onStop(this);
337
- }
338
- return this;
339
- }
340
- /**
341
- * Sends an event to this actor.
342
- *
343
- * The guards of the state machine of the actor decide if the event causes a
344
- * transition. Give any event of the event union of the machine: a domain event, a
345
- * routing event, and so on.
346
- *
347
- * @param event - An event of the `EventFromLogic<TMachine>` union of the machine.
348
- *
349
- * @throws {InvalidEventError} When `event` is not a plain object, for example
350
- * `null`, `undefined`, a string, or a number. Import the class from
351
- * `@xmachines/play-xstate/errors`.
352
- *
353
- * @example
354
- * ```typescript
355
- * // A domain event, with the type of the event union of the machine
356
- * actor.send({ type: "auth.login", userId: "123" });
357
- *
358
- * // A routing event
359
- * actor.send({ type: "play.route", to: "#home" });
360
- * ```
361
- */
362
- send(event) {
363
- // A defensive check: the event must not be null and not undefined
364
- if (!event || typeof event !== "object") {
365
- throw new InvalidEventError(event);
366
- }
367
- // Inside the construction window there is no readable snapshot, and no hook can
368
- // exist yet: deliver the event and return.
369
- if (!this.constructed) {
370
- Actor.prototype.send.call(this, event);
371
- return;
372
- }
373
- // The code captures the snapshot always, because getSnapshot() is one property
374
- // read, and because the options object is live: an onTransition handler that
375
- // arrives during the processing of this event, for example from onStateChange,
376
- // must still run for it, with the correct snapshot from before the send.
377
- const prevSnapshot = this.getSnapshot();
378
- // Send the event to the XState actor.
379
- // The class declares send() to narrow the
380
- // event type, and TypeScript forbids a super call to an abstract member.
381
- // Therefore the code reaches the implementation of XState directly. `this` IS the
382
- // actor. This call is therefore exactly the call of `super.send(event)`: the relay
383
- // that emits the @xstate.event inspection event and puts the event in the mailbox
384
- // of this actor.
385
- Actor.prototype.send.call(this, event);
386
- // Call the onTransition hook
387
- const onTransition = this.hooks.onTransition;
388
- if (onTransition) {
389
- const nextSnapshot = this.getSnapshot();
390
- onTransition(this, prevSnapshot, nextSnapshot);
391
- }
392
- }
393
- subscribe(nextListenerOrObserver, errorListener, completeListener) {
394
- // The subscribe() method of XState accepts a function and also an observer, and it
395
- // normalizes them internally. The cast joins the two overload signatures only.
396
- const subscription = super.subscribe(nextListenerOrObserver, errorListener, completeListener);
397
- // An observer without an error listener makes XState throw each actor error again,
398
- // globally, for that observer. The code counts those observers. Therefore the loud
399
- // default of the internal listener, which also throws again, stands down while one
400
- // of them is active. See the error listener of the constructor.
401
- const hasErrorListener = typeof nextListenerOrObserver === "object" && nextListenerOrObserver !== null
402
- ? typeof nextListenerOrObserver.error === "function"
403
- : typeof errorListener === "function";
404
- if (hasErrorListener) {
405
- return subscription;
406
- }
407
- this.nextOnlySubscriptions = (this.nextOnlySubscriptions ?? 0) + 1;
408
- let counted = true;
409
- return {
410
- unsubscribe: () => {
411
- if (counted) {
412
- counted = false;
413
- this.nextOnlySubscriptions = (this.nextOnlySubscriptions ?? 1) - 1;
414
- }
415
- subscription.unsubscribe();
416
- },
417
- };
418
- }
419
- /**
420
- * Returns the persisted snapshot of this actor.
421
- *
422
- * Use it to serialize the state, and to restore it later with the
423
- * `restore.snapshot` option of the factory.
424
- */
425
- getPersistedSnapshot(options) {
426
- const forward = super.getPersistedSnapshot;
427
- return forward.call(this, options);
428
- }
429
- /**
430
- * The point where a capability derives its own atoms from a new snapshot.
431
- *
432
- * The base derives nothing: `PlayActor` asks for `state` and `send`, and this class
433
- * gives exactly those.
434
- *
435
- * A capability that PUBLISHES a value overrides this method NOT. It derives the value
436
- * with a computed atom over `state`, as `withRouting` and `withView` both do, and the
437
- * engine then evaluates every derivation from the one write of a transition, in
438
- * topological order. The composition order therefore decides no value.
439
- *
440
- * This method remains for a capability that must run an EFFECT on a transition. Such a
441
- * mixin overrides it and calls `super.onSnapshot(snapshot)` FIRST, so that every
442
- * capability composed before it runs its own effect first.
443
- *
444
- * The method runs after `state` holds the new snapshot and before the `onStateChange`
445
- * hook of the options.
446
- *
447
- * @param snapshot - The stable snapshot of this transition.
448
- */
449
- onSnapshot(snapshot) {
450
- // The base class holds no capability. A mixin overrides this method.
451
- void snapshot;
452
- }
453
- }
32
+ * The concrete XState actor. It implements the atom protocol of the Play Architecture
33
+ *
34
+ * The class extends the `Actor` class of XState directly, and it implements
35
+ * {@link @xmachines/play-actor!PlayActor}. It gives you the XState v5 integration, and it keeps the
36
+ * compatibility with the ecosystem, such as the XState inspection and the devtools.
37
+ * The constructor of the base class receives the machine. Therefore a `PlayerActor`
38
+ * **is** the XState actor, and it is no wrapper around one: every member of the
39
+ * XState `Actor` class works on the state of this instance, and this class adds the
40
+ * reactive state on the atoms for the observation by the infrastructure.
41
+ *
42
+ * **Capabilities:** the class implements both the
43
+ * {@link @xmachines/play-router!index.Routable} interface and the
44
+ * {@link @xmachines/play-view!index.Viewable} interface. It therefore supports the
45
+ * routing and the view rendering.
46
+ *
47
+ * **Architectural context:** the class implements **Actor Authority (INV-01)**,
48
+ * because the guards of the XState machine control every decision of the
49
+ * navigation. The infrastructure observes the atoms of the actor (`state`,
50
+ * `currentRoute`, and `currentView`), but it changes no state directly: every
51
+ * change goes through the event handlers of the state machine.
52
+ *
53
+ * @typeParam TMachine - The type of the XState v5 state machine
54
+ *
55
+ * @example
56
+ * The creation of an actor, and its lifecycle
57
+ * ```typescript
58
+ * import { setup } from "xstate";
59
+ * import { definePlayer, compose, PlayerActor } from "@xmachines/play-xstate";
60
+ *
61
+ * const machine = setup({}).createMachine({
62
+ * initial: 'idle',
63
+ * states: {
64
+ * idle: {
65
+ * meta: {
66
+ * route: '/',
67
+ * // A view spec needs `root` and `elements`. Every other shape derives null.
68
+ * view: {
69
+ * root: 'home',
70
+ * elements: { home: { type: 'HomePage', props: {}, children: [] } },
71
+ * },
72
+ * },
73
+ * }
74
+ * }
75
+ * });
76
+ *
77
+ * const createPlayer = definePlayer({ machine });
78
+ * const actor = createPlayer();
79
+ * actor.start();
80
+ *
81
+ * // Observe the atoms
82
+ * console.log(actor.currentRoute.get()); // '/'
83
+ * console.log(actor.currentView.get()?.root); // 'home'
84
+ * ```
85
+ *
86
+ * @example
87
+ * The atom lifecycle with a watch
88
+ * ```typescript
89
+ * import { watchAtom } from "@xmachines/play-atom";
90
+ *
91
+ * const stop = watchAtom(actor.state, (snapshot) => {
92
+ * console.log('State changed:', snapshot.value);
93
+ * });
94
+ *
95
+ * actor.send({ type: 'play.route', to: '#about' });
96
+ * // watchAtom delivers the new value from a microtask
97
+ *
98
+ * stop();
99
+ * ```
100
+ *
101
+ * @see [Play RFC](../../docs/rfc/play.md)
102
+ * @see {@link definePlayer} for the creation through a factory
103
+ * @see {@link @xmachines/play-actor!PlayActor} for the atom protocol
104
+ * @see {@link @xmachines/play-router!index.Routable} for the routing capability
105
+ * @see {@link @xmachines/play-view!index.Viewable} for the view rendering capability
106
+ *
107
+ * @remarks
108
+ * **The routing:** this actor supports the `route: {}` config pattern of XState and
109
+ * also a `play.route` event with parameters. The `deriveRoute()` function reads
110
+ * `meta.route`, which is the Stately pattern, for a URL template, and it substitutes
111
+ * each parameter.
112
+ *
113
+ * **The pattern of the view atom:** `withView` of `@xmachines/play-xstate/view` adds
114
+ * `currentView`, and that atom is a COMPUTED atom over `state`. One write for each
115
+ * transition — `state.set(snapshot)` — therefore reaches the view and the route in one
116
+ * flush, and no subscriber reads the new state beside an old derivation. The atom
117
+ * memoizes the result, so a repeated read computes no view again.
118
+ */
119
+ var PlayerActor = class extends Actor {
120
+ playerOptions;
121
+ /**
122
+ * The three inputs of the constructor that a CAPABILITY needs.
123
+ *
124
+ * A mixin over a generic base cannot declare a constructor: TypeScript then demands the
125
+ * signature `...args: any[]` (TS2545), which this workspace does not write. A field
126
+ * initializer of a subclass runs after `super()` instead, and it reads these. The
127
+ * routing capability derives `initialRoute` from all three.
128
+ */
129
+ playerMachine;
130
+ /** @see {@link playerMachine} */
131
+ playerInput;
132
+ /** @see {@link playerMachine} */
133
+ playerRestoredSnapshot;
134
+ /**
135
+ * The live options object of the caller, or an empty object during the construction.
136
+ *
137
+ * XState gives this actor to a context factory as `self`, and to an `inspect`
138
+ * observer as `actorRef`, from inside its own constructor, before the code assigns
139
+ * any field here. A read of a hook through this accessor therefore does not throw
140
+ * in that window. A throw goes to the initialization of XState, which parks it as
141
+ * an error snapshot. The accessor also keeps the behavior that the documentation of
142
+ * the object promises: a read at the moment of the delivery.
143
+ *
144
+ * The fields below that this window touches have a `declare` modifier for the same
145
+ * reason. This target compiles a class field into `Object.defineProperty`, which
146
+ * runs after `super()` returns. A plain declaration, with an initializer or without
147
+ * one, therefore resets each value of the window to `undefined`. A `declare`
148
+ * modifier emits nothing, and those values survive.
149
+ */
150
+ get hooks() {
151
+ return this.playerOptions ?? {};
152
+ }
153
+ state;
154
+ /**
155
+ * Tells you if the current state of the actor accepts the given event.
156
+ *
157
+ * The type is the event union of the machine. An unknown event type is therefore a
158
+ * compile error. The method evaluates the event against the snapshot atom.
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * if (actor.can({ type: "auth.logout" })) { ... }
163
+ * ```
164
+ */
165
+ can(event) {
166
+ const snapshot = this.state?.get();
167
+ if (snapshot?.status !== "active") return false;
168
+ return typeof snapshot.can === "function" ? snapshot.can(event) : false;
169
+ }
170
+ constructor(machine, options, input, restoredSnapshot) {
171
+ if (!machine || typeof machine !== "object") throw new InvalidMachineError();
172
+ super(machine, {
173
+ input,
174
+ snapshot: restoredSnapshot,
175
+ inspect: options?.inspect
176
+ });
177
+ this.playerMachine = machine;
178
+ this.playerInput = input;
179
+ this.playerRestoredSnapshot = restoredSnapshot;
180
+ this.playerOptions = options || {};
181
+ this.state = createWritableAtom(this.getSnapshot());
182
+ super.subscribe({
183
+ next: (snapshot) => {
184
+ if (isObservableSnapshot(snapshot)) {
185
+ this.state.set(snapshot);
186
+ this.onSnapshot(snapshot);
187
+ const onStateChange = this.hooks.onStateChange;
188
+ if (onStateChange) onStateChange(this, snapshot);
189
+ }
190
+ },
191
+ error: (error) => {
192
+ const handler = this.hooks.onError;
193
+ if (handler) {
194
+ handler(this, toError(error));
195
+ return;
196
+ }
197
+ if ((this.nextOnlySubscriptions ?? 0) > 0) return;
198
+ throw error;
199
+ }
200
+ });
201
+ this.constructed = true;
202
+ }
203
+ /**
204
+ * Starts the actor.
205
+ *
206
+ * The method fires `onStart` on each real start, which is every transition from
207
+ * "not running" to "running". A start after a stop is such a transition, and XState
208
+ * permits it: its own `start()` stops only while the actor RUNS already. A second
209
+ * call while the actor runs fires no hook. Therefore a defensive double mount runs
210
+ * the side effects of `onStart` one time for one real start.
211
+ */
212
+ start() {
213
+ if (!this.constructed) return this;
214
+ super.start();
215
+ if (this.lifecycle !== "running") {
216
+ this.lifecycle = "running";
217
+ const onStart = this.hooks.onStart;
218
+ if (onStart) onStart(this);
219
+ }
220
+ return this;
221
+ }
222
+ /**
223
+ * Stops the actor and cleans up.
224
+ *
225
+ * The method fires `onStop` only when the actor ran. This matches XState, where a
226
+ * stop of an actor that never started, or of an actor that stopped already, does
227
+ * nothing and tears nothing down. Therefore the paired cleanup runs never two
228
+ * times, and it runs never against a resource that `onStart` did not take. A stop
229
+ * does not close the actor for ever: a later `start()` is a new lifecycle, and it
230
+ * fires `onStart` again.
231
+ */
232
+ stop() {
233
+ if (!this.constructed) return this;
234
+ super.stop();
235
+ const wasRunning = this.lifecycle === "running";
236
+ this.lifecycle = "stopped";
237
+ const onStop = this.hooks.onStop;
238
+ if (wasRunning && onStop) onStop(this);
239
+ return this;
240
+ }
241
+ /**
242
+ * Sends an event to this actor.
243
+ *
244
+ * The guards of the state machine of the actor decide if the event causes a
245
+ * transition. Give any event of the event union of the machine: a domain event, a
246
+ * routing event, and so on.
247
+ *
248
+ * @param event - An event of the `EventFromLogic<TMachine>` union of the machine.
249
+ *
250
+ * @throws {InvalidEventError} When `event` is not a plain object, for example
251
+ * `null`, `undefined`, a string, or a number. Import the class from
252
+ * `@xmachines/play-xstate/errors`.
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * // A domain event, with the type of the event union of the machine
257
+ * actor.send({ type: "auth.login", userId: "123" });
258
+ *
259
+ * // A routing event
260
+ * actor.send({ type: "play.route", to: "#home" });
261
+ * ```
262
+ */
263
+ send(event) {
264
+ if (!event || typeof event !== "object") throw new InvalidEventError(event);
265
+ if (!this.constructed) {
266
+ Actor.prototype.send.call(this, event);
267
+ return;
268
+ }
269
+ const prevSnapshot = this.getSnapshot();
270
+ Actor.prototype.send.call(this, event);
271
+ const onTransition = this.hooks.onTransition;
272
+ if (onTransition) {
273
+ const nextSnapshot = this.getSnapshot();
274
+ onTransition(this, prevSnapshot, nextSnapshot);
275
+ }
276
+ }
277
+ subscribe(nextListenerOrObserver, errorListener, completeListener) {
278
+ const subscription = super.subscribe(nextListenerOrObserver, errorListener, completeListener);
279
+ if (typeof nextListenerOrObserver === "object" && nextListenerOrObserver !== null ? typeof nextListenerOrObserver.error === "function" : typeof errorListener === "function") return subscription;
280
+ this.nextOnlySubscriptions = (this.nextOnlySubscriptions ?? 0) + 1;
281
+ let counted = true;
282
+ return { unsubscribe: () => {
283
+ if (counted) {
284
+ counted = false;
285
+ this.nextOnlySubscriptions = (this.nextOnlySubscriptions ?? 1) - 1;
286
+ }
287
+ subscription.unsubscribe();
288
+ } };
289
+ }
290
+ /**
291
+ * Returns the persisted snapshot of this actor.
292
+ *
293
+ * Use it to serialize the state, and to restore it later with the
294
+ * `restore.snapshot` option of the factory.
295
+ */
296
+ getPersistedSnapshot(options) {
297
+ return super.getPersistedSnapshot.call(this, options);
298
+ }
299
+ /**
300
+ * The point where a capability derives its own atoms from a new snapshot.
301
+ *
302
+ * The base derives nothing: `PlayActor` asks for `state` and `send`, and this class
303
+ * gives exactly those.
304
+ *
305
+ * A capability that PUBLISHES a value overrides this method NOT. It derives the value
306
+ * with a computed atom over `state`, as `withRouting` and `withView` both do, and the
307
+ * engine then evaluates every derivation from the one write of a transition, in
308
+ * topological order. The composition order therefore decides no value.
309
+ *
310
+ * This method remains for a capability that must run an EFFECT on a transition. Such a
311
+ * mixin overrides it and calls `super.onSnapshot(snapshot)` FIRST, so that every
312
+ * capability composed before it runs its own effect first.
313
+ *
314
+ * The method runs after `state` holds the new snapshot and before the `onStateChange`
315
+ * hook of the options.
316
+ *
317
+ * @param snapshot - The stable snapshot of this transition.
318
+ */
319
+ onSnapshot(snapshot) {}
320
+ };
321
+ //#endregion
322
+ export { PlayerActor, toError };
323
+
454
324
  //# sourceMappingURL=player-actor.js.map