@defold-typescript/transpiler 0.47.0 → 0.49.0

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