@defold-typescript/transpiler 0.47.0 → 0.49.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/dist/hsm/debug.ts +36 -8
- package/dist/hsm/index.ts +1310 -286
- package/package.json +2 -2
package/dist/hsm/index.ts
CHANGED
|
@@ -17,24 +17,33 @@ export type MoveCause = "event" | "after" | "update" | "stop" | "reload" | "alwa
|
|
|
17
17
|
/**
|
|
18
18
|
* A listener given to `onMove`. It gets the leaf left, the leaf entered (`undefined` after
|
|
19
19
|
* `stop()`), the cause, and the event, which is `undefined` unless the cause is `event`. It runs
|
|
20
|
-
* after the move's `enter` hooks.
|
|
20
|
+
* after the move's `enter` hooks. The event is read-only: the listener cannot assign to its
|
|
21
|
+
* fields.
|
|
21
22
|
* @noSelf
|
|
22
23
|
*/
|
|
23
24
|
export type MoveListener<E extends EventObject, P extends string = string> = (
|
|
24
25
|
from: P,
|
|
25
26
|
to: P | undefined,
|
|
26
27
|
cause: MoveCause,
|
|
27
|
-
event: E | undefined,
|
|
28
|
+
event: Readonly<E> | undefined,
|
|
28
29
|
) => void;
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
32
|
+
* A listener given to `onUnhandled`. It gets the event no rule of an active state took. The
|
|
33
|
+
* event is read-only: the listener cannot assign to its fields.
|
|
33
34
|
* @noSelf
|
|
34
35
|
*/
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
export type UnhandledListener<E extends EventObject> = (event: Readonly<E>) => void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What a `when` sees of the running machine: which states are active, and nothing that changes
|
|
40
|
+
* it. `P` is the union of the paths it takes. The `machine` a `when` receives takes the machine's
|
|
41
|
+
* state paths, and also any path below a state that holds only callbacks with unannotated
|
|
42
|
+
* parameters and `after` or `every` lists with one in them, or whose child states all do. `Tag`
|
|
43
|
+
* is the union of the machine's tag names.
|
|
44
|
+
* @noSelf
|
|
45
|
+
*/
|
|
46
|
+
export interface MachineView<P extends string = string, Tag extends string = never> {
|
|
38
47
|
/**
|
|
39
48
|
* The full path of the deepest active state, or `undefined` once stopped. In a parallel state
|
|
40
49
|
* it is the first region's leaf.
|
|
@@ -45,17 +54,54 @@ export interface MachineInstance<Ctx, E extends EventObject, P extends string =
|
|
|
45
54
|
* once stopped. It is the same array on every read, updated in place.
|
|
46
55
|
*/
|
|
47
56
|
readonly leaves: readonly P[];
|
|
57
|
+
// Read off a method signature, which TypeScript compares bivariantly: a machine with narrower
|
|
58
|
+
// paths fits a parameter typed with wider ones, so a hook typed `StateHook<Ctx, E>` fits its
|
|
59
|
+
// slot. `this: void` repeats on the signature what `@noSelf` says of the interface: a call
|
|
60
|
+
// passes no self in Lua.
|
|
48
61
|
/** Whether the state at the full path `path` is active. Ancestors of the current state count. */
|
|
49
|
-
readonly matches: (path: P)
|
|
62
|
+
readonly matches: { matches(this: void, path: P): boolean }["matches"];
|
|
63
|
+
/**
|
|
64
|
+
* Whether an active state carries `tag`. A parent's tag holds while any of its children is
|
|
65
|
+
* active, and a tag in any region of a parallel state counts. The answer is `false` for a name
|
|
66
|
+
* no state carries, and once stopped.
|
|
67
|
+
*/
|
|
68
|
+
readonly hasTag: (tag: Tag) => boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One running machine, returned by `start`. `P` is the union of the paths it takes: the
|
|
73
|
+
* machine's state paths on a started instance. The `machine` a hook, a `run` or a `task`
|
|
74
|
+
* receives takes those paths, and also any path below a state that holds only callbacks with
|
|
75
|
+
* unannotated parameters and `after` or `every` lists with one in them, or whose child states all
|
|
76
|
+
* do. `Tag` is the union of the machine's tag names.
|
|
77
|
+
* @noSelf
|
|
78
|
+
*/
|
|
79
|
+
export interface MachineInstance<
|
|
80
|
+
Ctx,
|
|
81
|
+
E extends EventObject,
|
|
82
|
+
P extends string = string,
|
|
83
|
+
Tag extends string = never,
|
|
84
|
+
> extends MachineView<P, Tag> {
|
|
85
|
+
/** The context passed to `start`. */
|
|
86
|
+
readonly ctx: Ctx;
|
|
87
|
+
/**
|
|
88
|
+
* Whether a rule of an active state would take `event` if it were sent at that moment. The
|
|
89
|
+
* answer is for that one event, payload included: a rule's `when` reads it. A rule with no
|
|
90
|
+
* `to` counts. No hook, `run` or listener runs and nothing is queued, and once the machine is
|
|
91
|
+
* stopped the answer is `false`.
|
|
92
|
+
*/
|
|
93
|
+
readonly can: (event: E) => boolean;
|
|
50
94
|
/**
|
|
51
95
|
* Handles `event`, or queues it when called during a step. An event no active state handles is
|
|
52
|
-
* dropped
|
|
96
|
+
* dropped: it runs the active states' `unhandled` hooks and is then reported to the
|
|
97
|
+
* `onUnhandled` listeners. Once the machine is stopped the call does nothing.
|
|
53
98
|
*/
|
|
54
99
|
readonly send: (event: E) => void;
|
|
55
100
|
/**
|
|
56
|
-
* Advances the machine by `dt` seconds: runs the `update` hooks, then
|
|
57
|
-
*
|
|
58
|
-
* is
|
|
101
|
+
* Advances the machine by `dt` seconds: runs the `update` hooks, then, in every region no hook
|
|
102
|
+
* moved, each state's `every` entries and after them its `after` timers. A machine with no
|
|
103
|
+
* parallel state is one region. A machine whose `update` is not called is frozen in time, and
|
|
104
|
+
* once the machine is stopped the call does nothing.
|
|
59
105
|
*/
|
|
60
106
|
readonly update: (dt: number) => void;
|
|
61
107
|
/**
|
|
@@ -70,17 +116,53 @@ export interface MachineInstance<Ctx, E extends EventObject, P extends string =
|
|
|
70
116
|
* function removes that one.
|
|
71
117
|
*/
|
|
72
118
|
readonly onMove: (listener: MoveListener<E, P>) => () => void;
|
|
119
|
+
/**
|
|
120
|
+
* Adds a listener called with each event the machine processes that no rule of an active state
|
|
121
|
+
* takes. It runs inside the step, after the active states' `unhandled` hooks and before the
|
|
122
|
+
* next queued event. A rule with no `to` takes its event. Each call adds a listener, and the
|
|
123
|
+
* returned function removes that one.
|
|
124
|
+
*/
|
|
125
|
+
readonly onUnhandled: (listener: UnhandledListener<E>) => () => void;
|
|
73
126
|
}
|
|
74
127
|
|
|
75
128
|
/**
|
|
76
129
|
* A function a rule's `run` calls during the move, after the `exit` hooks and before the `enter`
|
|
77
|
-
* hooks. Its `event` is the variant the `on` key names
|
|
130
|
+
* hooks. Its `event` is the variant the `on` key names, and is read-only: a `run` changes `ctx`
|
|
131
|
+
* and cannot assign to a field of the event. `Hook` is the union of the paths its `machine`
|
|
132
|
+
* takes: the machine's paths for a `run` written in the config, and any `string` when left out.
|
|
78
133
|
*/
|
|
79
|
-
export type MoveAction<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
134
|
+
export type MoveAction<
|
|
135
|
+
Ctx,
|
|
136
|
+
E extends EventObject,
|
|
137
|
+
V extends E = E,
|
|
138
|
+
Tag extends string = never,
|
|
139
|
+
Hook extends string = string,
|
|
140
|
+
> = (ctx: Ctx, event: Readonly<V>, machine: MachineInstance<Ctx, E, Hook, Tag>) => void;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A function a `run` calls where there is no event: under `after`, `always` and `every`. `Hook`
|
|
144
|
+
* is the union of the paths its `machine` takes: the machine's paths for a `run` written in the
|
|
145
|
+
* config, and any `string` when left out.
|
|
146
|
+
*/
|
|
147
|
+
export type RunAction<
|
|
148
|
+
Ctx,
|
|
149
|
+
E extends EventObject,
|
|
150
|
+
Tag extends string = never,
|
|
151
|
+
Hook extends string = string,
|
|
152
|
+
> = (ctx: Ctx, machine: MachineInstance<Ctx, E, Hook, Tag>) => void;
|
|
153
|
+
|
|
154
|
+
// A rule typed without the machine's event type has no run. The default cannot be EventObject:
|
|
155
|
+
// MachineInstance is invariant in E, so such a rule would not fit a machine with narrower events.
|
|
156
|
+
// That makes the rule types invariant in E too, and TypeScript does not compare two instances of
|
|
157
|
+
// one type by structure, so each slot lists the rule typed without E beside the one typed with it.
|
|
158
|
+
type RuleRun<
|
|
159
|
+
Ctx,
|
|
160
|
+
E extends EventObject,
|
|
161
|
+
Tag extends string = never,
|
|
162
|
+
Hook extends string = string,
|
|
163
|
+
> = [E] extends [never]
|
|
164
|
+
? never
|
|
165
|
+
: RunAction<Ctx, E, Tag, Hook> | readonly RunAction<Ctx, E, Tag, Hook>[];
|
|
84
166
|
|
|
85
167
|
// A list with a first entry: Lua cannot tell [] from {}, so an empty list would compile to a
|
|
86
168
|
// transition with no to. It is an object type, not a tuple, because TypeScript reports a bad
|
|
@@ -101,6 +183,8 @@ export interface TransitionConfig<
|
|
|
101
183
|
E extends EventObject,
|
|
102
184
|
V extends E = E,
|
|
103
185
|
To extends string = string,
|
|
186
|
+
Tag extends string = never,
|
|
187
|
+
Hook extends string = string,
|
|
104
188
|
> {
|
|
105
189
|
/**
|
|
106
190
|
* The full path of the state to move to. A `to` naming the state the rule is on exits and
|
|
@@ -110,15 +194,22 @@ export interface TransitionConfig<
|
|
|
110
194
|
/**
|
|
111
195
|
* The rule is taken only when this returns `true`. It must have no side effects: it is also
|
|
112
196
|
* checked when the move does not happen, in guarded lists and on every `always` recheck.
|
|
197
|
+
* `ctx` and `event` are read-only here, one level deep: a write belongs in `run`.
|
|
198
|
+
* `machine` reads the active states; inside a parallel state it shows the regions that already
|
|
199
|
+
* took the event.
|
|
113
200
|
*/
|
|
114
|
-
readonly when?: (
|
|
201
|
+
readonly when?: (
|
|
202
|
+
ctx: Readonly<Ctx>,
|
|
203
|
+
event: Readonly<V>,
|
|
204
|
+
machine: MachineView<Hook, Tag>,
|
|
205
|
+
) => boolean;
|
|
115
206
|
/**
|
|
116
207
|
* Runs during the move. The event is accepted when its key matches and `when` passes; then
|
|
117
208
|
* `exit` runs on each state left, deepest first; then `run`; then `enter` on each state
|
|
118
209
|
* entered, outermost first. Shared ancestors neither exit nor enter. A rule with no `to` runs
|
|
119
210
|
* only `run`.
|
|
120
211
|
*/
|
|
121
|
-
readonly run?: MoveAction<Ctx, E, V> | readonly MoveAction<Ctx, E, V>[];
|
|
212
|
+
readonly run?: MoveAction<Ctx, E, V, Tag, Hook> | readonly MoveAction<Ctx, E, V, Tag, Hook>[];
|
|
122
213
|
/** Renamed to `to`. */
|
|
123
214
|
readonly target?: never;
|
|
124
215
|
/** Renamed to `when`. */
|
|
@@ -138,21 +229,39 @@ export type TransitionSpec<
|
|
|
138
229
|
E extends EventObject,
|
|
139
230
|
V extends E = E,
|
|
140
231
|
To extends string = string,
|
|
141
|
-
|
|
232
|
+
Tag extends string = never,
|
|
233
|
+
Hook extends string = string,
|
|
234
|
+
> =
|
|
235
|
+
| To
|
|
236
|
+
| TransitionConfig<Ctx, E, V, To, Tag, Hook>
|
|
237
|
+
| RuleList<TransitionConfig<Ctx, E, V, To, Tag, Hook>>;
|
|
142
238
|
|
|
143
239
|
/**
|
|
144
|
-
* The object form of an `always` rule: a `to` and an optional `
|
|
145
|
-
* no `
|
|
240
|
+
* The object form of an `always` rule: a `to`, an optional `when` and an optional `run`. It gets
|
|
241
|
+
* no event. `E` is the machine's event type; a rule typed without it has no `run`. `Hook` is
|
|
242
|
+
* the union of the paths the `machine` of its `when` and `run` takes: any `string` when left out.
|
|
146
243
|
* @noSelf
|
|
147
244
|
*/
|
|
148
|
-
export interface AlwaysConfig<
|
|
245
|
+
export interface AlwaysConfig<
|
|
246
|
+
Ctx,
|
|
247
|
+
To extends string = string,
|
|
248
|
+
E extends EventObject = never,
|
|
249
|
+
Tag extends string = never,
|
|
250
|
+
Hook extends string = string,
|
|
251
|
+
> {
|
|
149
252
|
/**
|
|
150
253
|
* The full path of the state to move to. An `always` rule without one throws when the machine
|
|
151
254
|
* is defined.
|
|
152
255
|
*/
|
|
153
256
|
readonly to: To;
|
|
154
|
-
/**
|
|
155
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Checked on every recheck, so it must have no side effects. `ctx` is read-only here, one
|
|
259
|
+
* level deep: a write belongs in `run`. `machine` reads the active states; inside a parallel
|
|
260
|
+
* state it shows the regions that already moved.
|
|
261
|
+
*/
|
|
262
|
+
readonly when?: (ctx: Readonly<Ctx>, machine: MachineView<Hook, Tag>) => boolean;
|
|
263
|
+
/** Runs between the `exit` and `enter` hooks of the move, only when the rule is taken. */
|
|
264
|
+
readonly run?: RuleRun<Ctx, E, Tag, Hook>;
|
|
156
265
|
/** Renamed to `to`. */
|
|
157
266
|
readonly target?: never;
|
|
158
267
|
/** Renamed to `when`. */
|
|
@@ -163,64 +272,221 @@ export interface AlwaysConfig<Ctx, To extends string = string> {
|
|
|
163
272
|
* What `always` takes: a target path, an `AlwaysConfig`, or a non-empty list of them. The first
|
|
164
273
|
* whose `when` is missing or passes is taken.
|
|
165
274
|
*/
|
|
166
|
-
export type AlwaysSpec<
|
|
275
|
+
export type AlwaysSpec<
|
|
276
|
+
Ctx,
|
|
277
|
+
To extends string = string,
|
|
278
|
+
E extends EventObject = never,
|
|
279
|
+
Tag extends string = never,
|
|
280
|
+
Hook extends string = string,
|
|
281
|
+
> =
|
|
167
282
|
| To
|
|
168
|
-
| AlwaysConfig<Ctx, To>
|
|
169
|
-
|
|
|
283
|
+
| AlwaysConfig<Ctx, To, E, Tag, Hook>
|
|
284
|
+
| AlwaysConfig<Ctx, To, never, Tag, Hook>
|
|
285
|
+
| RuleList<AlwaysConfig<Ctx, To, E, Tag, Hook> | AlwaysConfig<Ctx, To, never, Tag, Hook>>;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The object form of a rule on an `after` timer: a `to`, a `when` and a `run`, each optional. A
|
|
289
|
+
* rule with neither a `to` nor a `run` throws when the machine is defined. `E` is the machine's
|
|
290
|
+
* event type; a rule typed without it has no `run`. `Hook` is the union of the paths the
|
|
291
|
+
* `machine` of its `when` and `run` takes: any `string` when left out.
|
|
292
|
+
* @noSelf
|
|
293
|
+
*/
|
|
294
|
+
export interface AfterRule<
|
|
295
|
+
Ctx,
|
|
296
|
+
To extends string = string,
|
|
297
|
+
E extends EventObject = never,
|
|
298
|
+
Tag extends string = never,
|
|
299
|
+
Hook extends string = string,
|
|
300
|
+
> {
|
|
301
|
+
/**
|
|
302
|
+
* The full path of the state to move to when the timer fires. A rule without one runs its
|
|
303
|
+
* `run` and nothing else: no state exits or enters, and it is not a move.
|
|
304
|
+
*/
|
|
305
|
+
readonly to?: To;
|
|
306
|
+
/**
|
|
307
|
+
* The timer fires only when this returns `true`. It is first checked once the delay has
|
|
308
|
+
* passed, then on every `update` call until it passes or the state is left, so it must have no
|
|
309
|
+
* side effects. `ctx` is read-only here, one level deep: a write belongs in `run`. `machine`
|
|
310
|
+
* reads the active states.
|
|
311
|
+
*/
|
|
312
|
+
readonly when?: (ctx: Readonly<Ctx>, machine: MachineView<Hook, Tag>) => boolean;
|
|
313
|
+
/**
|
|
314
|
+
* Runs when the timer fires: between the `exit` and `enter` hooks of the move, or on its own
|
|
315
|
+
* for a rule with no `to`.
|
|
316
|
+
*/
|
|
317
|
+
readonly run?: RuleRun<Ctx, E, Tag, Hook>;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* One entry of the list form of `after`: a `delay` beside the fields of an `AfterRule`.
|
|
322
|
+
* @noSelf
|
|
323
|
+
*/
|
|
324
|
+
export interface AfterConfig<
|
|
325
|
+
Ctx,
|
|
326
|
+
To extends string = string,
|
|
327
|
+
E extends EventObject = never,
|
|
328
|
+
Tag extends string = never,
|
|
329
|
+
Hook extends string = string,
|
|
330
|
+
> extends AfterRule<Ctx, To, E, Tag, Hook> {
|
|
331
|
+
/**
|
|
332
|
+
* The delay in seconds of game time: a number, or a function of the context that returns one.
|
|
333
|
+
* A function is read once each time the state is entered, after its `enter` hook, and again on
|
|
334
|
+
* a hot reload, so the delay is fixed for that stay. The function only reads the context: it
|
|
335
|
+
* cannot assign to a field of `ctx`. A result that is not a non-negative number throws, naming
|
|
336
|
+
* the state.
|
|
337
|
+
*/
|
|
338
|
+
readonly delay: number | ((ctx: Readonly<Ctx>) => number);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* One entry of the list form of `every`: a `delay` and the `run` it repeats.
|
|
343
|
+
* @noSelf
|
|
344
|
+
*/
|
|
345
|
+
export interface EveryConfig<
|
|
346
|
+
Ctx,
|
|
347
|
+
E extends EventObject,
|
|
348
|
+
Tag extends string = never,
|
|
349
|
+
Hook extends string = string,
|
|
350
|
+
> {
|
|
351
|
+
/**
|
|
352
|
+
* The seconds of game time between two runs: a number, or a function of the context that
|
|
353
|
+
* returns one. A function is read once each time the state is entered, after its `enter` hook,
|
|
354
|
+
* and again on a hot reload, so the delay is fixed for that stay. The function only reads the
|
|
355
|
+
* context: it cannot assign to a field of `ctx`. A result that is not a positive number
|
|
356
|
+
* throws, naming the state.
|
|
357
|
+
*/
|
|
358
|
+
readonly delay: number | ((ctx: Readonly<Ctx>) => number);
|
|
359
|
+
/** Runs each time the delay passes: one function, or a list run in order. */
|
|
360
|
+
readonly run: RunAction<Ctx, E, Tag, Hook> | readonly RunAction<Ctx, E, Tag, Hook>[];
|
|
361
|
+
/** Not taken here: an `after` timer leaves the state. */
|
|
362
|
+
readonly to?: never;
|
|
363
|
+
/** Not taken here: a `run` tests a condition itself. */
|
|
364
|
+
readonly when?: never;
|
|
365
|
+
}
|
|
170
366
|
|
|
171
367
|
/**
|
|
172
368
|
* A state's rules by event `type`. Only the machine's event types are keys, and each rule's
|
|
173
369
|
* `when` and `run` see the event variant its key names.
|
|
174
370
|
*/
|
|
175
|
-
export type OnConfig<
|
|
176
|
-
|
|
371
|
+
export type OnConfig<
|
|
372
|
+
Ctx,
|
|
373
|
+
E extends EventObject,
|
|
374
|
+
To extends string = string,
|
|
375
|
+
Tag extends string = never,
|
|
376
|
+
Hook extends string = string,
|
|
377
|
+
> = {
|
|
378
|
+
readonly [K in E["type"]]?: TransitionSpec<Ctx, E, Extract<E, { type: K }>, To, Tag, Hook>;
|
|
177
379
|
};
|
|
178
380
|
|
|
179
381
|
/**
|
|
180
382
|
* Runs on every way in (`enter`) or out (`exit`) of the state: a move, `start` and `stop`. A hot
|
|
181
383
|
* reload enters the states it adds, but drops a removed state without its `exit`. It gets no
|
|
182
|
-
* event; event data reaches `enter` through `ctx`, set in `run`.
|
|
384
|
+
* event; event data reaches `enter` through `ctx`, set in `run`. `Hook` is the union of the
|
|
385
|
+
* paths its `machine` takes: the machine's paths for a hook written in the config, and any
|
|
386
|
+
* `string` when left out.
|
|
183
387
|
*/
|
|
184
|
-
export type StateHook<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
388
|
+
export type StateHook<
|
|
389
|
+
Ctx,
|
|
390
|
+
E extends EventObject,
|
|
391
|
+
Tag extends string = never,
|
|
392
|
+
Hook extends string = string,
|
|
393
|
+
> = (ctx: Ctx, machine: MachineInstance<Ctx, E, Hook, Tag>) => void;
|
|
188
394
|
|
|
189
395
|
/**
|
|
190
396
|
* Runs on every `update(dt)` while the state is active, deepest state first. It returns a full
|
|
191
397
|
* path to move there, or `undefined` to pass. The first hook to return a path wins: the hooks
|
|
192
|
-
* above it and the `after` timers do not run on that call, and the move has no
|
|
398
|
+
* above it and the `after` timers of its region do not run on that call, and the move has no
|
|
399
|
+
* `run`. Under a parallel state the other regions still run their hooks and timers, unless the
|
|
400
|
+
* move left the parallel state or entered it again. `Hook` is the union of the paths its
|
|
401
|
+
* `machine` takes: the machine's paths for a hook written in the config, and any `string` when
|
|
402
|
+
* left out.
|
|
193
403
|
*/
|
|
194
|
-
export type UpdateHook<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
404
|
+
export type UpdateHook<
|
|
405
|
+
Ctx,
|
|
406
|
+
E extends EventObject,
|
|
407
|
+
To extends string = string,
|
|
408
|
+
Tag extends string = never,
|
|
409
|
+
Hook extends string = string,
|
|
410
|
+
> = (ctx: Ctx, dt: number, machine: MachineInstance<Ctx, E, Hook, Tag>) => To | undefined;
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Runs when the machine drops an event while the state is active: for the events `onUnhandled`
|
|
414
|
+
* reports, and no others. Every active state that has the hook runs it, deepest first, in the
|
|
415
|
+
* order `update` hooks run. `event` is the machine's whole event union, and is read-only: the
|
|
416
|
+
* hook cannot assign to its fields. The hook moves nothing: a `send` from it queues, and a
|
|
417
|
+
* `stop()` ends the step once every hook and listener has run for the event. `Hook` is the
|
|
418
|
+
* union of the paths its `machine` takes: the machine's paths for a hook written in the config,
|
|
419
|
+
* and any `string` when left out.
|
|
420
|
+
*/
|
|
421
|
+
export type UnhandledHook<
|
|
422
|
+
Ctx,
|
|
423
|
+
E extends EventObject,
|
|
424
|
+
Tag extends string = never,
|
|
425
|
+
Hook extends string = string,
|
|
426
|
+
> = (ctx: Ctx, event: Readonly<E>, machine: MachineInstance<Ctx, E, Hook, Tag>) => void;
|
|
199
427
|
|
|
200
428
|
/**
|
|
201
429
|
* Starts work that lives as long as the state. `finish` sends one event and is ignored after the
|
|
202
|
-
* first call or once the state is left. The returned cleanup runs on every way out.
|
|
430
|
+
* first call or once the state is left. The returned cleanup runs on every way out. A task that
|
|
431
|
+
* starts several callbacks counts them with `waitGroup`. `Hook` is the union of the paths its
|
|
432
|
+
* `machine` takes: the machine's paths for a task written in the config, and any `string` when
|
|
433
|
+
* left out.
|
|
203
434
|
* @noSelf
|
|
204
435
|
*/
|
|
205
|
-
export type TaskStart<
|
|
436
|
+
export type TaskStart<
|
|
437
|
+
Ctx,
|
|
438
|
+
E extends EventObject,
|
|
439
|
+
Tag extends string = never,
|
|
440
|
+
Hook extends string = string,
|
|
441
|
+
> = (
|
|
206
442
|
ctx: Ctx,
|
|
207
443
|
finish: (event: E) => void,
|
|
208
|
-
machine: MachineInstance<Ctx, E>,
|
|
444
|
+
machine: MachineInstance<Ctx, E, Hook, Tag>,
|
|
209
445
|
// biome-ignore lint/suspicious/noConfusingVoidType: `void` keeps an expression-body task returning a void call (`=> go.animate(...)`) valid.
|
|
210
446
|
) => (() => void) | void;
|
|
211
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Counts several pieces of engine work a state starts and waits for, returned by `waitGroup()`.
|
|
450
|
+
* It knows nothing about a machine: built inside a `task`, it is new for each entry of the
|
|
451
|
+
* state.
|
|
452
|
+
* @noSelf
|
|
453
|
+
*/
|
|
454
|
+
export interface WaitGroup {
|
|
455
|
+
/**
|
|
456
|
+
* Records one pending piece of work and returns the function that reports it finished. That
|
|
457
|
+
* function counts once, and a later call does nothing. It ignores its arguments, so an engine
|
|
458
|
+
* function takes it as its callback, as in `http.request(url, "GET", group.add())`.
|
|
459
|
+
*/
|
|
460
|
+
readonly add: () => () => void;
|
|
461
|
+
/**
|
|
462
|
+
* Calls `callback` once when no added work is pending: at once when none is pending at the
|
|
463
|
+
* call, and otherwise inside the report that finishes the last pending piece. Each call
|
|
464
|
+
* registers one callback, and several run in the order registered. A `wait` made before the
|
|
465
|
+
* first `add` runs at once, so it goes after them.
|
|
466
|
+
*/
|
|
467
|
+
readonly wait: (callback: () => void) => void;
|
|
468
|
+
}
|
|
469
|
+
|
|
212
470
|
/**
|
|
213
471
|
* A machine definition, returned by the second `defineMachine` call. Its `start` creates the
|
|
214
472
|
* instances that run.
|
|
215
473
|
* @noSelf
|
|
216
474
|
*/
|
|
217
|
-
export interface Machine<
|
|
475
|
+
export interface Machine<
|
|
476
|
+
Ctx,
|
|
477
|
+
E extends EventObject,
|
|
478
|
+
P extends string = string,
|
|
479
|
+
Tag extends string = never,
|
|
480
|
+
> {
|
|
218
481
|
/**
|
|
219
|
-
* Creates an instance with `ctx` as its context
|
|
220
|
-
*
|
|
221
|
-
*
|
|
482
|
+
* Creates an instance with `ctx` as its context. Without `at` it enters the root's `initial`
|
|
483
|
+
* chain. With `at`, a list of full state paths such as a copy of `leaves`, it enters toward
|
|
484
|
+
* each path: a level or region no path names goes through `initial`, a path the machine lacks
|
|
485
|
+
* falls back to its closest parent, and of two paths that cannot be active together the first
|
|
486
|
+
* wins. Timer progress and `restoreDepth` memory are not part of the list. Each call returns a
|
|
487
|
+
* separate instance, which has already handled the events its `enter` hooks sent.
|
|
222
488
|
*/
|
|
223
|
-
readonly start: (ctx: Ctx) => MachineInstance<Ctx, E, P>;
|
|
489
|
+
readonly start: (ctx: Ctx, at?: readonly P[]) => MachineInstance<Ctx, E, P, Tag>;
|
|
224
490
|
}
|
|
225
491
|
|
|
226
492
|
type PathDepth = [never, 0, 1, 2, 3];
|
|
@@ -240,19 +506,6 @@ type TreePathsBelow<T, D extends number> = {
|
|
|
240
506
|
|
|
241
507
|
type TreePath<T> = `/${TreePathsBelow<T, 4>}`;
|
|
242
508
|
|
|
243
|
-
// The paths a config slot accepts. TypeScript checks a config once before it types the
|
|
244
|
-
// callbacks, and a level whose states hold only callbacks has no inferred children in that
|
|
245
|
-
// check, so a node with no inferred children accepts any path below it.
|
|
246
|
-
type SlotPathsBelow<T, D extends number> = unknown extends T
|
|
247
|
-
? string
|
|
248
|
-
: {
|
|
249
|
-
[K in keyof T & string]:
|
|
250
|
-
| K
|
|
251
|
-
| `${K}/${D extends 1 ? string : SlotPathsBelow<T[K], PathDepth[D]>}`;
|
|
252
|
-
}[keyof T & string];
|
|
253
|
-
|
|
254
|
-
type SlotPath<T> = `/${SlotPathsBelow<T, 4>}`;
|
|
255
|
-
|
|
256
509
|
type TreeOf<C> = C extends { readonly states: infer Children }
|
|
257
510
|
? { readonly [K in keyof Children]: TreeOf<Children[K]> }
|
|
258
511
|
: unknown;
|
|
@@ -281,64 +534,142 @@ type TreeDepth<T, D extends number> = unknown extends T
|
|
|
281
534
|
: never
|
|
282
535
|
: never);
|
|
283
536
|
|
|
284
|
-
type AnyChild<
|
|
285
|
-
|
|
537
|
+
type AnyChild<
|
|
538
|
+
Ctx,
|
|
539
|
+
E extends EventObject,
|
|
540
|
+
T,
|
|
541
|
+
To extends string,
|
|
542
|
+
Tag extends string = never,
|
|
543
|
+
Hook extends string = string,
|
|
544
|
+
> = unknown extends T
|
|
545
|
+
? { readonly [name: string]: StateNode<Ctx, E, unknown, string, To, Tag, Hook> }
|
|
286
546
|
: unknown;
|
|
287
547
|
|
|
548
|
+
// A config is read into three trees. `T`, inferred from the `states` keys alone, types the
|
|
549
|
+
// started instance and each state's `initial` and `restoreDepth`. The node tree types the
|
|
550
|
+
// config slots, through `To`. The hook node tree types the callbacks, through `Hook`. The two
|
|
551
|
+
// node trees are intersected onto the root's `states` in `RootState`.
|
|
288
552
|
// `states` is the only slot that infers the tree; every other slot that mentions it is wrapped
|
|
289
553
|
// in NoInfer. `on`, `after` and `update` are wrapped whole so inference never reads them:
|
|
290
554
|
// without exactOptionalPropertyTypes, reading `on` asks for the paths while the tree is still
|
|
291
555
|
// being inferred, and a valid config fails as circular. `always` is wrapped only through `To`:
|
|
292
556
|
// NoInfer over a union that holds object types hides its string members from completion.
|
|
293
|
-
//
|
|
294
|
-
//
|
|
557
|
+
// `every` mentions no path and is wrapped whole all the same: where inference reads it, an
|
|
558
|
+
// unannotated callback in it, such as `delay: (ctx) => ctx.rate`, gets no parameter types.
|
|
559
|
+
// No callback parameter mentions `T`: one that did would make TypeScript settle the tree at the
|
|
560
|
+
// first such callback, before the states beside it are read. Callback parameters mention the
|
|
561
|
+
// hook node tree, through `Hook`: a second node tree inferred from the same config, which
|
|
562
|
+
// settles at the first callback and types nothing else.
|
|
563
|
+
// `tags` mentions only `Tag`, which the first `defineMachine` call fixes and nothing infers, so
|
|
564
|
+
// it stays outside NoInfer.
|
|
295
565
|
/** @noSelf */
|
|
296
|
-
interface StateBase<
|
|
566
|
+
interface StateBase<
|
|
567
|
+
Ctx,
|
|
568
|
+
E extends EventObject,
|
|
569
|
+
T,
|
|
570
|
+
Self extends string,
|
|
571
|
+
To extends string,
|
|
572
|
+
Tag extends string = never,
|
|
573
|
+
Hook extends string = string,
|
|
574
|
+
> {
|
|
297
575
|
/** Child states, by name. A name is non-empty and contains no `/`. */
|
|
298
576
|
readonly states?: {
|
|
299
|
-
readonly [K in keyof T]: StateNode<Ctx, E, T[K], `${Self}/${K & string}`, To>;
|
|
300
|
-
} & NoInfer<AnyChild<Ctx, E, T, To>>;
|
|
577
|
+
readonly [K in keyof T]: StateNode<Ctx, E, T[K], `${Self}/${K & string}`, To, Tag, Hook>;
|
|
578
|
+
} & NoInfer<AnyChild<Ctx, E, T, To, Tag, Hook>>;
|
|
579
|
+
/**
|
|
580
|
+
* Names from the machine's third type argument that hold while the state is active, read with
|
|
581
|
+
* `hasTag`.
|
|
582
|
+
*/
|
|
583
|
+
readonly tags?: readonly Tag[];
|
|
301
584
|
/**
|
|
302
585
|
* Rules by event `type`, each a target path, a rule object or a list of them. An event starts
|
|
303
586
|
* at the deepest active state and moves up to the parent while no rule takes it.
|
|
304
587
|
*/
|
|
305
|
-
readonly on?: NoInfer<OnConfig<Ctx, E, To>>;
|
|
588
|
+
readonly on?: NoInfer<OnConfig<Ctx, E, To, Tag, Hook>>;
|
|
589
|
+
/**
|
|
590
|
+
* Timers, in one of two forms: targets or `AfterRule` objects by delay in seconds of game
|
|
591
|
+
* time, such as `{ 2: "/idle" }`, or a list of `AfterConfig` entries, such as
|
|
592
|
+
* `[{ delay: (ctx) => ctx.patrolFor, to: "/rest" }]`, whose `delay` is a number or a function
|
|
593
|
+
* of the context. A delay function is read once each time the state is entered, after its
|
|
594
|
+
* `enter` hook, and again on a hot reload. The delay is counted by `update(dt)` while the state
|
|
595
|
+
* is active, from zero on each entry, and a timer fires at most once per stay. An `update`
|
|
596
|
+
* call tries the due timers in delay order: one whose `when` fails stays pending, one with no
|
|
597
|
+
* `to` runs its `run` and the call goes on, and the first that moves ends the timers of its
|
|
598
|
+
* region for that call. None is tried in a region an `update` hook moved on that call. Under a
|
|
599
|
+
* parallel state each region follows both rules on its own, and the parallel state's own
|
|
600
|
+
* timers are tried only when no region moved. A state's `every` entries run before its timers.
|
|
601
|
+
*/
|
|
602
|
+
readonly after?: NoInfer<
|
|
603
|
+
| {
|
|
604
|
+
readonly [seconds: number]:
|
|
605
|
+
| To
|
|
606
|
+
| AfterRule<Ctx, To, E, Tag, Hook>
|
|
607
|
+
| AfterRule<Ctx, To, never, Tag, Hook>;
|
|
608
|
+
}
|
|
609
|
+
| readonly (AfterConfig<Ctx, To, E, Tag, Hook> | AfterConfig<Ctx, To, never, Tag, Hook>)[]
|
|
610
|
+
>;
|
|
306
611
|
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
612
|
+
* Code that repeats while the state is active, in one of two forms: a `run` by delay in
|
|
613
|
+
* seconds of game time, such as `{ 0.5: (ctx) => burn(ctx) }`, or a list of `EveryConfig`
|
|
614
|
+
* entries, such as `[{ delay: (ctx) => ctx.fireEvery, run: fire }]`, whose `delay` is a number
|
|
615
|
+
* or a function of the context, read as an `after` delay is. A `run` is one function or a list
|
|
616
|
+
* run in order. An entry first runs one delay after the state is entered, then each time
|
|
617
|
+
* another delay has passed on the state's clock, which `update(dt)` advances while the state
|
|
618
|
+
* is active, from zero on each entry. It runs at most once per `update` call: a call that
|
|
619
|
+
* covers several delays runs it once, and its later runs stay on multiples of the delay. A run
|
|
620
|
+
* is not a move: no `update` hook is skipped, `always` is not rechecked and `onMove` is not
|
|
621
|
+
* called. A state's entries run where its `after` timers are tried and before them, a list in
|
|
622
|
+
* list order and the other form shortest delay first; on a call in which an `update` hook or a
|
|
623
|
+
* timer of a deeper state moved, they wait for the next call. A delay that is not a positive
|
|
624
|
+
* number throws, naming the state.
|
|
310
625
|
*/
|
|
311
|
-
readonly
|
|
626
|
+
readonly every?: NoInfer<
|
|
627
|
+
| {
|
|
628
|
+
readonly [seconds: number]:
|
|
629
|
+
| RunAction<Ctx, E, Tag, Hook>
|
|
630
|
+
| readonly RunAction<Ctx, E, Tag, Hook>[];
|
|
631
|
+
}
|
|
632
|
+
| readonly EveryConfig<Ctx, E, Tag, Hook>[]
|
|
633
|
+
>;
|
|
312
634
|
/**
|
|
313
|
-
*
|
|
314
|
-
* missing or passes wins. It is checked after a move only:
|
|
315
|
-
* not on an `update` call that moves nothing.
|
|
635
|
+
* Rules taken right after any move that leaves this state active; the first whose `when` is
|
|
636
|
+
* missing or passes wins, and its `run` runs during the move. It is checked after a move only:
|
|
637
|
+
* not after a rule with no `to`, and not on an `update` call that moves nothing.
|
|
316
638
|
*/
|
|
317
|
-
readonly always?: AlwaysSpec<Ctx, To>;
|
|
639
|
+
readonly always?: AlwaysSpec<Ctx, To, E, Tag, Hook>;
|
|
318
640
|
/**
|
|
319
641
|
* Runs each time the state is entered, outermost state first: on a move, at `start`, and when
|
|
320
642
|
* a hot reload adds the state. It gets no event.
|
|
321
643
|
*/
|
|
322
|
-
readonly enter?: StateHook<Ctx, E>;
|
|
644
|
+
readonly enter?: StateHook<Ctx, E, Tag, Hook>;
|
|
323
645
|
/**
|
|
324
646
|
* Runs each time the state is left, deepest state first, including on `stop()`. A state a hot
|
|
325
647
|
* reload removes is dropped without it.
|
|
326
648
|
*/
|
|
327
|
-
readonly exit?: StateHook<Ctx, E>;
|
|
649
|
+
readonly exit?: StateHook<Ctx, E, Tag, Hook>;
|
|
328
650
|
/**
|
|
329
651
|
* Runs on every `update(dt)` while the state is active. Return one of the machine's full paths
|
|
330
652
|
* to move there, or `undefined` to stay.
|
|
331
653
|
*/
|
|
332
|
-
readonly update?: NoInfer<UpdateHook<Ctx, E, To>>;
|
|
654
|
+
readonly update?: NoInfer<UpdateHook<Ctx, E, To, Tag, Hook>>;
|
|
333
655
|
/** Started on each entry; see `TaskStart`. */
|
|
334
|
-
readonly task?: TaskStart<Ctx, E>;
|
|
656
|
+
readonly task?: TaskStart<Ctx, E, Tag, Hook>;
|
|
657
|
+
/** Runs when the machine drops an event while the state is active; see `UnhandledHook`. */
|
|
658
|
+
readonly unhandled?: UnhandledHook<Ctx, E, Tag, Hook>;
|
|
335
659
|
/** Renamed to `task`. */
|
|
336
660
|
readonly invoke?: never;
|
|
337
661
|
}
|
|
338
662
|
|
|
339
663
|
/** @noSelf */
|
|
340
|
-
interface CompoundState<
|
|
341
|
-
|
|
664
|
+
interface CompoundState<
|
|
665
|
+
Ctx,
|
|
666
|
+
E extends EventObject,
|
|
667
|
+
T,
|
|
668
|
+
Self extends string,
|
|
669
|
+
To extends string,
|
|
670
|
+
Tag extends string = never,
|
|
671
|
+
Hook extends string = string,
|
|
672
|
+
> extends StateBase<Ctx, E, T, Self, To, Tag, Hook> {
|
|
342
673
|
/**
|
|
343
674
|
* `"parallel"` keeps every child active at once, each as a region with its own active state.
|
|
344
675
|
* Not allowed on the root.
|
|
@@ -359,8 +690,15 @@ interface CompoundState<Ctx, E extends EventObject, T, Self extends string, To e
|
|
|
359
690
|
}
|
|
360
691
|
|
|
361
692
|
/** @noSelf */
|
|
362
|
-
interface ParallelState<
|
|
363
|
-
|
|
693
|
+
interface ParallelState<
|
|
694
|
+
Ctx,
|
|
695
|
+
E extends EventObject,
|
|
696
|
+
T,
|
|
697
|
+
Self extends string,
|
|
698
|
+
To extends string,
|
|
699
|
+
Tag extends string = never,
|
|
700
|
+
Hook extends string = string,
|
|
701
|
+
> extends StateBase<Ctx, E, T, Self, To, Tag, Hook> {
|
|
364
702
|
/**
|
|
365
703
|
* `"parallel"` keeps every child active at once, each as a region with its own active state.
|
|
366
704
|
* Not allowed on the root.
|
|
@@ -370,51 +708,146 @@ interface ParallelState<Ctx, E extends EventObject, T, Self extends string, To e
|
|
|
370
708
|
readonly restoreDepth?: never;
|
|
371
709
|
}
|
|
372
710
|
|
|
373
|
-
type StateNode<
|
|
374
|
-
| CompoundState<Ctx, E, T, Self, To>
|
|
375
|
-
| ParallelState<Ctx, E, T, Self, To>;
|
|
376
|
-
|
|
377
|
-
type RootState<Ctx, E extends EventObject, T> = CompoundState<
|
|
711
|
+
type StateNode<
|
|
378
712
|
Ctx,
|
|
379
|
-
E,
|
|
713
|
+
E extends EventObject,
|
|
380
714
|
T,
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
715
|
+
Self extends string,
|
|
716
|
+
To extends string,
|
|
717
|
+
Tag extends string = never,
|
|
718
|
+
Hook extends string = string,
|
|
719
|
+
> = CompoundState<Ctx, E, T, Self, To, Tag, Hook> | ParallelState<Ctx, E, T, Self, To, Tag, Hook>;
|
|
720
|
+
|
|
721
|
+
type StateKey =
|
|
722
|
+
| keyof CompoundState<unknown, EventObject, unknown, string, string>
|
|
723
|
+
| keyof ParallelState<unknown, EventObject, unknown, string, string>;
|
|
724
|
+
|
|
725
|
+
// The node tree: every key each state writes, so a state with a `states` key is a level
|
|
726
|
+
// whatever its children are, and a state without one is a leaf. Each state is inferred at two
|
|
727
|
+
// sites. The mapped type `NodeKeys` reads a state that holds a callback with unannotated
|
|
728
|
+
// parameters beside one other slot, property by property. The bare site `InferOnly` reads a
|
|
729
|
+
// state that holds no such callback whole, `{}` included, which the mapped type never reads
|
|
730
|
+
// because `{}` has no property. A bare `NT[K]` in its place would intersect the state literal
|
|
731
|
+
// into every slot type, and a bad target would be reported on the whole `on` object.
|
|
732
|
+
type NodeLevel<NT> = { readonly [K in keyof NT]: NodeKeys<NT[K]> & InferOnly<NT[K]> };
|
|
733
|
+
|
|
734
|
+
type NodeKeys<N> = { readonly [P in keyof N]: NodeSlot<P, N[P], keyof N> };
|
|
735
|
+
|
|
736
|
+
// The slot value is a type argument of its own: inside the true branch of
|
|
737
|
+
// `P extends "states"`, `N[P]` reads as `N[P & "states"]`, which inference does not look for,
|
|
738
|
+
// and the level below would never be inferred. `NodeKeys` makes every key the config writes a
|
|
739
|
+
// known property, which switches the excess-property check off for a state, so a key no state
|
|
740
|
+
// has maps to `never` to keep that error. `initial` and `restoreDepth` map to `never` on a
|
|
741
|
+
// state with no `states` key.
|
|
742
|
+
type NodeSlot<P, V, Keys> = P extends "states"
|
|
743
|
+
? NodeLevel<V>
|
|
744
|
+
: P extends "initial" | "restoreDepth"
|
|
745
|
+
? "states" extends Keys
|
|
746
|
+
? unknown
|
|
747
|
+
: never
|
|
748
|
+
: P extends StateKey
|
|
749
|
+
? unknown
|
|
750
|
+
: never;
|
|
751
|
+
|
|
752
|
+
type InferOnly<X> = X extends unknown ? unknown : X;
|
|
753
|
+
|
|
754
|
+
type StatesOf<N> = N extends { readonly states: infer C } ? C : unknown;
|
|
755
|
+
|
|
756
|
+
type HasStates<N> = "states" extends keyof N ? true : false;
|
|
757
|
+
|
|
758
|
+
// The paths a config slot or a callback's machine accepts, read off a node tree. TypeScript
|
|
759
|
+
// checks a config once before it types the callbacks, and a state that holds only callbacks
|
|
760
|
+
// with unannotated parameters, or `after` and `every` lists with one in them, is `unknown` in
|
|
761
|
+
// that check: it may be a leaf, or a compound state whose `initial` is missing. Such a state
|
|
762
|
+
// accepts any path below it. Every other state with no `states` key accepts none.
|
|
763
|
+
type NodePathsBelow<L, D extends number> = unknown extends L
|
|
764
|
+
? string
|
|
765
|
+
: {
|
|
766
|
+
[K in keyof L & string]:
|
|
767
|
+
| K
|
|
768
|
+
| (unknown extends L[K]
|
|
769
|
+
? `${K}/${string}`
|
|
770
|
+
: HasStates<L[K]> extends true
|
|
771
|
+
? `${K}/${D extends 1 ? string : NodePathsBelow<StatesOf<L[K]>, PathDepth[D]>}`
|
|
772
|
+
: never);
|
|
773
|
+
}[keyof L & string];
|
|
774
|
+
|
|
775
|
+
type NodePath<NT> = `/${NodePathsBelow<NT, 4>}`;
|
|
776
|
+
|
|
777
|
+
// The node tree is applied to the children of `states` only; the root's own keys, `privateCtx`
|
|
778
|
+
// among them, are not read into it.
|
|
779
|
+
type RootState<
|
|
780
|
+
Ctx,
|
|
781
|
+
E extends EventObject,
|
|
782
|
+
T,
|
|
783
|
+
Tag extends string = never,
|
|
784
|
+
NT = unknown,
|
|
785
|
+
HT = unknown,
|
|
786
|
+
> = CompoundState<Ctx, E, T, "", NoInfer<NodePath<NT>>, Tag, NoInfer<NodePath<HT>>> & {
|
|
384
787
|
readonly initial: string;
|
|
385
788
|
readonly states: object;
|
|
386
|
-
};
|
|
789
|
+
} & { readonly states: NodeLevel<NT> } & { readonly states: NodeLevel<HT> };
|
|
387
790
|
|
|
388
791
|
/**
|
|
389
792
|
* The config of one state. Every field is optional. A state with child `states` is a compound
|
|
390
793
|
* state and needs an `initial`, unless its `type` is `"parallel"`; a state without children is a
|
|
391
|
-
* leaf.
|
|
794
|
+
* leaf. `Tag` is the union of the machine's tag names. The `machine` a callback of a config
|
|
795
|
+
* typed this way receives takes any `string` path.
|
|
392
796
|
*/
|
|
393
|
-
export type StateConfig<Ctx, E extends EventObject> = StateNode<
|
|
797
|
+
export type StateConfig<Ctx, E extends EventObject, Tag extends string = never> = StateNode<
|
|
798
|
+
Ctx,
|
|
799
|
+
E,
|
|
800
|
+
unknown,
|
|
801
|
+
string,
|
|
802
|
+
string,
|
|
803
|
+
Tag
|
|
804
|
+
>;
|
|
394
805
|
|
|
395
806
|
/**
|
|
396
807
|
* The root config passed to `defineMachine`. It takes the fields of a state and requires
|
|
397
|
-
* `initial` and `states`. The root has no name and cannot be parallel.
|
|
808
|
+
* `initial` and `states`. The root has no name and cannot be parallel. `Tag` is the union of the
|
|
809
|
+
* machine's tag names. The `machine` a callback of a config typed this way receives takes any
|
|
810
|
+
* `string` path.
|
|
398
811
|
*/
|
|
399
|
-
export type MachineConfig<Ctx, E extends EventObject> = CompoundState<
|
|
812
|
+
export type MachineConfig<Ctx, E extends EventObject, Tag extends string = never> = CompoundState<
|
|
400
813
|
Ctx,
|
|
401
814
|
E,
|
|
402
815
|
unknown,
|
|
403
816
|
string,
|
|
404
|
-
string
|
|
817
|
+
string,
|
|
818
|
+
Tag
|
|
405
819
|
> & {
|
|
406
820
|
readonly initial: string;
|
|
407
|
-
readonly states: { readonly [name: string]: StateConfig<Ctx, E> };
|
|
821
|
+
readonly states: { readonly [name: string]: StateConfig<Ctx, E, Tag> };
|
|
408
822
|
};
|
|
409
823
|
|
|
410
|
-
type Guard<Ctx, E extends EventObject> = (ctx: Ctx, event: E) => boolean;
|
|
824
|
+
type Guard<Ctx, E extends EventObject> = (ctx: Ctx, event: E, machine: MachineView) => boolean;
|
|
411
825
|
|
|
412
826
|
interface Transition<Ctx, E extends EventObject> {
|
|
413
827
|
readonly target: number;
|
|
414
828
|
readonly when: Guard<Ctx, E> | undefined;
|
|
415
829
|
readonly run: readonly MoveAction<Ctx, E>[];
|
|
830
|
+
// Set on an after timer only, whose sorted position is not its entry: the delay of a record
|
|
831
|
+
// entry, the index of a list entry.
|
|
832
|
+
key?: number;
|
|
416
833
|
}
|
|
417
834
|
|
|
835
|
+
/**
|
|
836
|
+
* How the core calls a `MoveListener`: the declared arguments, then the path of the state whose
|
|
837
|
+
* rule made the move and that rule's key. `MoveListener` declares neither.
|
|
838
|
+
* @noSelf
|
|
839
|
+
*/
|
|
840
|
+
type MoveReport<E extends EventObject> = (
|
|
841
|
+
from: string,
|
|
842
|
+
to: string | undefined,
|
|
843
|
+
cause: MoveCause,
|
|
844
|
+
event: E | undefined,
|
|
845
|
+
source: string | undefined,
|
|
846
|
+
key: number | undefined,
|
|
847
|
+
) => void;
|
|
848
|
+
|
|
849
|
+
type Delay<Ctx> = number | ((ctx: Ctx) => number);
|
|
850
|
+
|
|
418
851
|
interface Compiled<Ctx, E extends EventObject> {
|
|
419
852
|
count: number;
|
|
420
853
|
readonly configs: StateConfig<Ctx, E>[];
|
|
@@ -430,9 +863,18 @@ interface Compiled<Ctx, E extends EventObject> {
|
|
|
430
863
|
readonly onIndex: { [type: string]: number }[];
|
|
431
864
|
readonly transitions: Transition<Ctx, E>[][];
|
|
432
865
|
readonly afterDelays: number[][];
|
|
433
|
-
readonly
|
|
866
|
+
readonly afterRules: Transition<Ctx, E>[][];
|
|
867
|
+
// Set only for a state whose after list holds a delay function: its entries in list order,
|
|
868
|
+
// for each instance to sort once the function is read.
|
|
869
|
+
readonly afterListDelays: (Delay<Ctx>[] | undefined)[];
|
|
870
|
+
readonly afterListRules: (Transition<Ctx, E>[] | undefined)[];
|
|
871
|
+
// A state's every entries: the record form in delay order, a list in list order.
|
|
872
|
+
readonly everyDelays: Delay<Ctx>[][];
|
|
873
|
+
readonly everyRuns: RunAction<Ctx, E>[][][];
|
|
434
874
|
readonly always: Transition<Ctx, E>[][];
|
|
435
875
|
readonly pathIndex: { [path: string]: number };
|
|
876
|
+
// For each tag, the states that carry it.
|
|
877
|
+
readonly tagStates: { [tag: string]: number[] };
|
|
436
878
|
}
|
|
437
879
|
|
|
438
880
|
const ROOT = 0;
|
|
@@ -467,10 +909,13 @@ function resolveTarget<Ctx, E extends EventObject>(
|
|
|
467
909
|
return index;
|
|
468
910
|
}
|
|
469
911
|
|
|
470
|
-
// A structural cast keeps each read a plain method call: native in JavaScript, string.byte
|
|
912
|
+
// A structural cast keeps each read a plain method call: native in JavaScript, string.byte and
|
|
913
|
+
// string.sub in Lua.
|
|
471
914
|
interface StringUnits {
|
|
472
915
|
charCodeAt(index: number): number;
|
|
473
916
|
byte(index: number): number;
|
|
917
|
+
substring(start: number, end: number): string;
|
|
918
|
+
sub(first: number, last: number): string;
|
|
474
919
|
}
|
|
475
920
|
|
|
476
921
|
// JavaScript strings hold UTF-16 code units; Lua strings hold UTF-8 bytes.
|
|
@@ -484,6 +929,36 @@ function unitAt(text: string, index: number): number {
|
|
|
484
929
|
return units.byte(index + 1);
|
|
485
930
|
}
|
|
486
931
|
|
|
932
|
+
const SLASH = 47;
|
|
933
|
+
|
|
934
|
+
// The state at path, or at its closest parent that exists: the path loses its last name until
|
|
935
|
+
// a state has it, and the root stands in when none does.
|
|
936
|
+
function closestState<Ctx, E extends EventObject>(
|
|
937
|
+
compiled: Compiled<Ctx, E>,
|
|
938
|
+
path: string,
|
|
939
|
+
): number {
|
|
940
|
+
let text = path;
|
|
941
|
+
while (true) {
|
|
942
|
+
const state = lookupPath(compiled, text);
|
|
943
|
+
if (state !== undefined) {
|
|
944
|
+
return state;
|
|
945
|
+
}
|
|
946
|
+
let cut = text.length - 1;
|
|
947
|
+
while (cut > 0 && unitAt(text, cut) !== SLASH) {
|
|
948
|
+
cut--;
|
|
949
|
+
}
|
|
950
|
+
if (cut <= 0) {
|
|
951
|
+
return ROOT;
|
|
952
|
+
}
|
|
953
|
+
const units = text as unknown as StringUnits;
|
|
954
|
+
if (UTF16_UNITS) {
|
|
955
|
+
text = units.substring(0, cut);
|
|
956
|
+
} else {
|
|
957
|
+
text = units.sub(1, cut);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
487
962
|
// Moves surrogates above the rest of the BMP, so UTF-16 units compare in code-point order.
|
|
488
963
|
// UTF-8 bytes already do, and every byte is below the surrogate range.
|
|
489
964
|
function codePointRank(unit: number): number {
|
|
@@ -527,6 +1002,18 @@ function registerState<Ctx, E extends EventObject>(
|
|
|
527
1002
|
compiled.levels[index] = 0;
|
|
528
1003
|
compiled.records[index] = false;
|
|
529
1004
|
compiled.pathIndex[path] = index;
|
|
1005
|
+
const tags = config.tags as readonly string[] | undefined;
|
|
1006
|
+
if (tags !== undefined) {
|
|
1007
|
+
for (let i = 0; i < tags.length; i++) {
|
|
1008
|
+
const tag = tags[i] as string;
|
|
1009
|
+
let carriers = compiled.tagStates[tag];
|
|
1010
|
+
if (carriers === undefined) {
|
|
1011
|
+
carriers = [];
|
|
1012
|
+
compiled.tagStates[tag] = carriers;
|
|
1013
|
+
}
|
|
1014
|
+
carriers.push(index);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
530
1017
|
const children = config.states;
|
|
531
1018
|
if (children === undefined) {
|
|
532
1019
|
return;
|
|
@@ -656,6 +1143,20 @@ function compileRecords<Ctx, E extends EventObject>(compiled: Compiled<Ctx, E>):
|
|
|
656
1143
|
}
|
|
657
1144
|
}
|
|
658
1145
|
|
|
1146
|
+
// A run is one function or a list of them; either way it compiles to a list.
|
|
1147
|
+
function collectRun<Action>(declared: Action | readonly Action[] | undefined): Action[] {
|
|
1148
|
+
const run: Action[] = [];
|
|
1149
|
+
if (typeof declared === "function") {
|
|
1150
|
+
run.push(declared);
|
|
1151
|
+
} else if (declared !== undefined) {
|
|
1152
|
+
const list = declared as readonly Action[];
|
|
1153
|
+
for (let i = 0; i < list.length; i++) {
|
|
1154
|
+
run.push(list[i] as Action);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return run;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
659
1160
|
function compileTransition<Ctx, E extends EventObject>(
|
|
660
1161
|
compiled: Compiled<Ctx, E>,
|
|
661
1162
|
source: number,
|
|
@@ -668,19 +1169,10 @@ function compileTransition<Ctx, E extends EventObject>(
|
|
|
668
1169
|
run: [],
|
|
669
1170
|
};
|
|
670
1171
|
}
|
|
671
|
-
const run: MoveAction<Ctx, E>[] = [];
|
|
672
|
-
const declared = spec.run;
|
|
673
|
-
if (typeof declared === "function") {
|
|
674
|
-
run.push(declared);
|
|
675
|
-
} else if (declared !== undefined) {
|
|
676
|
-
for (let i = 0; i < declared.length; i++) {
|
|
677
|
-
run.push(declared[i] as MoveAction<Ctx, E>);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
1172
|
return {
|
|
681
1173
|
target: spec.to === undefined ? NO_STATE : resolveTarget(compiled, source, spec.to),
|
|
682
1174
|
when: spec.when,
|
|
683
|
-
run,
|
|
1175
|
+
run: collectRun<MoveAction<Ctx, E>>(spec.run),
|
|
684
1176
|
};
|
|
685
1177
|
}
|
|
686
1178
|
|
|
@@ -724,18 +1216,18 @@ function compileAlways<Ctx, E extends EventObject>(
|
|
|
724
1216
|
// Read as an array type: the transpiler shifts an index to Lua's base 1 only on an array.
|
|
725
1217
|
const spec = (compiled.configs[index] as StateConfig<Ctx, E>).always as
|
|
726
1218
|
| string
|
|
727
|
-
| AlwaysConfig<Ctx>
|
|
728
|
-
| readonly AlwaysConfig<Ctx>[]
|
|
1219
|
+
| AlwaysConfig<Ctx, string, E>
|
|
1220
|
+
| readonly AlwaysConfig<Ctx, string, E>[]
|
|
729
1221
|
| undefined;
|
|
730
1222
|
if (spec === undefined) {
|
|
731
1223
|
return;
|
|
732
1224
|
}
|
|
733
|
-
const candidates: readonly (string | AlwaysConfig<Ctx>)[] =
|
|
734
|
-
typeof spec === "string" || (spec as readonly AlwaysConfig<Ctx>[])[0] === undefined
|
|
735
|
-
? [spec as string | AlwaysConfig<Ctx>]
|
|
736
|
-
: (spec as readonly AlwaysConfig<Ctx>[]);
|
|
1225
|
+
const candidates: readonly (string | AlwaysConfig<Ctx, string, E>)[] =
|
|
1226
|
+
typeof spec === "string" || (spec as readonly AlwaysConfig<Ctx, string, E>[])[0] === undefined
|
|
1227
|
+
? [spec as string | AlwaysConfig<Ctx, string, E>]
|
|
1228
|
+
: (spec as readonly AlwaysConfig<Ctx, string, E>[]);
|
|
737
1229
|
for (let i = 0; i < candidates.length; i++) {
|
|
738
|
-
const candidate = candidates[i] as string | AlwaysConfig<Ctx>;
|
|
1230
|
+
const candidate = candidates[i] as string | AlwaysConfig<Ctx, string, E>;
|
|
739
1231
|
if (typeof candidate !== "string" && candidate.to === undefined) {
|
|
740
1232
|
throw `hsm: state "${describePath(compiled.paths[index] as string)}" has an always transition with no "to"`;
|
|
741
1233
|
}
|
|
@@ -763,30 +1255,169 @@ function parseDelay(key: string | number): number {
|
|
|
763
1255
|
return delay;
|
|
764
1256
|
}
|
|
765
1257
|
|
|
1258
|
+
// NaN fails the comparison, so it is rejected with the negative numbers.
|
|
1259
|
+
function checkDelay(path: string, delay: unknown, written: unknown): void {
|
|
1260
|
+
if (typeof delay !== "number" || !(delay >= 0)) {
|
|
1261
|
+
throw `hsm: state "${describePath(path)}" has an after delay "${written}" that is not a non-negative number`;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Keeps the delays ascending; timers with one delay stay in the order they were added. Each
|
|
1266
|
+
// delay carries one item: a timer's rule, or the run of an every entry.
|
|
1267
|
+
function insertTimer<Item>(delays: number[], items: Item[], delay: number, item: Item): void {
|
|
1268
|
+
let slot = delays.length;
|
|
1269
|
+
while (slot > 0 && (delays[slot - 1] as number) > delay) {
|
|
1270
|
+
delays[slot] = delays[slot - 1] as number;
|
|
1271
|
+
items[slot] = items[slot - 1] as Item;
|
|
1272
|
+
slot--;
|
|
1273
|
+
}
|
|
1274
|
+
delays[slot] = delay;
|
|
1275
|
+
items[slot] = item;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// For each entry of an after list, the position of its rule among the sorted timers.
|
|
1279
|
+
function entryPositions<Ctx, E extends EventObject>(
|
|
1280
|
+
list: Transition<Ctx, E>[],
|
|
1281
|
+
sorted: Transition<Ctx, E>[],
|
|
1282
|
+
): number[] {
|
|
1283
|
+
const positions: number[] = [];
|
|
1284
|
+
for (let entry = 0; entry < list.length; entry++) {
|
|
1285
|
+
let at = 0;
|
|
1286
|
+
while (at < sorted.length && sorted[at] !== list[entry]) {
|
|
1287
|
+
at++;
|
|
1288
|
+
}
|
|
1289
|
+
positions[entry] = at;
|
|
1290
|
+
}
|
|
1291
|
+
return positions;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function compileTimer<Ctx, E extends EventObject>(
|
|
1295
|
+
compiled: Compiled<Ctx, E>,
|
|
1296
|
+
index: number,
|
|
1297
|
+
spec: string | AfterRule<Ctx, string, E>,
|
|
1298
|
+
key: number,
|
|
1299
|
+
): Transition<Ctx, E> {
|
|
1300
|
+
const rule = compileTransition(compiled, index, spec as string | TransitionConfig<Ctx, E>);
|
|
1301
|
+
if (rule.target === NO_STATE && rule.run.length === 0) {
|
|
1302
|
+
throw `hsm: state "${describePath(compiled.paths[index] as string)}" has an after timer with no "to" and no "run"`;
|
|
1303
|
+
}
|
|
1304
|
+
rule.key = key;
|
|
1305
|
+
return rule;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
766
1308
|
function compileAfter<Ctx, E extends EventObject>(compiled: Compiled<Ctx, E>, index: number): void {
|
|
767
1309
|
const delays: number[] = [];
|
|
768
|
-
const
|
|
1310
|
+
const rules: Transition<Ctx, E>[] = [];
|
|
769
1311
|
compiled.afterDelays[index] = delays;
|
|
770
|
-
compiled.
|
|
1312
|
+
compiled.afterRules[index] = rules;
|
|
771
1313
|
const after = (compiled.configs[index] as StateConfig<Ctx, E>).after;
|
|
772
1314
|
if (after === undefined) {
|
|
773
1315
|
return;
|
|
774
1316
|
}
|
|
775
1317
|
const path = compiled.paths[index] as string;
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1318
|
+
// Read as an array type: the transpiler shifts an index to Lua's base 1 only on an array.
|
|
1319
|
+
const list = after as readonly AfterConfig<Ctx, string, E>[];
|
|
1320
|
+
const first = list[0];
|
|
1321
|
+
// A rule object under the delay 0, or under 1 in Lua where the read is shifted, is an object
|
|
1322
|
+
// too, so the list form is the one whose first entry has a delay.
|
|
1323
|
+
if (typeof first !== "object" || first.delay === undefined) {
|
|
1324
|
+
const byDelay = after as { readonly [seconds: number]: string | AfterRule<Ctx, string, E> };
|
|
1325
|
+
for (const key in byDelay) {
|
|
1326
|
+
const delay = parseDelay(key);
|
|
1327
|
+
checkDelay(path, delay, key);
|
|
1328
|
+
const spec = byDelay[key as unknown as number] as string | AfterRule<Ctx, string, E>;
|
|
1329
|
+
insertTimer(delays, rules, delay, compileTimer(compiled, index, spec, delay));
|
|
787
1330
|
}
|
|
788
|
-
|
|
789
|
-
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const listDelays: Delay<Ctx>[] = [];
|
|
1334
|
+
const listRules: Transition<Ctx, E>[] = [];
|
|
1335
|
+
let perInstance = false;
|
|
1336
|
+
for (let i = 0; i < list.length; i++) {
|
|
1337
|
+
const entry = list[i] as AfterConfig<Ctx, string, E>;
|
|
1338
|
+
const delay = entry.delay;
|
|
1339
|
+
if (typeof delay === "function") {
|
|
1340
|
+
perInstance = true;
|
|
1341
|
+
} else {
|
|
1342
|
+
checkDelay(path, delay, delay);
|
|
1343
|
+
}
|
|
1344
|
+
listDelays[i] = delay;
|
|
1345
|
+
listRules[i] = compileTimer(compiled, index, entry, i);
|
|
1346
|
+
}
|
|
1347
|
+
if (perInstance) {
|
|
1348
|
+
compiled.afterListDelays[index] = listDelays;
|
|
1349
|
+
compiled.afterListRules[index] = listRules;
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
for (let i = 0; i < listDelays.length; i++) {
|
|
1353
|
+
insertTimer(delays, rules, listDelays[i] as number, listRules[i] as Transition<Ctx, E>);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// NaN fails the comparison, so it is rejected with zero and the negative numbers.
|
|
1358
|
+
function checkEveryDelay(path: string, delay: unknown, written: unknown): void {
|
|
1359
|
+
if (typeof delay !== "number" || !(delay > 0)) {
|
|
1360
|
+
throw `hsm: state "${describePath(path)}" has an every delay "${written}" that is not a positive number`;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// The first multiple of delay above clock. Rounding can leave the product at or below the
|
|
1365
|
+
// clock, and one more delay lifts it past.
|
|
1366
|
+
function nextRun(clock: number, delay: number): number {
|
|
1367
|
+
const at = (Math.floor(clock / delay) + 1) * delay;
|
|
1368
|
+
if (at > clock) {
|
|
1369
|
+
return at;
|
|
1370
|
+
}
|
|
1371
|
+
return at + delay;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function compileEveryRun<Ctx, E extends EventObject>(
|
|
1375
|
+
path: string,
|
|
1376
|
+
declared: RunAction<Ctx, E> | readonly RunAction<Ctx, E>[] | undefined,
|
|
1377
|
+
): RunAction<Ctx, E>[] {
|
|
1378
|
+
const run = collectRun<RunAction<Ctx, E>>(declared);
|
|
1379
|
+
if (run.length === 0) {
|
|
1380
|
+
throw `hsm: state "${describePath(path)}" has an every entry with no "run"`;
|
|
1381
|
+
}
|
|
1382
|
+
return run;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function compileEvery<Ctx, E extends EventObject>(compiled: Compiled<Ctx, E>, index: number): void {
|
|
1386
|
+
const delays: Delay<Ctx>[] = [];
|
|
1387
|
+
const runs: RunAction<Ctx, E>[][] = [];
|
|
1388
|
+
compiled.everyDelays[index] = delays;
|
|
1389
|
+
compiled.everyRuns[index] = runs;
|
|
1390
|
+
const every = (compiled.configs[index] as StateConfig<Ctx, E>).every;
|
|
1391
|
+
if (every === undefined) {
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const path = compiled.paths[index] as string;
|
|
1395
|
+
// Read as an array type: the transpiler shifts an index to Lua's base 1 only on an array.
|
|
1396
|
+
const list = every as readonly EveryConfig<Ctx, E>[];
|
|
1397
|
+
const first = list[0];
|
|
1398
|
+
// A run list under the delay 0, or under 1 in Lua where the read is shifted, is an object
|
|
1399
|
+
// too, so the list form is the one whose first entry has a delay.
|
|
1400
|
+
if (typeof first !== "object" || first.delay === undefined) {
|
|
1401
|
+
const byDelay = every as {
|
|
1402
|
+
readonly [seconds: number]: RunAction<Ctx, E> | readonly RunAction<Ctx, E>[];
|
|
1403
|
+
};
|
|
1404
|
+
// Lua's pairs walks number keys in no fixed order, so the entries are sorted by delay.
|
|
1405
|
+
for (const key in byDelay) {
|
|
1406
|
+
const delay = parseDelay(key);
|
|
1407
|
+
checkEveryDelay(path, delay, key);
|
|
1408
|
+
const run = compileEveryRun(path, byDelay[key as unknown as number]);
|
|
1409
|
+
insertTimer(delays as number[], runs, delay, run);
|
|
1410
|
+
}
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
for (let i = 0; i < list.length; i++) {
|
|
1414
|
+
const entry = list[i] as EveryConfig<Ctx, E>;
|
|
1415
|
+
const delay = entry.delay;
|
|
1416
|
+
if (typeof delay !== "function") {
|
|
1417
|
+
checkEveryDelay(path, delay, delay);
|
|
1418
|
+
}
|
|
1419
|
+
delays[i] = delay;
|
|
1420
|
+
runs[i] = compileEveryRun(path, entry.run);
|
|
790
1421
|
}
|
|
791
1422
|
}
|
|
792
1423
|
|
|
@@ -806,15 +1437,24 @@ function compile<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>): Com
|
|
|
806
1437
|
onIndex: [],
|
|
807
1438
|
transitions: [],
|
|
808
1439
|
afterDelays: [],
|
|
809
|
-
|
|
1440
|
+
afterRules: [],
|
|
1441
|
+
afterListDelays: [],
|
|
1442
|
+
afterListRules: [],
|
|
1443
|
+
everyDelays: [],
|
|
1444
|
+
everyRuns: [],
|
|
810
1445
|
always: [],
|
|
811
1446
|
pathIndex: {},
|
|
1447
|
+
// A JavaScript object inherits members such as `__proto__` and `constructor`, and a tag can
|
|
1448
|
+
// be named like one. This literal has no prototype, so every string is an own key. In Lua
|
|
1449
|
+
// the nil field leaves a plain table.
|
|
1450
|
+
tagStates: { __proto__: null } as unknown as { [tag: string]: number[] },
|
|
812
1451
|
};
|
|
813
1452
|
registerState(compiled, config, NO_STATE, "");
|
|
814
1453
|
for (let index = 0; index < compiled.count; index++) {
|
|
815
1454
|
compileInitial(compiled, index);
|
|
816
1455
|
compileOn(compiled, index);
|
|
817
1456
|
compileAfter(compiled, index);
|
|
1457
|
+
compileEvery(compiled, index);
|
|
818
1458
|
compileAlways(compiled, index);
|
|
819
1459
|
}
|
|
820
1460
|
compileRecords(compiled);
|
|
@@ -853,6 +1493,38 @@ function transitionDomain<Ctx, E extends EventObject>(
|
|
|
853
1493
|
return domain;
|
|
854
1494
|
}
|
|
855
1495
|
|
|
1496
|
+
// Reads a start position: by state, the child a compound state enters, or NO_STATE where no
|
|
1497
|
+
// path names one. Each path, in list order, claims the child of every compound state above the
|
|
1498
|
+
// state it names. A path that needs a child an earlier path claimed differently is dropped
|
|
1499
|
+
// whole, before it claims anything, so the first in the list wins and a dropped path leaves
|
|
1500
|
+
// nothing behind. Every state has an entry, so neither runtime reads a list with holes.
|
|
1501
|
+
function startChildren<Ctx, E extends EventObject>(
|
|
1502
|
+
compiled: Compiled<Ctx, E>,
|
|
1503
|
+
at: readonly string[],
|
|
1504
|
+
): number[] {
|
|
1505
|
+
const parent = compiled.parent;
|
|
1506
|
+
const chosen: number[] = [];
|
|
1507
|
+
for (let state = 0; state < compiled.count; state++) {
|
|
1508
|
+
chosen[state] = NO_STATE;
|
|
1509
|
+
}
|
|
1510
|
+
for (let i = 0; i < at.length; i++) {
|
|
1511
|
+
const target = closestState(compiled, at[i] as string);
|
|
1512
|
+
let free = true;
|
|
1513
|
+
for (let state = target; free && state !== ROOT; state = parent[state] as number) {
|
|
1514
|
+
const claimed = chosen[parent[state] as number] as number;
|
|
1515
|
+
free = claimed === NO_STATE || claimed === state;
|
|
1516
|
+
}
|
|
1517
|
+
for (let state = target; free && state !== ROOT; state = parent[state] as number) {
|
|
1518
|
+
const owner = parent[state] as number;
|
|
1519
|
+
// Every region of a parallel state is entered, so it holds no choice.
|
|
1520
|
+
if (compiled.parallel[owner] !== true) {
|
|
1521
|
+
chosen[owner] = state;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
return chosen;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
856
1528
|
interface Definition<Ctx, E extends EventObject> {
|
|
857
1529
|
compiled: Compiled<Ctx, E>;
|
|
858
1530
|
generation: number;
|
|
@@ -865,15 +1537,16 @@ interface Registered<Ctx, E extends EventObject> {
|
|
|
865
1537
|
|
|
866
1538
|
const registered: { [key: string]: Registered<unknown, EventObject> } = {};
|
|
867
1539
|
|
|
868
|
-
// Adds the fields the root's privateCtx returns to ctx
|
|
869
|
-
// has keeps its value: a hot reload adds new private fields and
|
|
1540
|
+
// Adds the fields the root's privateCtx returns to ctx; the factory receives target. With
|
|
1541
|
+
// onlyMissing, a field ctx already has keeps its value: a hot reload adds new private fields and
|
|
1542
|
+
// leaves the old ones alone.
|
|
870
1543
|
function fillPrivate(
|
|
871
1544
|
target: { [name: string]: unknown },
|
|
872
1545
|
root: unknown,
|
|
873
1546
|
onlyMissing: boolean,
|
|
874
1547
|
): void {
|
|
875
|
-
const factory = (root as
|
|
876
|
-
const fields = (factory?.() || {}) as { [name: string]: unknown };
|
|
1548
|
+
const factory = (root as PrivateCtxConfig<unknown, object>).privateCtx;
|
|
1549
|
+
const fields = (factory?.(target) || {}) as { [name: string]: unknown };
|
|
877
1550
|
for (const name in fields) {
|
|
878
1551
|
if (!onlyMissing || target[name] === undefined) {
|
|
879
1552
|
target[name] = fields[name];
|
|
@@ -881,10 +1554,35 @@ function fillPrivate(
|
|
|
881
1554
|
}
|
|
882
1555
|
}
|
|
883
1556
|
|
|
1557
|
+
// Copy-on-write: a report walks the list it started with, so a listener removed mid-report
|
|
1558
|
+
// never makes it skip another.
|
|
1559
|
+
function withListener<L>(list: readonly L[], listener: L): L[] {
|
|
1560
|
+
const added: L[] = [];
|
|
1561
|
+
for (let i = 0; i < list.length; i++) {
|
|
1562
|
+
added.push(list[i] as L);
|
|
1563
|
+
}
|
|
1564
|
+
added.push(listener);
|
|
1565
|
+
return added;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function withoutListener<L>(list: readonly L[], listener: L): L[] {
|
|
1569
|
+
const kept: L[] = [];
|
|
1570
|
+
let skipped = false;
|
|
1571
|
+
for (let i = 0; i < list.length; i++) {
|
|
1572
|
+
const current = list[i] as L;
|
|
1573
|
+
if (current !== listener || skipped) {
|
|
1574
|
+
kept.push(current);
|
|
1575
|
+
} else {
|
|
1576
|
+
skipped = true;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return kept;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
884
1582
|
function createMachine<Ctx, E extends EventObject>(
|
|
885
1583
|
definition: Definition<Ctx, E>,
|
|
886
1584
|
): Machine<Ctx, E> {
|
|
887
|
-
function start(ctx: Ctx): MachineInstance<Ctx, E> {
|
|
1585
|
+
function start(ctx: Ctx, at?: readonly string[]): MachineInstance<Ctx, E> {
|
|
888
1586
|
let compiled = definition.compiled;
|
|
889
1587
|
let generation = definition.generation;
|
|
890
1588
|
let paths = compiled.paths;
|
|
@@ -898,18 +1596,31 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
898
1596
|
let onIndex = compiled.onIndex;
|
|
899
1597
|
let transitions = compiled.transitions;
|
|
900
1598
|
let afterDelays = compiled.afterDelays;
|
|
901
|
-
let
|
|
1599
|
+
let afterRules = compiled.afterRules;
|
|
1600
|
+
let afterListDelays = compiled.afterListDelays;
|
|
1601
|
+
let afterListRules = compiled.afterListRules;
|
|
1602
|
+
let everyDelays = compiled.everyDelays;
|
|
1603
|
+
let everyRuns = compiled.everyRuns;
|
|
902
1604
|
let always = compiled.always;
|
|
903
1605
|
const activeChild: number[] = [];
|
|
904
1606
|
const isActive: boolean[] = [];
|
|
905
1607
|
const elapsed: number[] = [];
|
|
906
|
-
|
|
1608
|
+
// For a state with timers, which of them fired during this stay, by position in delay order.
|
|
1609
|
+
const fired: (boolean[] | undefined)[] = [];
|
|
1610
|
+
// The sorted timers of a state whose after list holds a delay function, read on each entry.
|
|
1611
|
+
const ownDelays: (number[] | undefined)[] = [];
|
|
1612
|
+
const ownRules: (Transition<Ctx, E>[] | undefined)[] = [];
|
|
1613
|
+
// For a state with every entries, by entry: the delay it counts in this stay, and the value
|
|
1614
|
+
// of the state's clock at its next run.
|
|
1615
|
+
const ownEvery: (number[] | undefined)[] = [];
|
|
1616
|
+
const nextRuns: (number[] | undefined)[] = [];
|
|
907
1617
|
const entryId: number[] = [];
|
|
908
1618
|
const cleanup: ((() => void) | undefined)[] = [];
|
|
909
1619
|
// Keyed by path, not index, so a remembered child outlives a rebind's recompilation.
|
|
910
1620
|
const remembered: { [parentPath: string]: string } = {};
|
|
911
1621
|
let slotCount = 0;
|
|
912
1622
|
growSlots();
|
|
1623
|
+
sizeFired();
|
|
913
1624
|
const leaves: string[] = [];
|
|
914
1625
|
let entryCount = 0;
|
|
915
1626
|
const queue: (E | undefined)[] = [];
|
|
@@ -920,27 +1631,70 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
920
1631
|
let stopRequested = false;
|
|
921
1632
|
let moved = false;
|
|
922
1633
|
let listeners: MoveListener<E>[] = [];
|
|
1634
|
+
let unhandledListeners: UnhandledListener<E>[] = [];
|
|
923
1635
|
|
|
924
1636
|
const instance = {
|
|
925
1637
|
ctx,
|
|
926
1638
|
path: undefined as string | undefined,
|
|
927
1639
|
leaves,
|
|
928
1640
|
matches,
|
|
1641
|
+
hasTag,
|
|
1642
|
+
can,
|
|
929
1643
|
send,
|
|
930
1644
|
update,
|
|
931
1645
|
stop,
|
|
932
1646
|
onMove,
|
|
1647
|
+
onUnhandled,
|
|
933
1648
|
};
|
|
934
1649
|
|
|
935
1650
|
function clearSlot(state: number): void {
|
|
936
1651
|
activeChild[state] = NO_STATE;
|
|
937
1652
|
isActive[state] = false;
|
|
938
1653
|
elapsed[state] = 0;
|
|
939
|
-
fired[state] =
|
|
1654
|
+
fired[state] = undefined;
|
|
1655
|
+
ownDelays[state] = undefined;
|
|
1656
|
+
ownRules[state] = undefined;
|
|
1657
|
+
ownEvery[state] = undefined;
|
|
1658
|
+
nextRuns[state] = undefined;
|
|
940
1659
|
entryId[state] = 0;
|
|
941
1660
|
cleanup[state] = undefined;
|
|
942
1661
|
}
|
|
943
1662
|
|
|
1663
|
+
function readDelays(state: number): void {
|
|
1664
|
+
const own = ownEvery[state];
|
|
1665
|
+
if (own !== undefined) {
|
|
1666
|
+
const declared = everyDelays[state] as Delay<Ctx>[];
|
|
1667
|
+
const runAt = nextRuns[state] as number[];
|
|
1668
|
+
const clock = elapsed[state] as number;
|
|
1669
|
+
for (let i = 0; i < own.length; i++) {
|
|
1670
|
+
let delay = declared[i] as Delay<Ctx>;
|
|
1671
|
+
if (typeof delay === "function") {
|
|
1672
|
+
delay = delay(ctx);
|
|
1673
|
+
checkEveryDelay(paths[state] as string, delay, delay);
|
|
1674
|
+
}
|
|
1675
|
+
own[i] = delay;
|
|
1676
|
+
runAt[i] = nextRun(clock, delay);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
const list = afterListDelays[state];
|
|
1680
|
+
if (list === undefined) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
const listRules = afterListRules[state] as Transition<Ctx, E>[];
|
|
1684
|
+
const delays: number[] = [];
|
|
1685
|
+
const rules: Transition<Ctx, E>[] = [];
|
|
1686
|
+
for (let i = 0; i < list.length; i++) {
|
|
1687
|
+
let delay = list[i] as Delay<Ctx>;
|
|
1688
|
+
if (typeof delay === "function") {
|
|
1689
|
+
delay = delay(ctx);
|
|
1690
|
+
checkDelay(paths[state] as string, delay, delay);
|
|
1691
|
+
}
|
|
1692
|
+
insertTimer(delays, rules, delay, listRules[i] as Transition<Ctx, E>);
|
|
1693
|
+
}
|
|
1694
|
+
ownDelays[state] = delays;
|
|
1695
|
+
ownRules[state] = rules;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
944
1698
|
function growSlots(): void {
|
|
945
1699
|
while (slotCount < compiled.count) {
|
|
946
1700
|
clearSlot(slotCount);
|
|
@@ -948,6 +1702,33 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
948
1702
|
}
|
|
949
1703
|
}
|
|
950
1704
|
|
|
1705
|
+
// The flag lists and the two lists of a state's every entries are built here, once per
|
|
1706
|
+
// compiled definition: an entry and an update only write them in place.
|
|
1707
|
+
function sizeFired(): void {
|
|
1708
|
+
for (let state = 0; state < compiled.count; state++) {
|
|
1709
|
+
const list = afterListDelays[state];
|
|
1710
|
+
const count = list === undefined ? (afterDelays[state] as number[]).length : list.length;
|
|
1711
|
+
if (count > 0) {
|
|
1712
|
+
const flags: boolean[] = [];
|
|
1713
|
+
for (let i = 0; i < count; i++) {
|
|
1714
|
+
flags[i] = false;
|
|
1715
|
+
}
|
|
1716
|
+
fired[state] = flags;
|
|
1717
|
+
}
|
|
1718
|
+
const entries = (everyDelays[state] as Delay<Ctx>[]).length;
|
|
1719
|
+
if (entries > 0) {
|
|
1720
|
+
const own: number[] = [];
|
|
1721
|
+
const runAt: number[] = [];
|
|
1722
|
+
for (let i = 0; i < entries; i++) {
|
|
1723
|
+
own[i] = 0;
|
|
1724
|
+
runAt[i] = 0;
|
|
1725
|
+
}
|
|
1726
|
+
ownEvery[state] = own;
|
|
1727
|
+
nextRuns[state] = runAt;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
951
1732
|
function collectActive(state: number, order: number[]): void {
|
|
952
1733
|
order.push(state);
|
|
953
1734
|
const list = children[state] as number[];
|
|
@@ -967,14 +1748,19 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
967
1748
|
collectActive(ROOT, order);
|
|
968
1749
|
const keptPaths: string[] = [];
|
|
969
1750
|
const keptElapsed: number[] = [];
|
|
970
|
-
const keptFired:
|
|
1751
|
+
const keptFired: (boolean[] | undefined)[] = [];
|
|
1752
|
+
const keptAt: (number[] | undefined)[] = [];
|
|
971
1753
|
const keptEntryId: number[] = [];
|
|
972
1754
|
const keptCleanup: ((() => void) | undefined)[] = [];
|
|
973
1755
|
for (let i = 0; i < order.length; i++) {
|
|
974
1756
|
const state = order[i] as number;
|
|
975
1757
|
keptPaths[i] = paths[state] as string;
|
|
976
1758
|
keptElapsed[i] = elapsed[state] as number;
|
|
977
|
-
keptFired[i] = fired[state]
|
|
1759
|
+
keptFired[i] = fired[state];
|
|
1760
|
+
const sorted = ownRules[state];
|
|
1761
|
+
if (sorted !== undefined) {
|
|
1762
|
+
keptAt[i] = entryPositions(afterListRules[state] as Transition<Ctx, E>[], sorted);
|
|
1763
|
+
}
|
|
978
1764
|
keptEntryId[i] = entryId[state] as number;
|
|
979
1765
|
keptCleanup[i] = cleanup[state];
|
|
980
1766
|
}
|
|
@@ -994,10 +1780,15 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
994
1780
|
onIndex = compiled.onIndex;
|
|
995
1781
|
transitions = compiled.transitions;
|
|
996
1782
|
afterDelays = compiled.afterDelays;
|
|
997
|
-
|
|
1783
|
+
afterRules = compiled.afterRules;
|
|
1784
|
+
afterListDelays = compiled.afterListDelays;
|
|
1785
|
+
afterListRules = compiled.afterListRules;
|
|
1786
|
+
everyDelays = compiled.everyDelays;
|
|
1787
|
+
everyRuns = compiled.everyRuns;
|
|
998
1788
|
always = compiled.always;
|
|
999
1789
|
fillPrivate(ctx as never, configs[ROOT], true);
|
|
1000
1790
|
growSlots();
|
|
1791
|
+
sizeFired();
|
|
1001
1792
|
const dropped: number[] = [];
|
|
1002
1793
|
for (let i = 0; i < order.length; i++) {
|
|
1003
1794
|
const state = lookupPath(compiled, keptPaths[i] as string);
|
|
@@ -1016,9 +1807,30 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1016
1807
|
activeChild[owner] = state;
|
|
1017
1808
|
}
|
|
1018
1809
|
elapsed[state] = keptElapsed[i] as number;
|
|
1019
|
-
fired[state] = keptFired[i] as number;
|
|
1020
1810
|
entryId[state] = keptEntryId[i] as number;
|
|
1021
1811
|
cleanup[state] = keptCleanup[i];
|
|
1812
|
+
// After the clock is restored: an every entry next runs at the first multiple of its
|
|
1813
|
+
// delay above it.
|
|
1814
|
+
readDelays(state);
|
|
1815
|
+
// By list entry where the old and the new definition both read a delay per instance,
|
|
1816
|
+
// else by position in delay order: a timer the new definition lacks is dropped, and a
|
|
1817
|
+
// new one starts unfired.
|
|
1818
|
+
const before = keptFired[i];
|
|
1819
|
+
const flags = fired[state];
|
|
1820
|
+
if (before !== undefined && flags !== undefined) {
|
|
1821
|
+
const was = keptAt[i];
|
|
1822
|
+
const sorted = ownRules[state];
|
|
1823
|
+
if (was !== undefined && sorted !== undefined) {
|
|
1824
|
+
const at = entryPositions(afterListRules[state] as Transition<Ctx, E>[], sorted);
|
|
1825
|
+
for (let entry = 0; entry < was.length && entry < at.length; entry++) {
|
|
1826
|
+
flags[at[entry] as number] = before[was[entry] as number] as boolean;
|
|
1827
|
+
}
|
|
1828
|
+
} else {
|
|
1829
|
+
for (let timer = 0; timer < before.length && timer < flags.length; timer++) {
|
|
1830
|
+
flags[timer] = before[timer] as boolean;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1022
1834
|
}
|
|
1023
1835
|
for (let i = dropped.length - 1; i >= 0; i--) {
|
|
1024
1836
|
const run = keptCleanup[dropped[i] as number];
|
|
@@ -1038,32 +1850,27 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1038
1850
|
settleAlways();
|
|
1039
1851
|
}
|
|
1040
1852
|
|
|
1041
|
-
// Copy-on-write: a report walks the list it started with, so a listener removed mid-report
|
|
1042
|
-
// never makes it skip another.
|
|
1043
1853
|
function onMove(listener: MoveListener<E>): () => void {
|
|
1044
|
-
|
|
1045
|
-
for (let i = 0; i < listeners.length; i++) {
|
|
1046
|
-
added.push(listeners[i] as MoveListener<E>);
|
|
1047
|
-
}
|
|
1048
|
-
added.push(listener);
|
|
1049
|
-
listeners = added;
|
|
1854
|
+
listeners = withListener(listeners, listener);
|
|
1050
1855
|
let removed = false;
|
|
1051
1856
|
return () => {
|
|
1052
1857
|
if (removed) {
|
|
1053
1858
|
return;
|
|
1054
1859
|
}
|
|
1055
1860
|
removed = true;
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1861
|
+
listeners = withoutListener(listeners, listener);
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function onUnhandled(listener: UnhandledListener<E>): () => void {
|
|
1866
|
+
unhandledListeners = withListener(unhandledListeners, listener);
|
|
1867
|
+
let removed = false;
|
|
1868
|
+
return () => {
|
|
1869
|
+
if (removed) {
|
|
1870
|
+
return;
|
|
1065
1871
|
}
|
|
1066
|
-
|
|
1872
|
+
removed = true;
|
|
1873
|
+
unhandledListeners = withoutListener(unhandledListeners, listener);
|
|
1067
1874
|
};
|
|
1068
1875
|
}
|
|
1069
1876
|
|
|
@@ -1072,10 +1879,12 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1072
1879
|
to: string | undefined,
|
|
1073
1880
|
cause: MoveCause,
|
|
1074
1881
|
event: E | undefined,
|
|
1882
|
+
source?: string,
|
|
1883
|
+
key?: number,
|
|
1075
1884
|
): void {
|
|
1076
1885
|
const current = listeners;
|
|
1077
1886
|
for (let i = 0; i < current.length; i++) {
|
|
1078
|
-
(current[i] as
|
|
1887
|
+
(current[i] as MoveReport<E>)(from, to, cause, event, source, key);
|
|
1079
1888
|
}
|
|
1080
1889
|
}
|
|
1081
1890
|
|
|
@@ -1112,7 +1921,12 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1112
1921
|
isActive[state] = true;
|
|
1113
1922
|
activeChild[state] = NO_STATE;
|
|
1114
1923
|
elapsed[state] = 0;
|
|
1115
|
-
fired[state]
|
|
1924
|
+
const flags = fired[state];
|
|
1925
|
+
if (flags !== undefined) {
|
|
1926
|
+
for (let i = 0; i < flags.length; i++) {
|
|
1927
|
+
flags[i] = false;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1116
1930
|
entryCount++;
|
|
1117
1931
|
const id = entryCount;
|
|
1118
1932
|
entryId[state] = id;
|
|
@@ -1121,6 +1935,7 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1121
1935
|
if (hook !== undefined) {
|
|
1122
1936
|
hook(ctx, instance);
|
|
1123
1937
|
}
|
|
1938
|
+
readDelays(state);
|
|
1124
1939
|
const task = config.task;
|
|
1125
1940
|
if (task !== undefined) {
|
|
1126
1941
|
// A path, not an index, so a finish held across a rebind still finds its entry.
|
|
@@ -1244,6 +2059,26 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1244
2059
|
}
|
|
1245
2060
|
}
|
|
1246
2061
|
|
|
2062
|
+
// For a state already entered, at a start with a list of paths: chosen is what
|
|
2063
|
+
// startChildren read from the list.
|
|
2064
|
+
function enterAt(state: number, chosen: number[]): void {
|
|
2065
|
+
if (parallel[state] === true) {
|
|
2066
|
+
const list = children[state] as number[];
|
|
2067
|
+
for (let i = 0; i < list.length; i++) {
|
|
2068
|
+
enterState(list[i] as number);
|
|
2069
|
+
enterAt(list[i] as number, chosen);
|
|
2070
|
+
}
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
const child = chosen[state] as number;
|
|
2074
|
+
if (child === NO_STATE) {
|
|
2075
|
+
enterDefaults(state, 0);
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
enterState(child);
|
|
2079
|
+
enterAt(child, chosen);
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1247
2082
|
function enterMissing(state: number): void {
|
|
1248
2083
|
if (parallel[state] === true) {
|
|
1249
2084
|
const list = children[state] as number[];
|
|
@@ -1286,6 +2121,7 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1286
2121
|
run: readonly MoveAction<Ctx, E>[] | undefined,
|
|
1287
2122
|
event: E | undefined,
|
|
1288
2123
|
cause: MoveCause,
|
|
2124
|
+
key?: number,
|
|
1289
2125
|
): void {
|
|
1290
2126
|
if (target === NO_STATE) {
|
|
1291
2127
|
runActions(run, event);
|
|
@@ -1306,7 +2142,7 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1306
2142
|
refreshLeaves();
|
|
1307
2143
|
moved = true;
|
|
1308
2144
|
if (listeners.length > 0) {
|
|
1309
|
-
report(from, firstLeaf(domain), cause, event);
|
|
2145
|
+
report(from, firstLeaf(domain), cause, event, paths[source] as string, key);
|
|
1310
2146
|
}
|
|
1311
2147
|
}
|
|
1312
2148
|
|
|
@@ -1334,14 +2170,14 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1334
2170
|
const list = always[state] as Transition<Ctx, E>[];
|
|
1335
2171
|
for (let i = 0; i < list.length; i++) {
|
|
1336
2172
|
const candidate = list[i] as Transition<Ctx, E>;
|
|
1337
|
-
const when = candidate.when as ((ctx: Ctx) => boolean) | undefined;
|
|
1338
|
-
if (when === undefined || when(ctx)) {
|
|
2173
|
+
const when = candidate.when as ((ctx: Ctx, machine: MachineView) => boolean) | undefined;
|
|
2174
|
+
if (when === undefined || when(ctx, instance)) {
|
|
1339
2175
|
if (overLimit) {
|
|
1340
2176
|
clearQueue();
|
|
1341
2177
|
busy = false;
|
|
1342
2178
|
throw `hsm: state "${describePath(paths[state] as string)}" took ${ALWAYS_LIMIT} always transitions in a row; check for an always loop`;
|
|
1343
2179
|
}
|
|
1344
|
-
transition(state, candidate.target,
|
|
2180
|
+
transition(state, candidate.target, candidate.run, undefined, "always", i);
|
|
1345
2181
|
return true;
|
|
1346
2182
|
}
|
|
1347
2183
|
}
|
|
@@ -1352,7 +2188,14 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1352
2188
|
run: readonly MoveAction<Ctx, E>[] | undefined,
|
|
1353
2189
|
event: E | undefined,
|
|
1354
2190
|
): void {
|
|
1355
|
-
if (run === undefined
|
|
2191
|
+
if (run === undefined) {
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
if (event === undefined) {
|
|
2195
|
+
// Only a rule outside `on` runs with no event, and its entries are RunActions.
|
|
2196
|
+
for (let i = 0; i < run.length; i++) {
|
|
2197
|
+
(run[i] as unknown as RunAction<Ctx, E>)(ctx, instance);
|
|
2198
|
+
}
|
|
1356
2199
|
return;
|
|
1357
2200
|
}
|
|
1358
2201
|
for (let i = 0; i < run.length; i++) {
|
|
@@ -1368,8 +2211,8 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1368
2211
|
const list = transitions[listIndex] as Transition<Ctx, E>[];
|
|
1369
2212
|
for (let i = 0; i < list.length; i++) {
|
|
1370
2213
|
const candidate = list[i] as Transition<Ctx, E>;
|
|
1371
|
-
if (candidate.when === undefined || candidate.when(ctx, event)) {
|
|
1372
|
-
transition(state, candidate.target, candidate.run, event, "event");
|
|
2214
|
+
if (candidate.when === undefined || candidate.when(ctx, event, instance)) {
|
|
2215
|
+
transition(state, candidate.target, candidate.run, event, "event", i);
|
|
1373
2216
|
return true;
|
|
1374
2217
|
}
|
|
1375
2218
|
}
|
|
@@ -1402,6 +2245,42 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1402
2245
|
return takeEvent(state, event);
|
|
1403
2246
|
}
|
|
1404
2247
|
|
|
2248
|
+
// offerEvent's walk with no transition: the first region that would take the event ends
|
|
2249
|
+
// the question, and one that would not leaves the state the next region's guards read.
|
|
2250
|
+
// A hook can ask while a parallel state is half entered or half left, so a region answers
|
|
2251
|
+
// only once it is active.
|
|
2252
|
+
function wouldTake(state: number, event: E): boolean {
|
|
2253
|
+
if (parallel[state] === true) {
|
|
2254
|
+
const list = children[state] as number[];
|
|
2255
|
+
for (let i = 0; i < list.length; i++) {
|
|
2256
|
+
const region = list[i] as number;
|
|
2257
|
+
if (isActive[region] === true && wouldTake(region, event)) {
|
|
2258
|
+
return true;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
const child = activeChild[state] as number;
|
|
2263
|
+
if (child !== NO_STATE && wouldTake(child, event)) {
|
|
2264
|
+
return true;
|
|
2265
|
+
}
|
|
2266
|
+
const listIndex = (onIndex[state] as { [type: string]: number })[event.type];
|
|
2267
|
+
if (typeof listIndex !== "number") {
|
|
2268
|
+
return false;
|
|
2269
|
+
}
|
|
2270
|
+
const list = transitions[listIndex] as Transition<Ctx, E>[];
|
|
2271
|
+
for (let i = 0; i < list.length; i++) {
|
|
2272
|
+
const when = (list[i] as Transition<Ctx, E>).when;
|
|
2273
|
+
if (when === undefined || when(ctx, event, instance)) {
|
|
2274
|
+
return true;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
return false;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function can(event: E): boolean {
|
|
2281
|
+
return running && !stopRequested && wouldTake(ROOT, event);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
1405
2284
|
function clearQueue(): void {
|
|
1406
2285
|
while (queueHead < queueTail) {
|
|
1407
2286
|
queue[queueHead] = undefined;
|
|
@@ -1424,13 +2303,38 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1424
2303
|
}
|
|
1425
2304
|
}
|
|
1426
2305
|
|
|
2306
|
+
// No hook can move the machine, so the active states are the same from the first call to
|
|
2307
|
+
// the last, and the walk checks neither stopRequested nor entryId.
|
|
2308
|
+
function fireUnhandledHooks(state: number, event: E): void {
|
|
2309
|
+
if (parallel[state] === true) {
|
|
2310
|
+
const list = children[state] as number[];
|
|
2311
|
+
for (let i = 0; i < list.length; i++) {
|
|
2312
|
+
fireUnhandledHooks(list[i] as number, event);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
const child = activeChild[state] as number;
|
|
2316
|
+
if (child !== NO_STATE) {
|
|
2317
|
+
fireUnhandledHooks(child, event);
|
|
2318
|
+
}
|
|
2319
|
+
const hook = (configs[state] as StateConfig<Ctx, E>).unhandled;
|
|
2320
|
+
if (hook !== undefined) {
|
|
2321
|
+
hook(ctx, event, instance);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
1427
2325
|
function endStep(): void {
|
|
1428
2326
|
while (!stopRequested && queueHead < queueTail) {
|
|
1429
2327
|
const event = queue[queueHead] as E;
|
|
1430
2328
|
queue[queueHead] = undefined;
|
|
1431
2329
|
queueHead++;
|
|
1432
2330
|
moved = false;
|
|
1433
|
-
offerEvent(ROOT, event)
|
|
2331
|
+
if (!offerEvent(ROOT, event)) {
|
|
2332
|
+
fireUnhandledHooks(ROOT, event);
|
|
2333
|
+
const current = unhandledListeners;
|
|
2334
|
+
for (let i = 0; i < current.length; i++) {
|
|
2335
|
+
(current[i] as UnhandledListener<E>)(event);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
1434
2338
|
if (moved) {
|
|
1435
2339
|
settleAlways();
|
|
1436
2340
|
}
|
|
@@ -1518,18 +2422,50 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1518
2422
|
if (child !== NO_STATE && fireTimers(child)) {
|
|
1519
2423
|
return true;
|
|
1520
2424
|
}
|
|
1521
|
-
const
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
(
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
2425
|
+
const runAt = nextRuns[state];
|
|
2426
|
+
if (runAt !== undefined) {
|
|
2427
|
+
const own = ownEvery[state] as number[];
|
|
2428
|
+
const runs = everyRuns[state] as RunAction<Ctx, E>[][];
|
|
2429
|
+
const clock = elapsed[state] as number;
|
|
2430
|
+
for (let i = 0; i < runAt.length; i++) {
|
|
2431
|
+
if ((runAt[i] as number) <= clock) {
|
|
2432
|
+
// Set before the run and from the clock, not from the last run: a call that covers
|
|
2433
|
+
// several delays runs the entry once and leaves no drift.
|
|
2434
|
+
runAt[i] = nextRun(clock, own[i] as number);
|
|
2435
|
+
const run = runs[i] as RunAction<Ctx, E>[];
|
|
2436
|
+
for (let action = 0; action < run.length; action++) {
|
|
2437
|
+
(run[action] as RunAction<Ctx, E>)(ctx, instance);
|
|
2438
|
+
}
|
|
2439
|
+
if (stopRequested) {
|
|
2440
|
+
return true;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
const flags = fired[state];
|
|
2446
|
+
if (flags === undefined) {
|
|
2447
|
+
return false;
|
|
2448
|
+
}
|
|
2449
|
+
const delays = ownDelays[state] || (afterDelays[state] as number[]);
|
|
2450
|
+
const rules = ownRules[state] || (afterRules[state] as Transition<Ctx, E>[]);
|
|
2451
|
+
const due = elapsed[state] as number;
|
|
2452
|
+
for (let i = 0; i < delays.length && (delays[i] as number) <= due; i++) {
|
|
2453
|
+
if (flags[i] !== true) {
|
|
2454
|
+
const rule = rules[i] as Transition<Ctx, E>;
|
|
2455
|
+
const when = rule.when as ((ctx: Ctx, machine: MachineView) => boolean) | undefined;
|
|
2456
|
+
if (when === undefined || when(ctx, instance)) {
|
|
2457
|
+
// Set before the move: a `to` naming this state re-enters it, which clears the flags.
|
|
2458
|
+
flags[i] = true;
|
|
2459
|
+
if (rule.target !== NO_STATE) {
|
|
2460
|
+
transition(state, rule.target, rule.run, undefined, "after", rule.key);
|
|
2461
|
+
return true;
|
|
2462
|
+
}
|
|
2463
|
+
runActions(rule.run, undefined);
|
|
2464
|
+
if (stopRequested) {
|
|
2465
|
+
return true;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
1533
2469
|
}
|
|
1534
2470
|
return false;
|
|
1535
2471
|
}
|
|
@@ -1569,10 +2505,32 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1569
2505
|
return state !== undefined && state !== ROOT && isActive[state] === true;
|
|
1570
2506
|
}
|
|
1571
2507
|
|
|
2508
|
+
function hasTag(tag: string): boolean {
|
|
2509
|
+
const carriers = compiled.tagStates[tag];
|
|
2510
|
+
if (carriers === undefined) {
|
|
2511
|
+
return false;
|
|
2512
|
+
}
|
|
2513
|
+
for (let i = 0; i < carriers.length; i++) {
|
|
2514
|
+
if (isActive[carriers[i] as number] === true) {
|
|
2515
|
+
return true;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
return false;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
// The whole list is read before any hook runs, so a hook that changes it changes nothing.
|
|
2522
|
+
let chosen: number[] | undefined;
|
|
2523
|
+
if (at !== undefined) {
|
|
2524
|
+
chosen = startChildren(compiled, at);
|
|
2525
|
+
}
|
|
1572
2526
|
busy = true;
|
|
1573
2527
|
fillPrivate(ctx as never, configs[ROOT], false);
|
|
1574
2528
|
enterState(ROOT);
|
|
1575
|
-
|
|
2529
|
+
if (chosen === undefined) {
|
|
2530
|
+
enterDefaults(ROOT, 0);
|
|
2531
|
+
} else {
|
|
2532
|
+
enterAt(ROOT, chosen);
|
|
2533
|
+
}
|
|
1576
2534
|
refreshLeaves();
|
|
1577
2535
|
settleAlways();
|
|
1578
2536
|
endStep();
|
|
@@ -1603,6 +2561,11 @@ function define<Ctx, E extends EventObject>(
|
|
|
1603
2561
|
return machine;
|
|
1604
2562
|
}
|
|
1605
2563
|
|
|
2564
|
+
// Ctx for a config without privateCtx, Ctx & Priv for one with it. The tuple test keeps the
|
|
2565
|
+
// context whole while Priv has no inference yet, so a generic helper in a slot, such as
|
|
2566
|
+
// `sequence`, still takes its ctx from the slot.
|
|
2567
|
+
type WithPrivate<Ctx, Priv> = [Priv] extends [never] ? Ctx : Ctx & Priv;
|
|
2568
|
+
|
|
1606
2569
|
// Ctx and E are given explicitly and the config is inferred, so the config takes a second call.
|
|
1607
2570
|
// A key makes a later definition under it (a hot-reloaded module re-running) rebind live instances.
|
|
1608
2571
|
/**
|
|
@@ -1611,23 +2574,97 @@ function define<Ctx, E extends EventObject>(
|
|
|
1611
2574
|
* when that call runs, and a mistake throws, naming the state path. With a key, a later
|
|
1612
2575
|
* definition under it puts its states into the machine already built, so every running instance
|
|
1613
2576
|
* sees the edit; without one, each call builds a separate machine.
|
|
2577
|
+
*
|
|
2578
|
+
* A third type argument, as in `defineMachine<Ctx, E, Tag>()`, is a union of tag names: a state's
|
|
2579
|
+
* `tags` lists some of them, and `hasTag` asks whether an active state carries one. A machine
|
|
2580
|
+
* defined without it has no tags.
|
|
2581
|
+
*
|
|
2582
|
+
* The root config may hold `privateCtx`, for a machine that keeps fields of its own, such as a
|
|
2583
|
+
* counter, a heat level or a list it builds up. Every hook, `when`, `run`, `task` and `update`
|
|
2584
|
+
* then sees those fields beside `Ctx`, while `start` takes only `Ctx` and the instance it returns
|
|
2585
|
+
* shows only `Ctx` in its `ctx`. A machine without fields of its own leaves `privateCtx` out.
|
|
1614
2586
|
*/
|
|
1615
|
-
export function defineMachine<Ctx, E extends EventObject>(
|
|
2587
|
+
export function defineMachine<Ctx, E extends EventObject, Tag extends string = never>(
|
|
1616
2588
|
key?: string,
|
|
1617
|
-
): <T
|
|
2589
|
+
): <T, Priv extends object = never, NT = unknown, HT = unknown>(
|
|
2590
|
+
config: RootState<WithPrivate<Ctx, Priv>, E, T, Tag, NT, HT> & PrivateCtxConfig<Ctx, Priv>,
|
|
2591
|
+
) => Machine<Ctx, E, TreePath<T>, Tag> {
|
|
1618
2592
|
return ((config: MachineConfig<Ctx, E>) => define(key, config)) as never;
|
|
1619
2593
|
}
|
|
1620
2594
|
|
|
1621
2595
|
/**
|
|
1622
|
-
*
|
|
2596
|
+
* Makes a group that counts the engine work a `task` started and tells when all of it has
|
|
2597
|
+
* finished. Each `add()` returns the callback to pass to one engine call, and `wait` runs its
|
|
2598
|
+
* callback once every one of them has reported.
|
|
2599
|
+
*
|
|
2600
|
+
* @example
|
|
2601
|
+
* ```ts
|
|
2602
|
+
* loading: {
|
|
2603
|
+
* task: (ctx, finish) => {
|
|
2604
|
+
* const group = waitGroup();
|
|
2605
|
+
* http.request(ctx.url, "GET", group.add());
|
|
2606
|
+
* go.animate(
|
|
2607
|
+
* ctx.curtain,
|
|
2608
|
+
* "tint.w",
|
|
2609
|
+
* go.PLAYBACK_ONCE_FORWARD,
|
|
2610
|
+
* 0,
|
|
2611
|
+
* go.EASING_LINEAR,
|
|
2612
|
+
* 0.5,
|
|
2613
|
+
* 0,
|
|
2614
|
+
* group.add(),
|
|
2615
|
+
* );
|
|
2616
|
+
* group.wait(() => finish({ type: "READY" }));
|
|
2617
|
+
* },
|
|
2618
|
+
* on: { READY: "/playing" },
|
|
2619
|
+
* },
|
|
2620
|
+
* ```
|
|
2621
|
+
*/
|
|
2622
|
+
export function waitGroup(): WaitGroup {
|
|
2623
|
+
let pending = 0;
|
|
2624
|
+
let waiting: (() => void)[] = [];
|
|
2625
|
+
return {
|
|
2626
|
+
add: () => {
|
|
2627
|
+
pending++;
|
|
2628
|
+
let reported = false;
|
|
2629
|
+
return () => {
|
|
2630
|
+
if (reported) {
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
reported = true;
|
|
2634
|
+
pending--;
|
|
2635
|
+
if (pending === 0 && waiting.length > 0) {
|
|
2636
|
+
// Taken before the calls: an add or a wait a callback makes belongs to the next round.
|
|
2637
|
+
const ready = waiting;
|
|
2638
|
+
waiting = [];
|
|
2639
|
+
for (const callback of ready) {
|
|
2640
|
+
callback();
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
},
|
|
2645
|
+
wait: (callback) => {
|
|
2646
|
+
if (pending === 0) {
|
|
2647
|
+
callback();
|
|
2648
|
+
} else {
|
|
2649
|
+
waiting.push(callback);
|
|
2650
|
+
}
|
|
2651
|
+
},
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
/**
|
|
2656
|
+
* The optional key a `defineMachine` root config takes beside `initial` and `states`.
|
|
1623
2657
|
* @noSelf
|
|
1624
2658
|
*/
|
|
1625
2659
|
export interface PrivateCtxConfig<Ctx, Priv> {
|
|
1626
2660
|
/**
|
|
1627
2661
|
* Builds the fields only the machine uses. It runs once per `start`, before the root's `enter`
|
|
1628
2662
|
* hook, so every instance gets fresh fields of its own; a value written straight into the
|
|
1629
|
-
* config would be one array or table shared by every instance. It
|
|
1630
|
-
* a private field from a field of `Ctx
|
|
2663
|
+
* config would be one array or table shared by every instance. It receives the object passed
|
|
2664
|
+
* to `start`, typed `Ctx`, so a private field can start from a field of `Ctx`:
|
|
2665
|
+
* `privateCtx: (ctx: TurretOptions) => ({ heat: ctx.startHeat })`. Give `ctx` its type; without
|
|
2666
|
+
* one TypeScript can type a hook before it knows the private fields, and that hook reports the
|
|
2667
|
+
* field as missing.
|
|
1631
2668
|
*
|
|
1632
2669
|
* The fields are added to the object passed to `start`, which stays the instance's `ctx`, so
|
|
1633
2670
|
* restarting with `start(old.ctx)` keeps the public fields and builds the private ones fresh.
|
|
@@ -1637,94 +2674,81 @@ export interface PrivateCtxConfig<Ctx, Priv> {
|
|
|
1637
2674
|
*
|
|
1638
2675
|
* The fields are hidden from callers by type only. At run time `ctx` is one table, and hooks,
|
|
1639
2676
|
* `hsm-view` and a debugger see every field.
|
|
2677
|
+
*
|
|
2678
|
+
* There are two ways to type the fields.
|
|
2679
|
+
*
|
|
2680
|
+
* @example
|
|
2681
|
+
* **Inferred.** For a few fields, write them inline and let TypeScript infer their types. Add
|
|
2682
|
+
* `as` where the inferred type is wrong: an empty array is `never[]` until it reads
|
|
2683
|
+
* `[] as Hash[]`, and a field holding one of several strings is any `string` until it reads
|
|
2684
|
+
* `"cool" as "cool" | "hot"`.
|
|
2685
|
+
*
|
|
2686
|
+
* ```ts
|
|
2687
|
+
* type TurretEvent = { type: "SPOTTED" } | { type: "LOST" };
|
|
2688
|
+
*
|
|
2689
|
+
* export const turret = defineMachine<{ readonly fireRate: number }, TurretEvent>("turret")({
|
|
2690
|
+
* privateCtx: () => ({ heat: 0, targets: [] as Hash[] }),
|
|
2691
|
+
* initial: "/idle",
|
|
2692
|
+
* states: {
|
|
2693
|
+
* idle: { on: { SPOTTED: "/firing" } },
|
|
2694
|
+
* firing: {
|
|
2695
|
+
* enter: (ctx) => {
|
|
2696
|
+
* ctx.heat += 1 / ctx.fireRate;
|
|
2697
|
+
* },
|
|
2698
|
+
* on: { LOST: "/idle" },
|
|
2699
|
+
* },
|
|
2700
|
+
* },
|
|
2701
|
+
* });
|
|
2702
|
+
*
|
|
2703
|
+
* const gun = turret.start({ fireRate: 2 }); // gun.ctx shows fireRate only
|
|
2704
|
+
* ```
|
|
2705
|
+
*
|
|
2706
|
+
* @example
|
|
2707
|
+
* **A named type.** For more fields, or when a helper outside the config needs the whole
|
|
2708
|
+
* context, declare the fields as an interface and make it the factory's return type. The
|
|
2709
|
+
* fields need no `as`, a misspelled or unknown field is a compile error, and the whole context
|
|
2710
|
+
* is `TurretOptions & TurretPrivate`. `satisfies TurretPrivate` does not do this: it checks the
|
|
2711
|
+
* object but keeps its narrow types, so an empty array stays `never[]`.
|
|
2712
|
+
*
|
|
2713
|
+
* ```ts
|
|
2714
|
+
* interface TurretOptions {
|
|
2715
|
+
* readonly fireRate: number;
|
|
2716
|
+
* }
|
|
2717
|
+
*
|
|
2718
|
+
* interface TurretPrivate {
|
|
2719
|
+
* heat: number;
|
|
2720
|
+
* mode: "cool" | "hot";
|
|
2721
|
+
* }
|
|
2722
|
+
*
|
|
2723
|
+
* type TurretEvent = { type: "SPOTTED" } | { type: "LOST" };
|
|
2724
|
+
*
|
|
2725
|
+
* const overheated = (ctx: TurretOptions & TurretPrivate) => ctx.heat > ctx.fireRate;
|
|
2726
|
+
*
|
|
2727
|
+
* export const turret = defineMachine<TurretOptions, TurretEvent>("turret")({
|
|
2728
|
+
* privateCtx: (): TurretPrivate => ({ heat: 0, mode: "cool" }),
|
|
2729
|
+
* initial: "/idle",
|
|
2730
|
+
* states: {
|
|
2731
|
+
* idle: { on: { SPOTTED: "/firing" } },
|
|
2732
|
+
* firing: {
|
|
2733
|
+
* enter: (ctx) => {
|
|
2734
|
+
* ctx.heat += 1 / ctx.fireRate;
|
|
2735
|
+
* },
|
|
2736
|
+
* always: { to: "/cooling", when: overheated },
|
|
2737
|
+
* on: { LOST: "/idle" },
|
|
2738
|
+
* },
|
|
2739
|
+
* cooling: {
|
|
2740
|
+
* enter: (ctx) => {
|
|
2741
|
+
* ctx.mode = "hot";
|
|
2742
|
+
* },
|
|
2743
|
+
* exit: (ctx) => {
|
|
2744
|
+
* ctx.heat = 0;
|
|
2745
|
+
* ctx.mode = "cool";
|
|
2746
|
+
* },
|
|
2747
|
+
* after: { 2: "/idle" },
|
|
2748
|
+
* },
|
|
2749
|
+
* },
|
|
2750
|
+
* });
|
|
2751
|
+
* ```
|
|
1640
2752
|
*/
|
|
1641
|
-
readonly privateCtx:
|
|
2753
|
+
readonly privateCtx?: (ctx: Ctx) => Priv & { readonly [K in keyof Ctx & keyof Priv]: never };
|
|
1642
2754
|
}
|
|
1643
|
-
|
|
1644
|
-
/**
|
|
1645
|
-
* Like `defineMachine`, for a machine that keeps fields of its own, such as a counter, a heat
|
|
1646
|
-
* level or a list it builds up. The config's `privateCtx` returns those fields. Every hook,
|
|
1647
|
-
* `when`, `run`, `task` and `update` sees them beside `Ctx`, while `start` takes only `Ctx` and
|
|
1648
|
-
* the instance it returns shows only `Ctx` in its `ctx`. A machine without fields of its own
|
|
1649
|
-
* uses `defineMachine`.
|
|
1650
|
-
*
|
|
1651
|
-
* There are two ways to type the fields.
|
|
1652
|
-
*
|
|
1653
|
-
* @example
|
|
1654
|
-
* **Inferred.** For a few fields, write them inline and let TypeScript infer their types. Add
|
|
1655
|
-
* `as` where the inferred type is wrong: an empty array is `never[]` until it reads
|
|
1656
|
-
* `[] as Hash[]`, and a field holding one of several strings is any `string` until it reads
|
|
1657
|
-
* `"cool" as "cool" | "hot"`.
|
|
1658
|
-
*
|
|
1659
|
-
* ```ts
|
|
1660
|
-
* type TurretEvent = { type: "SPOTTED" } | { type: "LOST" };
|
|
1661
|
-
*
|
|
1662
|
-
* export const turret = definePrivateMachine<{ readonly fireRate: number }, TurretEvent>("turret")({
|
|
1663
|
-
* privateCtx: () => ({ heat: 0, targets: [] as Hash[] }),
|
|
1664
|
-
* initial: "/idle",
|
|
1665
|
-
* states: {
|
|
1666
|
-
* idle: { on: { SPOTTED: "/firing" } },
|
|
1667
|
-
* firing: {
|
|
1668
|
-
* enter: (ctx) => {
|
|
1669
|
-
* ctx.heat += 1 / ctx.fireRate;
|
|
1670
|
-
* },
|
|
1671
|
-
* on: { LOST: "/idle" },
|
|
1672
|
-
* },
|
|
1673
|
-
* },
|
|
1674
|
-
* });
|
|
1675
|
-
*
|
|
1676
|
-
* const gun = turret.start({ fireRate: 2 }); // gun.ctx shows fireRate only
|
|
1677
|
-
* ```
|
|
1678
|
-
*
|
|
1679
|
-
* @example
|
|
1680
|
-
* **A named type.** For more fields, or when a helper outside the config needs the whole
|
|
1681
|
-
* context, declare the fields as an interface and make it the factory's return type. The fields
|
|
1682
|
-
* need no `as`, a misspelled or unknown field is a compile error, and the whole context is
|
|
1683
|
-
* `TurretOptions & TurretPrivate`. `satisfies TurretPrivate` does not do this: it checks the
|
|
1684
|
-
* object but keeps its narrow types, so an empty array stays `never[]`.
|
|
1685
|
-
*
|
|
1686
|
-
* ```ts
|
|
1687
|
-
* interface TurretOptions {
|
|
1688
|
-
* readonly fireRate: number;
|
|
1689
|
-
* }
|
|
1690
|
-
*
|
|
1691
|
-
* interface TurretPrivate {
|
|
1692
|
-
* heat: number;
|
|
1693
|
-
* mode: "cool" | "hot";
|
|
1694
|
-
* }
|
|
1695
|
-
*
|
|
1696
|
-
* type TurretEvent = { type: "SPOTTED" } | { type: "LOST" };
|
|
1697
|
-
*
|
|
1698
|
-
* const overheated = (ctx: TurretOptions & TurretPrivate) => ctx.heat > ctx.fireRate;
|
|
1699
|
-
*
|
|
1700
|
-
* export const turret = definePrivateMachine<TurretOptions, TurretEvent>("turret")({
|
|
1701
|
-
* privateCtx: (): TurretPrivate => ({ heat: 0, mode: "cool" }),
|
|
1702
|
-
* initial: "/idle",
|
|
1703
|
-
* states: {
|
|
1704
|
-
* idle: { on: { SPOTTED: "/firing" } },
|
|
1705
|
-
* firing: {
|
|
1706
|
-
* enter: (ctx) => {
|
|
1707
|
-
* ctx.heat += 1 / ctx.fireRate;
|
|
1708
|
-
* },
|
|
1709
|
-
* always: { to: "/cooling", when: overheated },
|
|
1710
|
-
* on: { LOST: "/idle" },
|
|
1711
|
-
* },
|
|
1712
|
-
* cooling: {
|
|
1713
|
-
* enter: (ctx) => {
|
|
1714
|
-
* ctx.mode = "hot";
|
|
1715
|
-
* },
|
|
1716
|
-
* exit: (ctx) => {
|
|
1717
|
-
* ctx.heat = 0;
|
|
1718
|
-
* ctx.mode = "cool";
|
|
1719
|
-
* },
|
|
1720
|
-
* after: { 2: "/idle" },
|
|
1721
|
-
* },
|
|
1722
|
-
* },
|
|
1723
|
-
* });
|
|
1724
|
-
* ```
|
|
1725
|
-
*/
|
|
1726
|
-
export const definePrivateMachine: <Ctx, E extends EventObject>(
|
|
1727
|
-
key?: string,
|
|
1728
|
-
) => <T, Priv extends object>(
|
|
1729
|
-
config: RootState<Ctx & Priv, E, T> & PrivateCtxConfig<Ctx, Priv>,
|
|
1730
|
-
) => Machine<Ctx, E, TreePath<T>> = defineMachine as never;
|