@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.
- package/README.md +66 -65
- package/dist/define-player.d.ts +16 -16
- package/dist/define-player.js +16 -16
- package/dist/errors.d.ts +57 -50
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +63 -55
- package/dist/errors.js.map +1 -1
- package/dist/guards/compose.d.ts +53 -50
- package/dist/guards/compose.d.ts.map +1 -1
- package/dist/guards/compose.js +67 -63
- package/dist/guards/compose.js.map +1 -1
- package/dist/guards/helpers.d.ts +22 -22
- package/dist/guards/helpers.js +23 -23
- package/dist/guards/index.d.ts +9 -9
- package/dist/guards/index.js +9 -9
- package/dist/guards/types.d.ts +9 -8
- package/dist/guards/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/player-actor.d.ts +162 -146
- package/dist/player-actor.d.ts.map +1 -1
- package/dist/player-actor.js +269 -241
- package/dist/player-actor.js.map +1 -1
- package/dist/routing/build-url.d.ts +19 -16
- package/dist/routing/build-url.d.ts.map +1 -1
- package/dist/routing/build-url.js +62 -59
- package/dist/routing/build-url.js.map +1 -1
- package/dist/routing/derive-current-route.d.ts +42 -36
- package/dist/routing/derive-current-route.d.ts.map +1 -1
- package/dist/routing/derive-current-route.js +57 -49
- package/dist/routing/derive-current-route.js.map +1 -1
- package/dist/routing/derive-initial-route.d.ts +23 -20
- package/dist/routing/derive-initial-route.d.ts.map +1 -1
- package/dist/routing/derive-initial-route.js +27 -24
- package/dist/routing/derive-initial-route.js.map +1 -1
- package/dist/routing/derive-route.d.ts +38 -37
- package/dist/routing/derive-route.d.ts.map +1 -1
- package/dist/routing/derive-route.js +45 -42
- package/dist/routing/derive-route.js.map +1 -1
- package/dist/routing/format-play-route-transitions.d.ts +34 -28
- package/dist/routing/format-play-route-transitions.d.ts.map +1 -1
- package/dist/routing/format-play-route-transitions.js +32 -28
- package/dist/routing/format-play-route-transitions.js.map +1 -1
- package/dist/routing/index.d.ts +3 -3
- package/dist/routing/index.js +3 -3
- package/dist/routing/types.d.ts +12 -11
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/types.d.ts +64 -60
- package/dist/types.d.ts.map +1 -1
- package/dist/view/derive-current-view.d.ts +42 -40
- package/dist/view/derive-current-view.d.ts.map +1 -1
- package/dist/view/derive-current-view.js +51 -47
- package/dist/view/derive-current-view.js.map +1 -1
- package/package.json +7 -6
package/dist/player-actor.js
CHANGED
|
@@ -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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* runtime has it (Node >= 24,
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
25
|
+
* Normalizes a failure of the actor for `onError`.
|
|
24
26
|
*
|
|
25
|
-
*
|
|
26
|
-
* identity
|
|
27
|
-
*
|
|
28
|
-
* becomes a
|
|
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
|
-
//
|
|
38
|
-
// traps
|
|
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
|
-
*
|
|
47
|
+
* The structural equality of two derived view specs, with a limit on its depth.
|
|
44
48
|
*
|
|
45
|
-
*
|
|
46
|
-
* `elements`, then
|
|
47
|
-
* `Object.is`.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
-
//
|
|
62
|
-
// reference
|
|
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
|
-
*
|
|
84
|
-
* snapshot
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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
|
-
*
|
|
97
|
+
* The concrete XState actor. It implements the signal protocol of the Play Architecture
|
|
91
98
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* XState
|
|
97
|
-
*
|
|
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:**
|
|
100
|
-
* {@link @xmachines/play-actor!
|
|
101
|
-
*
|
|
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
|
|
104
|
-
* XState machine
|
|
105
|
-
* the actor
|
|
106
|
-
*
|
|
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
|
|
118
|
+
* @typeParam TMachine - The type of the XState v5 state machine
|
|
109
119
|
*
|
|
110
120
|
* @example
|
|
111
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
* //
|
|
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
|
|
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
|
-
* **
|
|
166
|
-
*
|
|
167
|
-
* `meta.route
|
|
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
|
-
* **
|
|
170
|
-
* `Signal.
|
|
171
|
-
*
|
|
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
|
|
188
|
+
* The live options object of the caller, or an empty object during the construction.
|
|
177
189
|
*
|
|
178
|
-
* XState
|
|
179
|
-
*
|
|
180
|
-
* any field here
|
|
181
|
-
* that window
|
|
182
|
-
*
|
|
183
|
-
* read
|
|
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
|
|
186
|
-
* This target compiles class
|
|
187
|
-
* after `super()` returns
|
|
188
|
-
*
|
|
189
|
-
* emits nothing,
|
|
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
|
|
196
|
-
*
|
|
197
|
-
* snapshot
|
|
198
|
-
* reference
|
|
199
|
-
* snapshot
|
|
200
|
-
*
|
|
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
|
|
215
|
+
// The requirements of the AbstractActor protocol
|
|
204
216
|
state;
|
|
205
217
|
/**
|
|
206
|
-
*
|
|
218
|
+
* Tells you if the current state of the actor accepts the given event.
|
|
207
219
|
*
|
|
208
|
-
*
|
|
209
|
-
* compile error.
|
|
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
|
-
//
|
|
218
|
-
//
|
|
219
|
-
// construction window, where
|
|
220
|
-
//
|
|
221
|
-
//
|
|
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
|
|
227
|
-
* machine state
|
|
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
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* is caught
|
|
232
|
-
*
|
|
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
|
-
* //
|
|
238
|
-
* // and null while the param is still
|
|
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
|
|
245
|
-
* never changes
|
|
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
|
-
*
|
|
248
|
-
* (
|
|
249
|
-
* different
|
|
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
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* `meta.route` templates are fixed at
|
|
254
|
-
* substitution uses the
|
|
255
|
-
*
|
|
256
|
-
* value
|
|
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
|
-
*
|
|
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
|
-
*
|
|
264
|
-
* a different state
|
|
265
|
-
* spec
|
|
266
|
-
*
|
|
267
|
-
*
|
|
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
|
|
270
|
-
* `state` under the read-only `/context` subtree
|
|
271
|
-
*
|
|
272
|
-
* (`{ $state: "/context/params/section" }`).
|
|
273
|
-
*
|
|
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
|
-
*
|
|
291
|
+
* The signal returns `null` when the current state has no `meta.view` metadata.
|
|
276
292
|
*
|
|
277
|
-
* Two states
|
|
278
|
-
*
|
|
279
|
-
* provider
|
|
280
|
-
*
|
|
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); //
|
|
287
|
-
* console.log(view.elements); // @xmachines/json-render-core
|
|
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
|
-
//
|
|
294
|
-
//
|
|
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
|
|
299
|
-
// XState
|
|
300
|
-
//
|
|
301
|
-
// Actor member
|
|
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
|
|
304
|
-
//
|
|
305
|
-
// TMachine. The cast stays inside the
|
|
306
|
-
//
|
|
307
|
-
// compile
|
|
308
|
-
//
|
|
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
|
|
315
|
-
// always the
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
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.
|
|
327
|
-
// XState
|
|
328
|
-
//
|
|
329
|
-
//
|
|
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
|
|
337
|
-
// next-only
|
|
338
|
-
// subscriptions
|
|
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
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
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
|
-
//
|
|
364
|
+
// Each state update is synchronous. Therefore a router bridge sees a guard redirect at once.
|
|
346
365
|
this.state.set(snapshot);
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
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
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
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
|
|
374
|
-
//
|
|
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
|
-
//
|
|
382
|
-
//
|
|
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
|
-
*
|
|
407
|
+
* Starts the actor.
|
|
387
408
|
*
|
|
388
|
-
*
|
|
389
|
-
* running
|
|
390
|
-
* `start()`
|
|
391
|
-
* while
|
|
392
|
-
*
|
|
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
|
|
396
|
-
//
|
|
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
|
-
*
|
|
432
|
+
* Stops the actor and cleans up.
|
|
412
433
|
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
* against
|
|
417
|
-
* actor for
|
|
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
|
|
422
|
-
//
|
|
423
|
-
// internal subscription
|
|
424
|
-
//
|
|
425
|
-
//
|
|
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
|
-
*
|
|
460
|
+
* Sends an event to this actor.
|
|
440
461
|
*
|
|
441
|
-
* The
|
|
442
|
-
*
|
|
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
|
|
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
|
|
447
|
-
* a string, number
|
|
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
|
-
* //
|
|
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
|
-
* //
|
|
477
|
+
* // A routing event
|
|
455
478
|
* actor.send({ type: "play.route", to: "#home" });
|
|
456
479
|
* ```
|
|
457
480
|
*/
|
|
458
481
|
send(event) {
|
|
459
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
470
|
-
// the options
|
|
471
|
-
//
|
|
472
|
-
// the correct
|
|
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`
|
|
476
|
-
// event type, and TypeScript forbids super
|
|
477
|
-
//
|
|
478
|
-
// is exactly the call `super.send(event)
|
|
479
|
-
// emits the @xstate.event inspection event and
|
|
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
|
-
*
|
|
513
|
+
* Returns the current snapshot
|
|
490
514
|
*/
|
|
491
515
|
getSnapshot() {
|
|
492
516
|
return super.getSnapshot();
|
|
493
517
|
}
|
|
494
518
|
subscribe(nextListenerOrObserver, errorListener, completeListener) {
|
|
495
|
-
//
|
|
496
|
-
//
|
|
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
|
-
//
|
|
499
|
-
// globally
|
|
500
|
-
//
|
|
501
|
-
//
|
|
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
|
-
*
|
|
545
|
+
* Listens for the events that this actor emits with the `emit` action.
|
|
522
546
|
*
|
|
523
|
-
* @param type -
|
|
524
|
-
* @param handler -
|
|
525
|
-
* @returns
|
|
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
|
-
*
|
|
555
|
+
* Returns the persisted snapshot of this actor.
|
|
532
556
|
*
|
|
533
|
-
*
|
|
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
|
-
*
|
|
542
|
-
*
|
|
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 -
|
|
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
|
|
554
|
-
// returns a fresh object
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
// IS the
|
|
561
|
-
// registers currentView as a dependency of a
|
|
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
|
-
//
|
|
564
|
-
// projection
|
|
565
|
-
//
|
|
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
|
|
603
|
+
// On an error: keep the last valid view, and clear it not
|
|
578
604
|
}
|
|
579
605
|
}
|
|
580
606
|
/**
|
|
581
|
-
*
|
|
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();
|