@defold-typescript/transpiler 0.48.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 +1133 -204
- 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,80 +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>>;
|
|
170
286
|
|
|
171
287
|
/**
|
|
172
|
-
*
|
|
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.
|
|
173
292
|
* @noSelf
|
|
174
293
|
*/
|
|
175
|
-
export interface
|
|
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> {
|
|
176
331
|
/**
|
|
177
332
|
* The delay in seconds of game time: a number, or a function of the context that returns one.
|
|
178
333
|
* A function is read once each time the state is entered, after its `enter` hook, and again on
|
|
179
|
-
* a hot reload, so the delay is fixed for that stay.
|
|
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
|
|
180
356
|
* throws, naming the state.
|
|
181
357
|
*/
|
|
182
|
-
readonly delay: number | ((ctx: Ctx) => number);
|
|
183
|
-
/**
|
|
184
|
-
readonly
|
|
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;
|
|
185
365
|
}
|
|
186
366
|
|
|
187
367
|
/**
|
|
188
368
|
* A state's rules by event `type`. Only the machine's event types are keys, and each rule's
|
|
189
369
|
* `when` and `run` see the event variant its key names.
|
|
190
370
|
*/
|
|
191
|
-
export type OnConfig<
|
|
192
|
-
|
|
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>;
|
|
193
379
|
};
|
|
194
380
|
|
|
195
381
|
/**
|
|
196
382
|
* Runs on every way in (`enter`) or out (`exit`) of the state: a move, `start` and `stop`. A hot
|
|
197
383
|
* reload enters the states it adds, but drops a removed state without its `exit`. It gets no
|
|
198
|
-
* 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.
|
|
199
387
|
*/
|
|
200
|
-
export type StateHook<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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;
|
|
204
394
|
|
|
205
395
|
/**
|
|
206
396
|
* Runs on every `update(dt)` while the state is active, deepest state first. It returns a full
|
|
207
397
|
* path to move there, or `undefined` to pass. The first hook to return a path wins: the hooks
|
|
208
|
-
* 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.
|
|
209
403
|
*/
|
|
210
|
-
export type UpdateHook<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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;
|
|
215
427
|
|
|
216
428
|
/**
|
|
217
429
|
* Starts work that lives as long as the state. `finish` sends one event and is ignored after the
|
|
218
|
-
* 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.
|
|
219
434
|
* @noSelf
|
|
220
435
|
*/
|
|
221
|
-
export type TaskStart<
|
|
436
|
+
export type TaskStart<
|
|
437
|
+
Ctx,
|
|
438
|
+
E extends EventObject,
|
|
439
|
+
Tag extends string = never,
|
|
440
|
+
Hook extends string = string,
|
|
441
|
+
> = (
|
|
222
442
|
ctx: Ctx,
|
|
223
443
|
finish: (event: E) => void,
|
|
224
|
-
machine: MachineInstance<Ctx, E>,
|
|
444
|
+
machine: MachineInstance<Ctx, E, Hook, Tag>,
|
|
225
445
|
// biome-ignore lint/suspicious/noConfusingVoidType: `void` keeps an expression-body task returning a void call (`=> go.animate(...)`) valid.
|
|
226
446
|
) => (() => void) | void;
|
|
227
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
|
+
|
|
228
470
|
/**
|
|
229
471
|
* A machine definition, returned by the second `defineMachine` call. Its `start` creates the
|
|
230
472
|
* instances that run.
|
|
231
473
|
* @noSelf
|
|
232
474
|
*/
|
|
233
|
-
export interface Machine<
|
|
475
|
+
export interface Machine<
|
|
476
|
+
Ctx,
|
|
477
|
+
E extends EventObject,
|
|
478
|
+
P extends string = string,
|
|
479
|
+
Tag extends string = never,
|
|
480
|
+
> {
|
|
234
481
|
/**
|
|
235
|
-
* Creates an instance with `ctx` as its context
|
|
236
|
-
*
|
|
237
|
-
*
|
|
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.
|
|
238
488
|
*/
|
|
239
|
-
readonly start: (ctx: Ctx) => MachineInstance<Ctx, E, P>;
|
|
489
|
+
readonly start: (ctx: Ctx, at?: readonly P[]) => MachineInstance<Ctx, E, P, Tag>;
|
|
240
490
|
}
|
|
241
491
|
|
|
242
492
|
type PathDepth = [never, 0, 1, 2, 3];
|
|
@@ -256,19 +506,6 @@ type TreePathsBelow<T, D extends number> = {
|
|
|
256
506
|
|
|
257
507
|
type TreePath<T> = `/${TreePathsBelow<T, 4>}`;
|
|
258
508
|
|
|
259
|
-
// The paths a config slot accepts. TypeScript checks a config once before it types the
|
|
260
|
-
// callbacks, and a level whose states hold only callbacks has no inferred children in that
|
|
261
|
-
// check, so a node with no inferred children accepts any path below it.
|
|
262
|
-
type SlotPathsBelow<T, D extends number> = unknown extends T
|
|
263
|
-
? string
|
|
264
|
-
: {
|
|
265
|
-
[K in keyof T & string]:
|
|
266
|
-
| K
|
|
267
|
-
| `${K}/${D extends 1 ? string : SlotPathsBelow<T[K], PathDepth[D]>}`;
|
|
268
|
-
}[keyof T & string];
|
|
269
|
-
|
|
270
|
-
type SlotPath<T> = `/${SlotPathsBelow<T, 4>}`;
|
|
271
|
-
|
|
272
509
|
type TreeOf<C> = C extends { readonly states: infer Children }
|
|
273
510
|
? { readonly [K in keyof Children]: TreeOf<Children[K]> }
|
|
274
511
|
: unknown;
|
|
@@ -297,68 +534,142 @@ type TreeDepth<T, D extends number> = unknown extends T
|
|
|
297
534
|
: never
|
|
298
535
|
: never);
|
|
299
536
|
|
|
300
|
-
type AnyChild<
|
|
301
|
-
|
|
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> }
|
|
302
546
|
: unknown;
|
|
303
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`.
|
|
304
552
|
// `states` is the only slot that infers the tree; every other slot that mentions it is wrapped
|
|
305
553
|
// in NoInfer. `on`, `after` and `update` are wrapped whole so inference never reads them:
|
|
306
554
|
// without exactOptionalPropertyTypes, reading `on` asks for the paths while the tree is still
|
|
307
555
|
// being inferred, and a valid config fails as circular. `always` is wrapped only through `To`:
|
|
308
556
|
// NoInfer over a union that holds object types hides its string members from completion.
|
|
309
|
-
//
|
|
310
|
-
//
|
|
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.
|
|
311
565
|
/** @noSelf */
|
|
312
|
-
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
|
+
> {
|
|
313
575
|
/** Child states, by name. A name is non-empty and contains no `/`. */
|
|
314
576
|
readonly states?: {
|
|
315
|
-
readonly [K in keyof T]: StateNode<Ctx, E, T[K], `${Self}/${K & string}`, To>;
|
|
316
|
-
} & 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[];
|
|
317
584
|
/**
|
|
318
585
|
* Rules by event `type`, each a target path, a rule object or a list of them. An event starts
|
|
319
586
|
* at the deepest active state and moves up to the parent while no rule takes it.
|
|
320
587
|
*/
|
|
321
|
-
readonly on?: NoInfer<OnConfig<Ctx, E, To>>;
|
|
588
|
+
readonly on?: NoInfer<OnConfig<Ctx, E, To, Tag, Hook>>;
|
|
322
589
|
/**
|
|
323
|
-
* Timers, in one of two forms: targets by delay in seconds of game
|
|
324
|
-
* `{ 2: "/idle" }`, or a list of `AfterConfig` entries, such as
|
|
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
|
|
325
592
|
* `[{ delay: (ctx) => ctx.patrolFor, to: "/rest" }]`, whose `delay` is a number or a function
|
|
326
593
|
* of the context. A delay function is read once each time the state is entered, after its
|
|
327
594
|
* `enter` hook, and again on a hot reload. The delay is counted by `update(dt)` while the state
|
|
328
|
-
* is active, from zero on each entry
|
|
329
|
-
*
|
|
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.
|
|
330
601
|
*/
|
|
331
|
-
readonly after?: NoInfer<
|
|
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
|
+
>;
|
|
332
611
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
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.
|
|
336
625
|
*/
|
|
337
|
-
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
|
+
>;
|
|
634
|
+
/**
|
|
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.
|
|
638
|
+
*/
|
|
639
|
+
readonly always?: AlwaysSpec<Ctx, To, E, Tag, Hook>;
|
|
338
640
|
/**
|
|
339
641
|
* Runs each time the state is entered, outermost state first: on a move, at `start`, and when
|
|
340
642
|
* a hot reload adds the state. It gets no event.
|
|
341
643
|
*/
|
|
342
|
-
readonly enter?: StateHook<Ctx, E>;
|
|
644
|
+
readonly enter?: StateHook<Ctx, E, Tag, Hook>;
|
|
343
645
|
/**
|
|
344
646
|
* Runs each time the state is left, deepest state first, including on `stop()`. A state a hot
|
|
345
647
|
* reload removes is dropped without it.
|
|
346
648
|
*/
|
|
347
|
-
readonly exit?: StateHook<Ctx, E>;
|
|
649
|
+
readonly exit?: StateHook<Ctx, E, Tag, Hook>;
|
|
348
650
|
/**
|
|
349
651
|
* Runs on every `update(dt)` while the state is active. Return one of the machine's full paths
|
|
350
652
|
* to move there, or `undefined` to stay.
|
|
351
653
|
*/
|
|
352
|
-
readonly update?: NoInfer<UpdateHook<Ctx, E, To>>;
|
|
654
|
+
readonly update?: NoInfer<UpdateHook<Ctx, E, To, Tag, Hook>>;
|
|
353
655
|
/** Started on each entry; see `TaskStart`. */
|
|
354
|
-
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>;
|
|
355
659
|
/** Renamed to `task`. */
|
|
356
660
|
readonly invoke?: never;
|
|
357
661
|
}
|
|
358
662
|
|
|
359
663
|
/** @noSelf */
|
|
360
|
-
interface CompoundState<
|
|
361
|
-
|
|
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> {
|
|
362
673
|
/**
|
|
363
674
|
* `"parallel"` keeps every child active at once, each as a region with its own active state.
|
|
364
675
|
* Not allowed on the root.
|
|
@@ -379,8 +690,15 @@ interface CompoundState<Ctx, E extends EventObject, T, Self extends string, To e
|
|
|
379
690
|
}
|
|
380
691
|
|
|
381
692
|
/** @noSelf */
|
|
382
|
-
interface ParallelState<
|
|
383
|
-
|
|
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> {
|
|
384
702
|
/**
|
|
385
703
|
* `"parallel"` keeps every child active at once, each as a region with its own active state.
|
|
386
704
|
* Not allowed on the root.
|
|
@@ -390,51 +708,144 @@ interface ParallelState<Ctx, E extends EventObject, T, Self extends string, To e
|
|
|
390
708
|
readonly restoreDepth?: never;
|
|
391
709
|
}
|
|
392
710
|
|
|
393
|
-
type StateNode<
|
|
394
|
-
| CompoundState<Ctx, E, T, Self, To>
|
|
395
|
-
| ParallelState<Ctx, E, T, Self, To>;
|
|
396
|
-
|
|
397
|
-
type RootState<Ctx, E extends EventObject, T> = CompoundState<
|
|
711
|
+
type StateNode<
|
|
398
712
|
Ctx,
|
|
399
|
-
E,
|
|
713
|
+
E extends EventObject,
|
|
400
714
|
T,
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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>>> & {
|
|
404
787
|
readonly initial: string;
|
|
405
788
|
readonly states: object;
|
|
406
|
-
};
|
|
789
|
+
} & { readonly states: NodeLevel<NT> } & { readonly states: NodeLevel<HT> };
|
|
407
790
|
|
|
408
791
|
/**
|
|
409
792
|
* The config of one state. Every field is optional. A state with child `states` is a compound
|
|
410
793
|
* state and needs an `initial`, unless its `type` is `"parallel"`; a state without children is a
|
|
411
|
-
* 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.
|
|
412
796
|
*/
|
|
413
|
-
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
|
+
>;
|
|
414
805
|
|
|
415
806
|
/**
|
|
416
807
|
* The root config passed to `defineMachine`. It takes the fields of a state and requires
|
|
417
|
-
* `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.
|
|
418
811
|
*/
|
|
419
|
-
export type MachineConfig<Ctx, E extends EventObject> = CompoundState<
|
|
812
|
+
export type MachineConfig<Ctx, E extends EventObject, Tag extends string = never> = CompoundState<
|
|
420
813
|
Ctx,
|
|
421
814
|
E,
|
|
422
815
|
unknown,
|
|
423
816
|
string,
|
|
424
|
-
string
|
|
817
|
+
string,
|
|
818
|
+
Tag
|
|
425
819
|
> & {
|
|
426
820
|
readonly initial: string;
|
|
427
|
-
readonly states: { readonly [name: string]: StateConfig<Ctx, E> };
|
|
821
|
+
readonly states: { readonly [name: string]: StateConfig<Ctx, E, Tag> };
|
|
428
822
|
};
|
|
429
823
|
|
|
430
|
-
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;
|
|
431
825
|
|
|
432
826
|
interface Transition<Ctx, E extends EventObject> {
|
|
433
827
|
readonly target: number;
|
|
434
828
|
readonly when: Guard<Ctx, E> | undefined;
|
|
435
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;
|
|
436
833
|
}
|
|
437
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
|
+
|
|
438
849
|
type Delay<Ctx> = number | ((ctx: Ctx) => number);
|
|
439
850
|
|
|
440
851
|
interface Compiled<Ctx, E extends EventObject> {
|
|
@@ -452,13 +863,18 @@ interface Compiled<Ctx, E extends EventObject> {
|
|
|
452
863
|
readonly onIndex: { [type: string]: number }[];
|
|
453
864
|
readonly transitions: Transition<Ctx, E>[][];
|
|
454
865
|
readonly afterDelays: number[][];
|
|
455
|
-
readonly
|
|
866
|
+
readonly afterRules: Transition<Ctx, E>[][];
|
|
456
867
|
// Set only for a state whose after list holds a delay function: its entries in list order,
|
|
457
868
|
// for each instance to sort once the function is read.
|
|
458
869
|
readonly afterListDelays: (Delay<Ctx>[] | undefined)[];
|
|
459
|
-
readonly
|
|
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>[][][];
|
|
460
874
|
readonly always: Transition<Ctx, E>[][];
|
|
461
875
|
readonly pathIndex: { [path: string]: number };
|
|
876
|
+
// For each tag, the states that carry it.
|
|
877
|
+
readonly tagStates: { [tag: string]: number[] };
|
|
462
878
|
}
|
|
463
879
|
|
|
464
880
|
const ROOT = 0;
|
|
@@ -493,10 +909,13 @@ function resolveTarget<Ctx, E extends EventObject>(
|
|
|
493
909
|
return index;
|
|
494
910
|
}
|
|
495
911
|
|
|
496
|
-
// 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.
|
|
497
914
|
interface StringUnits {
|
|
498
915
|
charCodeAt(index: number): number;
|
|
499
916
|
byte(index: number): number;
|
|
917
|
+
substring(start: number, end: number): string;
|
|
918
|
+
sub(first: number, last: number): string;
|
|
500
919
|
}
|
|
501
920
|
|
|
502
921
|
// JavaScript strings hold UTF-16 code units; Lua strings hold UTF-8 bytes.
|
|
@@ -510,6 +929,36 @@ function unitAt(text: string, index: number): number {
|
|
|
510
929
|
return units.byte(index + 1);
|
|
511
930
|
}
|
|
512
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
|
+
|
|
513
962
|
// Moves surrogates above the rest of the BMP, so UTF-16 units compare in code-point order.
|
|
514
963
|
// UTF-8 bytes already do, and every byte is below the surrogate range.
|
|
515
964
|
function codePointRank(unit: number): number {
|
|
@@ -553,6 +1002,18 @@ function registerState<Ctx, E extends EventObject>(
|
|
|
553
1002
|
compiled.levels[index] = 0;
|
|
554
1003
|
compiled.records[index] = false;
|
|
555
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
|
+
}
|
|
556
1017
|
const children = config.states;
|
|
557
1018
|
if (children === undefined) {
|
|
558
1019
|
return;
|
|
@@ -682,6 +1143,20 @@ function compileRecords<Ctx, E extends EventObject>(compiled: Compiled<Ctx, E>):
|
|
|
682
1143
|
}
|
|
683
1144
|
}
|
|
684
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
|
+
|
|
685
1160
|
function compileTransition<Ctx, E extends EventObject>(
|
|
686
1161
|
compiled: Compiled<Ctx, E>,
|
|
687
1162
|
source: number,
|
|
@@ -694,19 +1169,10 @@ function compileTransition<Ctx, E extends EventObject>(
|
|
|
694
1169
|
run: [],
|
|
695
1170
|
};
|
|
696
1171
|
}
|
|
697
|
-
const run: MoveAction<Ctx, E>[] = [];
|
|
698
|
-
const declared = spec.run;
|
|
699
|
-
if (typeof declared === "function") {
|
|
700
|
-
run.push(declared);
|
|
701
|
-
} else if (declared !== undefined) {
|
|
702
|
-
for (let i = 0; i < declared.length; i++) {
|
|
703
|
-
run.push(declared[i] as MoveAction<Ctx, E>);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
1172
|
return {
|
|
707
1173
|
target: spec.to === undefined ? NO_STATE : resolveTarget(compiled, source, spec.to),
|
|
708
1174
|
when: spec.when,
|
|
709
|
-
run,
|
|
1175
|
+
run: collectRun<MoveAction<Ctx, E>>(spec.run),
|
|
710
1176
|
};
|
|
711
1177
|
}
|
|
712
1178
|
|
|
@@ -750,18 +1216,18 @@ function compileAlways<Ctx, E extends EventObject>(
|
|
|
750
1216
|
// Read as an array type: the transpiler shifts an index to Lua's base 1 only on an array.
|
|
751
1217
|
const spec = (compiled.configs[index] as StateConfig<Ctx, E>).always as
|
|
752
1218
|
| string
|
|
753
|
-
| AlwaysConfig<Ctx>
|
|
754
|
-
| readonly AlwaysConfig<Ctx>[]
|
|
1219
|
+
| AlwaysConfig<Ctx, string, E>
|
|
1220
|
+
| readonly AlwaysConfig<Ctx, string, E>[]
|
|
755
1221
|
| undefined;
|
|
756
1222
|
if (spec === undefined) {
|
|
757
1223
|
return;
|
|
758
1224
|
}
|
|
759
|
-
const candidates: readonly (string | AlwaysConfig<Ctx>)[] =
|
|
760
|
-
typeof spec === "string" || (spec as readonly AlwaysConfig<Ctx>[])[0] === undefined
|
|
761
|
-
? [spec as string | AlwaysConfig<Ctx>]
|
|
762
|
-
: (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>[]);
|
|
763
1229
|
for (let i = 0; i < candidates.length; i++) {
|
|
764
|
-
const candidate = candidates[i] as string | AlwaysConfig<Ctx>;
|
|
1230
|
+
const candidate = candidates[i] as string | AlwaysConfig<Ctx, string, E>;
|
|
765
1231
|
if (typeof candidate !== "string" && candidate.to === undefined) {
|
|
766
1232
|
throw `hsm: state "${describePath(compiled.paths[index] as string)}" has an always transition with no "to"`;
|
|
767
1233
|
}
|
|
@@ -796,45 +1262,79 @@ function checkDelay(path: string, delay: unknown, written: unknown): void {
|
|
|
796
1262
|
}
|
|
797
1263
|
}
|
|
798
1264
|
|
|
799
|
-
// Keeps the delays ascending; timers with one delay stay in the order they were added.
|
|
800
|
-
|
|
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 {
|
|
801
1268
|
let slot = delays.length;
|
|
802
1269
|
while (slot > 0 && (delays[slot - 1] as number) > delay) {
|
|
803
1270
|
delays[slot] = delays[slot - 1] as number;
|
|
804
|
-
|
|
1271
|
+
items[slot] = items[slot - 1] as Item;
|
|
805
1272
|
slot--;
|
|
806
1273
|
}
|
|
807
1274
|
delays[slot] = delay;
|
|
808
|
-
|
|
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;
|
|
809
1306
|
}
|
|
810
1307
|
|
|
811
1308
|
function compileAfter<Ctx, E extends EventObject>(compiled: Compiled<Ctx, E>, index: number): void {
|
|
812
1309
|
const delays: number[] = [];
|
|
813
|
-
const
|
|
1310
|
+
const rules: Transition<Ctx, E>[] = [];
|
|
814
1311
|
compiled.afterDelays[index] = delays;
|
|
815
|
-
compiled.
|
|
1312
|
+
compiled.afterRules[index] = rules;
|
|
816
1313
|
const after = (compiled.configs[index] as StateConfig<Ctx, E>).after;
|
|
817
1314
|
if (after === undefined) {
|
|
818
1315
|
return;
|
|
819
1316
|
}
|
|
820
1317
|
const path = compiled.paths[index] as string;
|
|
821
1318
|
// Read as an array type: the transpiler shifts an index to Lua's base 1 only on an array.
|
|
822
|
-
const list = after as readonly AfterConfig<Ctx>[];
|
|
823
|
-
|
|
824
|
-
|
|
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> };
|
|
825
1325
|
for (const key in byDelay) {
|
|
826
1326
|
const delay = parseDelay(key);
|
|
827
1327
|
checkDelay(path, delay, key);
|
|
828
|
-
const
|
|
829
|
-
insertTimer(delays,
|
|
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));
|
|
830
1330
|
}
|
|
831
1331
|
return;
|
|
832
1332
|
}
|
|
833
1333
|
const listDelays: Delay<Ctx>[] = [];
|
|
834
|
-
const
|
|
1334
|
+
const listRules: Transition<Ctx, E>[] = [];
|
|
835
1335
|
let perInstance = false;
|
|
836
1336
|
for (let i = 0; i < list.length; i++) {
|
|
837
|
-
const entry = list[i] as AfterConfig<Ctx>;
|
|
1337
|
+
const entry = list[i] as AfterConfig<Ctx, string, E>;
|
|
838
1338
|
const delay = entry.delay;
|
|
839
1339
|
if (typeof delay === "function") {
|
|
840
1340
|
perInstance = true;
|
|
@@ -842,15 +1342,82 @@ function compileAfter<Ctx, E extends EventObject>(compiled: Compiled<Ctx, E>, in
|
|
|
842
1342
|
checkDelay(path, delay, delay);
|
|
843
1343
|
}
|
|
844
1344
|
listDelays[i] = delay;
|
|
845
|
-
|
|
1345
|
+
listRules[i] = compileTimer(compiled, index, entry, i);
|
|
846
1346
|
}
|
|
847
1347
|
if (perInstance) {
|
|
848
1348
|
compiled.afterListDelays[index] = listDelays;
|
|
849
|
-
compiled.
|
|
1349
|
+
compiled.afterListRules[index] = listRules;
|
|
850
1350
|
return;
|
|
851
1351
|
}
|
|
852
1352
|
for (let i = 0; i < listDelays.length; i++) {
|
|
853
|
-
insertTimer(delays,
|
|
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);
|
|
854
1421
|
}
|
|
855
1422
|
}
|
|
856
1423
|
|
|
@@ -870,17 +1437,24 @@ function compile<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>): Com
|
|
|
870
1437
|
onIndex: [],
|
|
871
1438
|
transitions: [],
|
|
872
1439
|
afterDelays: [],
|
|
873
|
-
|
|
1440
|
+
afterRules: [],
|
|
874
1441
|
afterListDelays: [],
|
|
875
|
-
|
|
1442
|
+
afterListRules: [],
|
|
1443
|
+
everyDelays: [],
|
|
1444
|
+
everyRuns: [],
|
|
876
1445
|
always: [],
|
|
877
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[] },
|
|
878
1451
|
};
|
|
879
1452
|
registerState(compiled, config, NO_STATE, "");
|
|
880
1453
|
for (let index = 0; index < compiled.count; index++) {
|
|
881
1454
|
compileInitial(compiled, index);
|
|
882
1455
|
compileOn(compiled, index);
|
|
883
1456
|
compileAfter(compiled, index);
|
|
1457
|
+
compileEvery(compiled, index);
|
|
884
1458
|
compileAlways(compiled, index);
|
|
885
1459
|
}
|
|
886
1460
|
compileRecords(compiled);
|
|
@@ -919,6 +1493,38 @@ function transitionDomain<Ctx, E extends EventObject>(
|
|
|
919
1493
|
return domain;
|
|
920
1494
|
}
|
|
921
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
|
+
|
|
922
1528
|
interface Definition<Ctx, E extends EventObject> {
|
|
923
1529
|
compiled: Compiled<Ctx, E>;
|
|
924
1530
|
generation: number;
|
|
@@ -948,10 +1554,35 @@ function fillPrivate(
|
|
|
948
1554
|
}
|
|
949
1555
|
}
|
|
950
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
|
+
|
|
951
1582
|
function createMachine<Ctx, E extends EventObject>(
|
|
952
1583
|
definition: Definition<Ctx, E>,
|
|
953
1584
|
): Machine<Ctx, E> {
|
|
954
|
-
function start(ctx: Ctx): MachineInstance<Ctx, E> {
|
|
1585
|
+
function start(ctx: Ctx, at?: readonly string[]): MachineInstance<Ctx, E> {
|
|
955
1586
|
let compiled = definition.compiled;
|
|
956
1587
|
let generation = definition.generation;
|
|
957
1588
|
let paths = compiled.paths;
|
|
@@ -965,23 +1596,31 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
965
1596
|
let onIndex = compiled.onIndex;
|
|
966
1597
|
let transitions = compiled.transitions;
|
|
967
1598
|
let afterDelays = compiled.afterDelays;
|
|
968
|
-
let
|
|
1599
|
+
let afterRules = compiled.afterRules;
|
|
969
1600
|
let afterListDelays = compiled.afterListDelays;
|
|
970
|
-
let
|
|
1601
|
+
let afterListRules = compiled.afterListRules;
|
|
1602
|
+
let everyDelays = compiled.everyDelays;
|
|
1603
|
+
let everyRuns = compiled.everyRuns;
|
|
971
1604
|
let always = compiled.always;
|
|
972
1605
|
const activeChild: number[] = [];
|
|
973
1606
|
const isActive: boolean[] = [];
|
|
974
1607
|
const elapsed: number[] = [];
|
|
975
|
-
|
|
1608
|
+
// For a state with timers, which of them fired during this stay, by position in delay order.
|
|
1609
|
+
const fired: (boolean[] | undefined)[] = [];
|
|
976
1610
|
// The sorted timers of a state whose after list holds a delay function, read on each entry.
|
|
977
1611
|
const ownDelays: (number[] | undefined)[] = [];
|
|
978
|
-
const
|
|
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)[] = [];
|
|
979
1617
|
const entryId: number[] = [];
|
|
980
1618
|
const cleanup: ((() => void) | undefined)[] = [];
|
|
981
1619
|
// Keyed by path, not index, so a remembered child outlives a rebind's recompilation.
|
|
982
1620
|
const remembered: { [parentPath: string]: string } = {};
|
|
983
1621
|
let slotCount = 0;
|
|
984
1622
|
growSlots();
|
|
1623
|
+
sizeFired();
|
|
985
1624
|
const leaves: string[] = [];
|
|
986
1625
|
let entryCount = 0;
|
|
987
1626
|
const queue: (E | undefined)[] = [];
|
|
@@ -992,47 +1631,68 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
992
1631
|
let stopRequested = false;
|
|
993
1632
|
let moved = false;
|
|
994
1633
|
let listeners: MoveListener<E>[] = [];
|
|
1634
|
+
let unhandledListeners: UnhandledListener<E>[] = [];
|
|
995
1635
|
|
|
996
1636
|
const instance = {
|
|
997
1637
|
ctx,
|
|
998
1638
|
path: undefined as string | undefined,
|
|
999
1639
|
leaves,
|
|
1000
1640
|
matches,
|
|
1641
|
+
hasTag,
|
|
1642
|
+
can,
|
|
1001
1643
|
send,
|
|
1002
1644
|
update,
|
|
1003
1645
|
stop,
|
|
1004
1646
|
onMove,
|
|
1647
|
+
onUnhandled,
|
|
1005
1648
|
};
|
|
1006
1649
|
|
|
1007
1650
|
function clearSlot(state: number): void {
|
|
1008
1651
|
activeChild[state] = NO_STATE;
|
|
1009
1652
|
isActive[state] = false;
|
|
1010
1653
|
elapsed[state] = 0;
|
|
1011
|
-
fired[state] =
|
|
1654
|
+
fired[state] = undefined;
|
|
1012
1655
|
ownDelays[state] = undefined;
|
|
1013
|
-
|
|
1656
|
+
ownRules[state] = undefined;
|
|
1657
|
+
ownEvery[state] = undefined;
|
|
1658
|
+
nextRuns[state] = undefined;
|
|
1014
1659
|
entryId[state] = 0;
|
|
1015
1660
|
cleanup[state] = undefined;
|
|
1016
1661
|
}
|
|
1017
1662
|
|
|
1018
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
|
+
}
|
|
1019
1679
|
const list = afterListDelays[state];
|
|
1020
1680
|
if (list === undefined) {
|
|
1021
1681
|
return;
|
|
1022
1682
|
}
|
|
1023
|
-
const
|
|
1683
|
+
const listRules = afterListRules[state] as Transition<Ctx, E>[];
|
|
1024
1684
|
const delays: number[] = [];
|
|
1025
|
-
const
|
|
1685
|
+
const rules: Transition<Ctx, E>[] = [];
|
|
1026
1686
|
for (let i = 0; i < list.length; i++) {
|
|
1027
1687
|
let delay = list[i] as Delay<Ctx>;
|
|
1028
1688
|
if (typeof delay === "function") {
|
|
1029
1689
|
delay = delay(ctx);
|
|
1030
1690
|
checkDelay(paths[state] as string, delay, delay);
|
|
1031
1691
|
}
|
|
1032
|
-
insertTimer(delays,
|
|
1692
|
+
insertTimer(delays, rules, delay, listRules[i] as Transition<Ctx, E>);
|
|
1033
1693
|
}
|
|
1034
1694
|
ownDelays[state] = delays;
|
|
1035
|
-
|
|
1695
|
+
ownRules[state] = rules;
|
|
1036
1696
|
}
|
|
1037
1697
|
|
|
1038
1698
|
function growSlots(): void {
|
|
@@ -1042,6 +1702,33 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1042
1702
|
}
|
|
1043
1703
|
}
|
|
1044
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
|
+
|
|
1045
1732
|
function collectActive(state: number, order: number[]): void {
|
|
1046
1733
|
order.push(state);
|
|
1047
1734
|
const list = children[state] as number[];
|
|
@@ -1061,14 +1748,19 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1061
1748
|
collectActive(ROOT, order);
|
|
1062
1749
|
const keptPaths: string[] = [];
|
|
1063
1750
|
const keptElapsed: number[] = [];
|
|
1064
|
-
const keptFired:
|
|
1751
|
+
const keptFired: (boolean[] | undefined)[] = [];
|
|
1752
|
+
const keptAt: (number[] | undefined)[] = [];
|
|
1065
1753
|
const keptEntryId: number[] = [];
|
|
1066
1754
|
const keptCleanup: ((() => void) | undefined)[] = [];
|
|
1067
1755
|
for (let i = 0; i < order.length; i++) {
|
|
1068
1756
|
const state = order[i] as number;
|
|
1069
1757
|
keptPaths[i] = paths[state] as string;
|
|
1070
1758
|
keptElapsed[i] = elapsed[state] as number;
|
|
1071
|
-
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
|
+
}
|
|
1072
1764
|
keptEntryId[i] = entryId[state] as number;
|
|
1073
1765
|
keptCleanup[i] = cleanup[state];
|
|
1074
1766
|
}
|
|
@@ -1088,12 +1780,15 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1088
1780
|
onIndex = compiled.onIndex;
|
|
1089
1781
|
transitions = compiled.transitions;
|
|
1090
1782
|
afterDelays = compiled.afterDelays;
|
|
1091
|
-
|
|
1783
|
+
afterRules = compiled.afterRules;
|
|
1092
1784
|
afterListDelays = compiled.afterListDelays;
|
|
1093
|
-
|
|
1785
|
+
afterListRules = compiled.afterListRules;
|
|
1786
|
+
everyDelays = compiled.everyDelays;
|
|
1787
|
+
everyRuns = compiled.everyRuns;
|
|
1094
1788
|
always = compiled.always;
|
|
1095
1789
|
fillPrivate(ctx as never, configs[ROOT], true);
|
|
1096
1790
|
growSlots();
|
|
1791
|
+
sizeFired();
|
|
1097
1792
|
const dropped: number[] = [];
|
|
1098
1793
|
for (let i = 0; i < order.length; i++) {
|
|
1099
1794
|
const state = lookupPath(compiled, keptPaths[i] as string);
|
|
@@ -1112,10 +1807,30 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1112
1807
|
activeChild[owner] = state;
|
|
1113
1808
|
}
|
|
1114
1809
|
elapsed[state] = keptElapsed[i] as number;
|
|
1115
|
-
fired[state] = keptFired[i] as number;
|
|
1116
1810
|
entryId[state] = keptEntryId[i] as number;
|
|
1117
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.
|
|
1118
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
|
+
}
|
|
1119
1834
|
}
|
|
1120
1835
|
for (let i = dropped.length - 1; i >= 0; i--) {
|
|
1121
1836
|
const run = keptCleanup[dropped[i] as number];
|
|
@@ -1135,32 +1850,27 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1135
1850
|
settleAlways();
|
|
1136
1851
|
}
|
|
1137
1852
|
|
|
1138
|
-
// Copy-on-write: a report walks the list it started with, so a listener removed mid-report
|
|
1139
|
-
// never makes it skip another.
|
|
1140
1853
|
function onMove(listener: MoveListener<E>): () => void {
|
|
1141
|
-
|
|
1142
|
-
for (let i = 0; i < listeners.length; i++) {
|
|
1143
|
-
added.push(listeners[i] as MoveListener<E>);
|
|
1144
|
-
}
|
|
1145
|
-
added.push(listener);
|
|
1146
|
-
listeners = added;
|
|
1854
|
+
listeners = withListener(listeners, listener);
|
|
1147
1855
|
let removed = false;
|
|
1148
1856
|
return () => {
|
|
1149
1857
|
if (removed) {
|
|
1150
1858
|
return;
|
|
1151
1859
|
}
|
|
1152
1860
|
removed = true;
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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;
|
|
1162
1871
|
}
|
|
1163
|
-
|
|
1872
|
+
removed = true;
|
|
1873
|
+
unhandledListeners = withoutListener(unhandledListeners, listener);
|
|
1164
1874
|
};
|
|
1165
1875
|
}
|
|
1166
1876
|
|
|
@@ -1169,10 +1879,12 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1169
1879
|
to: string | undefined,
|
|
1170
1880
|
cause: MoveCause,
|
|
1171
1881
|
event: E | undefined,
|
|
1882
|
+
source?: string,
|
|
1883
|
+
key?: number,
|
|
1172
1884
|
): void {
|
|
1173
1885
|
const current = listeners;
|
|
1174
1886
|
for (let i = 0; i < current.length; i++) {
|
|
1175
|
-
(current[i] as
|
|
1887
|
+
(current[i] as MoveReport<E>)(from, to, cause, event, source, key);
|
|
1176
1888
|
}
|
|
1177
1889
|
}
|
|
1178
1890
|
|
|
@@ -1209,7 +1921,12 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1209
1921
|
isActive[state] = true;
|
|
1210
1922
|
activeChild[state] = NO_STATE;
|
|
1211
1923
|
elapsed[state] = 0;
|
|
1212
|
-
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
|
+
}
|
|
1213
1930
|
entryCount++;
|
|
1214
1931
|
const id = entryCount;
|
|
1215
1932
|
entryId[state] = id;
|
|
@@ -1342,6 +2059,26 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1342
2059
|
}
|
|
1343
2060
|
}
|
|
1344
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
|
+
|
|
1345
2082
|
function enterMissing(state: number): void {
|
|
1346
2083
|
if (parallel[state] === true) {
|
|
1347
2084
|
const list = children[state] as number[];
|
|
@@ -1384,6 +2121,7 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1384
2121
|
run: readonly MoveAction<Ctx, E>[] | undefined,
|
|
1385
2122
|
event: E | undefined,
|
|
1386
2123
|
cause: MoveCause,
|
|
2124
|
+
key?: number,
|
|
1387
2125
|
): void {
|
|
1388
2126
|
if (target === NO_STATE) {
|
|
1389
2127
|
runActions(run, event);
|
|
@@ -1404,7 +2142,7 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1404
2142
|
refreshLeaves();
|
|
1405
2143
|
moved = true;
|
|
1406
2144
|
if (listeners.length > 0) {
|
|
1407
|
-
report(from, firstLeaf(domain), cause, event);
|
|
2145
|
+
report(from, firstLeaf(domain), cause, event, paths[source] as string, key);
|
|
1408
2146
|
}
|
|
1409
2147
|
}
|
|
1410
2148
|
|
|
@@ -1432,14 +2170,14 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1432
2170
|
const list = always[state] as Transition<Ctx, E>[];
|
|
1433
2171
|
for (let i = 0; i < list.length; i++) {
|
|
1434
2172
|
const candidate = list[i] as Transition<Ctx, E>;
|
|
1435
|
-
const when = candidate.when as ((ctx: Ctx) => boolean) | undefined;
|
|
1436
|
-
if (when === undefined || when(ctx)) {
|
|
2173
|
+
const when = candidate.when as ((ctx: Ctx, machine: MachineView) => boolean) | undefined;
|
|
2174
|
+
if (when === undefined || when(ctx, instance)) {
|
|
1437
2175
|
if (overLimit) {
|
|
1438
2176
|
clearQueue();
|
|
1439
2177
|
busy = false;
|
|
1440
2178
|
throw `hsm: state "${describePath(paths[state] as string)}" took ${ALWAYS_LIMIT} always transitions in a row; check for an always loop`;
|
|
1441
2179
|
}
|
|
1442
|
-
transition(state, candidate.target,
|
|
2180
|
+
transition(state, candidate.target, candidate.run, undefined, "always", i);
|
|
1443
2181
|
return true;
|
|
1444
2182
|
}
|
|
1445
2183
|
}
|
|
@@ -1450,7 +2188,14 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1450
2188
|
run: readonly MoveAction<Ctx, E>[] | undefined,
|
|
1451
2189
|
event: E | undefined,
|
|
1452
2190
|
): void {
|
|
1453
|
-
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
|
+
}
|
|
1454
2199
|
return;
|
|
1455
2200
|
}
|
|
1456
2201
|
for (let i = 0; i < run.length; i++) {
|
|
@@ -1466,8 +2211,8 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1466
2211
|
const list = transitions[listIndex] as Transition<Ctx, E>[];
|
|
1467
2212
|
for (let i = 0; i < list.length; i++) {
|
|
1468
2213
|
const candidate = list[i] as Transition<Ctx, E>;
|
|
1469
|
-
if (candidate.when === undefined || candidate.when(ctx, event)) {
|
|
1470
|
-
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);
|
|
1471
2216
|
return true;
|
|
1472
2217
|
}
|
|
1473
2218
|
}
|
|
@@ -1500,6 +2245,42 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1500
2245
|
return takeEvent(state, event);
|
|
1501
2246
|
}
|
|
1502
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
|
+
|
|
1503
2284
|
function clearQueue(): void {
|
|
1504
2285
|
while (queueHead < queueTail) {
|
|
1505
2286
|
queue[queueHead] = undefined;
|
|
@@ -1522,13 +2303,38 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1522
2303
|
}
|
|
1523
2304
|
}
|
|
1524
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
|
+
|
|
1525
2325
|
function endStep(): void {
|
|
1526
2326
|
while (!stopRequested && queueHead < queueTail) {
|
|
1527
2327
|
const event = queue[queueHead] as E;
|
|
1528
2328
|
queue[queueHead] = undefined;
|
|
1529
2329
|
queueHead++;
|
|
1530
2330
|
moved = false;
|
|
1531
|
-
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
|
+
}
|
|
1532
2338
|
if (moved) {
|
|
1533
2339
|
settleAlways();
|
|
1534
2340
|
}
|
|
@@ -1616,13 +2422,50 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1616
2422
|
if (child !== NO_STATE && fireTimers(child)) {
|
|
1617
2423
|
return true;
|
|
1618
2424
|
}
|
|
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
|
+
}
|
|
1619
2449
|
const delays = ownDelays[state] || (afterDelays[state] as number[]);
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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
|
+
}
|
|
1626
2469
|
}
|
|
1627
2470
|
return false;
|
|
1628
2471
|
}
|
|
@@ -1662,10 +2505,32 @@ function createMachine<Ctx, E extends EventObject>(
|
|
|
1662
2505
|
return state !== undefined && state !== ROOT && isActive[state] === true;
|
|
1663
2506
|
}
|
|
1664
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
|
+
}
|
|
1665
2526
|
busy = true;
|
|
1666
2527
|
fillPrivate(ctx as never, configs[ROOT], false);
|
|
1667
2528
|
enterState(ROOT);
|
|
1668
|
-
|
|
2529
|
+
if (chosen === undefined) {
|
|
2530
|
+
enterDefaults(ROOT, 0);
|
|
2531
|
+
} else {
|
|
2532
|
+
enterAt(ROOT, chosen);
|
|
2533
|
+
}
|
|
1669
2534
|
refreshLeaves();
|
|
1670
2535
|
settleAlways();
|
|
1671
2536
|
endStep();
|
|
@@ -1710,19 +2575,83 @@ type WithPrivate<Ctx, Priv> = [Priv] extends [never] ? Ctx : Ctx & Priv;
|
|
|
1710
2575
|
* definition under it puts its states into the machine already built, so every running instance
|
|
1711
2576
|
* sees the edit; without one, each call builds a separate machine.
|
|
1712
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
|
+
*
|
|
1713
2582
|
* The root config may hold `privateCtx`, for a machine that keeps fields of its own, such as a
|
|
1714
2583
|
* counter, a heat level or a list it builds up. Every hook, `when`, `run`, `task` and `update`
|
|
1715
2584
|
* then sees those fields beside `Ctx`, while `start` takes only `Ctx` and the instance it returns
|
|
1716
2585
|
* shows only `Ctx` in its `ctx`. A machine without fields of its own leaves `privateCtx` out.
|
|
1717
2586
|
*/
|
|
1718
|
-
export function defineMachine<Ctx, E extends EventObject>(
|
|
2587
|
+
export function defineMachine<Ctx, E extends EventObject, Tag extends string = never>(
|
|
1719
2588
|
key?: string,
|
|
1720
|
-
): <T, Priv extends object = never>(
|
|
1721
|
-
config: RootState<WithPrivate<Ctx, Priv>, E, T> & PrivateCtxConfig<Ctx, Priv>,
|
|
1722
|
-
) => Machine<Ctx, E, TreePath<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> {
|
|
1723
2592
|
return ((config: MachineConfig<Ctx, E>) => define(key, config)) as never;
|
|
1724
2593
|
}
|
|
1725
2594
|
|
|
2595
|
+
/**
|
|
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
|
+
|
|
1726
2655
|
/**
|
|
1727
2656
|
* The optional key a `defineMachine` root config takes beside `initial` and `states`.
|
|
1728
2657
|
* @noSelf
|