@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/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
- * One running machine, returned by `start`. `P` is the union of the machine's state paths; the
32
- * `machine` a hook, a `run` or a `task` receives uses plain `string` paths instead.
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 interface MachineInstance<Ctx, E extends EventObject, P extends string = string> {
36
- /** The context passed to `start`. */
37
- readonly ctx: Ctx;
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) => boolean;
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, and once the machine is stopped the call does nothing.
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 the `after` timers if no
57
- * hook moved it. A machine whose `update` is not called is frozen in time, and once the machine
58
- * is stopped the call does nothing.
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<Ctx, E extends EventObject, V extends E = E> = (
80
- ctx: Ctx,
81
- event: V,
82
- machine: MachineInstance<Ctx, E>,
83
- ) => void;
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?: (ctx: Ctx, event: V) => boolean;
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
- > = To | TransitionConfig<Ctx, E, V, To> | RuleList<TransitionConfig<Ctx, E, V, To>>;
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 `when`. It gets no event and has
145
- * no `run`; side effects belong in the target's `enter`.
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<Ctx, To extends string = string> {
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
- /** Checked on every recheck, so it must have no side effects. */
155
- readonly when?: (ctx: Ctx) => boolean;
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<Ctx, To extends string = string> =
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
- | RuleList<AlwaysConfig<Ctx, To>>;
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
- * One entry of the list form of `after`: a `delay` and the `to` its timer leads to.
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 AfterConfig<Ctx, To extends string = string> {
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. A result that is not a non-negative number
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
- /** The full path of the state to move to when the delay has passed. */
184
- readonly to: To;
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<Ctx, E extends EventObject, To extends string = string> = {
192
- readonly [K in E["type"]]?: TransitionSpec<Ctx, E, Extract<E, { type: K }>, To>;
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<Ctx, E extends EventObject> = (
201
- ctx: Ctx,
202
- machine: MachineInstance<Ctx, E>,
203
- ) => void;
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 `run`.
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<Ctx, E extends EventObject, To extends string = string> = (
211
- ctx: Ctx,
212
- dt: number,
213
- machine: MachineInstance<Ctx, E>,
214
- ) => To | undefined;
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<Ctx, E extends EventObject> = (
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<Ctx, E extends EventObject, P extends string = string> {
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 and enters the root's `initial` chain. Each
236
- * call returns a separate instance, which has already handled the events its `enter` hooks
237
- * sent.
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<Ctx, E extends EventObject, T, To extends string> = unknown extends T
301
- ? { readonly [name: string]: StateNode<Ctx, E, unknown, string, To> }
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
- // No callback parameter mentions the tree: one that did would make TypeScript settle the tree
310
- // at the first such callback, before the states beside it are read.
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<Ctx, E extends EventObject, T, Self extends string, To extends string> {
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 time, such as
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. One `update` call fires at most one timer, and none when
329
- * an `update` hook moved the machine.
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<{ readonly [seconds: number]: To } | readonly AfterConfig<Ctx, To>[]>;
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
- * Targets taken right after any move that leaves this state active; the first whose `when` is
334
- * missing or passes wins. It is checked after a move only: not after a rule with no `to`, and
335
- * not on an `update` call that moves nothing.
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 always?: AlwaysSpec<Ctx, To>;
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<Ctx, E extends EventObject, T, Self extends string, To extends string>
361
- extends StateBase<Ctx, E, T, Self, To> {
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<Ctx, E extends EventObject, T, Self extends string, To extends string>
383
- extends StateBase<Ctx, E, T, Self, To> {
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<Ctx, E extends EventObject, T, Self extends string, To extends string> =
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
- NoInfer<SlotPath<T>>
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<Ctx, E, unknown, string, string>;
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 afterTargets: number[][];
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 afterListTargets: (number[] | undefined)[];
870
+ readonly afterListRules: (Transition<Ctx, E>[] | undefined)[];
871
+ // A state's every entries: the record form in delay order, a list in list order.
872
+ readonly everyDelays: Delay<Ctx>[][];
873
+ readonly everyRuns: RunAction<Ctx, E>[][][];
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 in Lua.
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
- function insertTimer(delays: number[], targets: number[], delay: number, target: number): void {
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
- targets[slot] = targets[slot - 1] as number;
1271
+ items[slot] = items[slot - 1] as Item;
805
1272
  slot--;
806
1273
  }
807
1274
  delays[slot] = delay;
808
- targets[slot] = target;
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 targets: number[] = [];
1310
+ const rules: Transition<Ctx, E>[] = [];
814
1311
  compiled.afterDelays[index] = delays;
815
- compiled.afterTargets[index] = targets;
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
- if (typeof list[0] !== "object") {
824
- const byDelay = after as { readonly [seconds: number]: string };
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 target = resolveTarget(compiled, index, byDelay[key as unknown as number] as string);
829
- insertTimer(delays, targets, delay, target);
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 listTargets: number[] = [];
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
- listTargets[i] = resolveTarget(compiled, index, entry.to);
1345
+ listRules[i] = compileTimer(compiled, index, entry, i);
846
1346
  }
847
1347
  if (perInstance) {
848
1348
  compiled.afterListDelays[index] = listDelays;
849
- compiled.afterListTargets[index] = listTargets;
1349
+ compiled.afterListRules[index] = listRules;
850
1350
  return;
851
1351
  }
852
1352
  for (let i = 0; i < listDelays.length; i++) {
853
- insertTimer(delays, targets, listDelays[i] as number, listTargets[i] as number);
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
- afterTargets: [],
1440
+ afterRules: [],
874
1441
  afterListDelays: [],
875
- afterListTargets: [],
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 afterTargets = compiled.afterTargets;
1599
+ let afterRules = compiled.afterRules;
969
1600
  let afterListDelays = compiled.afterListDelays;
970
- let afterListTargets = compiled.afterListTargets;
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
- const fired: number[] = [];
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 ownTargets: (number[] | undefined)[] = [];
1612
+ const ownRules: (Transition<Ctx, E>[] | undefined)[] = [];
1613
+ // For a state with every entries, by entry: the delay it counts in this stay, and the value
1614
+ // of the state's clock at its next run.
1615
+ const ownEvery: (number[] | undefined)[] = [];
1616
+ const nextRuns: (number[] | undefined)[] = [];
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] = 0;
1654
+ fired[state] = undefined;
1012
1655
  ownDelays[state] = undefined;
1013
- ownTargets[state] = undefined;
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 listTargets = afterListTargets[state] as number[];
1683
+ const listRules = afterListRules[state] as Transition<Ctx, E>[];
1024
1684
  const delays: number[] = [];
1025
- const targets: number[] = [];
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, targets, delay, listTargets[i] as number);
1692
+ insertTimer(delays, rules, delay, listRules[i] as Transition<Ctx, E>);
1033
1693
  }
1034
1694
  ownDelays[state] = delays;
1035
- ownTargets[state] = targets;
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: number[] = [];
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] as number;
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
- afterTargets = compiled.afterTargets;
1783
+ afterRules = compiled.afterRules;
1092
1784
  afterListDelays = compiled.afterListDelays;
1093
- afterListTargets = compiled.afterListTargets;
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
- const added: MoveListener<E>[] = [];
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
- const kept: MoveListener<E>[] = [];
1154
- let skipped = false;
1155
- for (let i = 0; i < listeners.length; i++) {
1156
- const current = listeners[i] as MoveListener<E>;
1157
- if (current !== listener || skipped) {
1158
- kept.push(current);
1159
- } else {
1160
- skipped = true;
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
- listeners = kept;
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 MoveListener<E>)(from, to, cause, event);
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] = 0;
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, undefined, undefined, "always");
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 || event === 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 next = fired[state] as number;
1621
- if (next < delays.length && (delays[next] as number) <= (elapsed[state] as number)) {
1622
- fired[state] = next + 1;
1623
- const targets = ownTargets[state] || (afterTargets[state] as number[]);
1624
- transition(state, targets[next] as number, undefined, undefined, "after");
1625
- return true;
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
- enterDefaults(ROOT, 0);
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