@defold-typescript/transpiler 0.43.0 → 0.44.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
@@ -2,65 +2,139 @@ export interface EventObject {
2
2
  readonly type: string;
3
3
  }
4
4
 
5
+ export type MoveCause = "event" | "after" | "update" | "stop" | "reload" | "always";
6
+
7
+ /** @noSelf */
8
+ export type MoveListener<E extends EventObject, P extends string = string> = (
9
+ from: P,
10
+ to: P | undefined,
11
+ cause: MoveCause,
12
+ event: E | undefined,
13
+ ) => void;
14
+
5
15
  /** @noSelf */
6
16
  export interface MachineInstance<Ctx, E extends EventObject, P extends string = string> {
7
17
  readonly ctx: Ctx;
8
18
  readonly path: P | undefined;
19
+ readonly leaves: readonly P[];
9
20
  readonly matches: (path: P) => boolean;
10
21
  readonly send: (event: E) => void;
11
22
  readonly update: (dt: number) => void;
12
23
  readonly stop: () => void;
24
+ /**
25
+ * Adds a listener called after each move completes, with the leaf left, the leaf entered and
26
+ * the cause. It is not called for the first entry at `start` or for a move with no `to`; a
27
+ * parallel machine calls it once per moved region. Each call adds a listener, and the returned
28
+ * function removes that one.
29
+ */
30
+ readonly onMove: (listener: MoveListener<E, P>) => () => void;
13
31
  }
14
32
 
15
- export type TransitionAction<Ctx, E extends EventObject, V extends E = E> = (
33
+ export type MoveAction<Ctx, E extends EventObject, V extends E = E> = (
16
34
  ctx: Ctx,
17
35
  event: V,
18
- m: MachineInstance<Ctx, E>,
36
+ machine: MachineInstance<Ctx, E>,
19
37
  ) => void;
20
38
 
21
39
  /** @noSelf */
22
40
  export interface TransitionConfig<Ctx, E extends EventObject, V extends E = E> {
23
- readonly target?: string;
24
- readonly guard?: (ctx: Ctx, event: V) => boolean;
25
- readonly actions?: TransitionAction<Ctx, E, V> | readonly TransitionAction<Ctx, E, V>[];
26
- readonly reenter?: boolean;
41
+ /**
42
+ * The full path of the state to move to. A `to` naming the state the rule is on exits and
43
+ * re-enters it, restarting its timers and `task`; a `to` below it keeps it active.
44
+ */
45
+ readonly to?: string;
46
+ /**
47
+ * The rule is taken only when this returns `true`. It must have no side effects: it is also
48
+ * checked when the move does not happen, in guarded lists and on every `always` recheck.
49
+ */
50
+ readonly when?: (ctx: Ctx, event: V) => boolean;
51
+ /**
52
+ * Runs during the move. The event is accepted when its key matches and `when` passes; then
53
+ * `exit` runs on each state left, deepest first; then `run`; then `enter` on each state
54
+ * entered, outermost first. Shared ancestors neither exit nor enter. A rule with no `to` runs
55
+ * only `run`.
56
+ */
57
+ readonly run?: MoveAction<Ctx, E, V> | readonly MoveAction<Ctx, E, V>[];
58
+ /** Renamed to `to`. */
59
+ readonly target?: never;
60
+ /** Renamed to `when`. */
61
+ readonly guard?: never;
62
+ /** Renamed to `run`. */
63
+ readonly actions?: never;
64
+ /** Removed: a `to` naming the state the rule is on restarts it. */
65
+ readonly reenter?: never;
27
66
  }
28
67
 
29
68
  export type TransitionSpec<Ctx, E extends EventObject, V extends E = E> =
30
69
  | string
31
70
  | TransitionConfig<Ctx, E, V>
32
- // Lua cannot tell [] from {}, so an empty list would compile to a targetless transition.
71
+ // Lua cannot tell [] from {}, so an empty list would compile to a transition with no to.
33
72
  | readonly [TransitionConfig<Ctx, E, V>, ...TransitionConfig<Ctx, E, V>[]];
34
73
 
74
+ /** @noSelf */
75
+ export interface AlwaysConfig<Ctx> {
76
+ readonly to: string;
77
+ /** Checked on every recheck, so it must have no side effects. */
78
+ readonly when?: (ctx: Ctx) => boolean;
79
+ /** Renamed to `to`. */
80
+ readonly target?: never;
81
+ /** Renamed to `when`. */
82
+ readonly guard?: never;
83
+ }
84
+
85
+ export type AlwaysSpec<Ctx> =
86
+ | string
87
+ | AlwaysConfig<Ctx>
88
+ | readonly [AlwaysConfig<Ctx>, ...AlwaysConfig<Ctx>[]];
89
+
35
90
  export type OnConfig<Ctx, E extends EventObject> = {
36
91
  readonly [K in E["type"]]?: TransitionSpec<Ctx, E, Extract<E, { type: K }>>;
37
92
  };
38
93
 
39
- export type StateHook<Ctx, E extends EventObject> = (ctx: Ctx, m: MachineInstance<Ctx, E>) => void;
94
+ /**
95
+ * Runs on every way in (`enter`) or out (`exit`) of the state: a move, `start` and `stop`. A hot
96
+ * reload enters the states it adds, but drops a removed state without its `exit`. It gets no
97
+ * event; event data reaches `enter` through `ctx`, set in `run`.
98
+ */
99
+ export type StateHook<Ctx, E extends EventObject> = (
100
+ ctx: Ctx,
101
+ machine: MachineInstance<Ctx, E>,
102
+ ) => void;
40
103
 
41
104
  export type UpdateHook<Ctx, E extends EventObject> = (
42
105
  ctx: Ctx,
43
106
  dt: number,
44
- m: MachineInstance<Ctx, E>,
107
+ machine: MachineInstance<Ctx, E>,
45
108
  ) => string | undefined;
46
109
 
47
- /** @noSelf */
48
- export type InvokeStart<Ctx, E extends EventObject> = (
110
+ /**
111
+ * Starts work that lives as long as the state. `finish` sends one event and is ignored after the
112
+ * first call or once the state is left. The returned cleanup runs on every way out.
113
+ * @noSelf
114
+ */
115
+ export type TaskStart<Ctx, E extends EventObject> = (
49
116
  ctx: Ctx,
50
- settle: (event: E) => void,
51
- m: MachineInstance<Ctx, E>,
52
- ) => void;
117
+ finish: (event: E) => void,
118
+ machine: MachineInstance<Ctx, E>,
119
+ // biome-ignore lint/suspicious/noConfusingVoidType: `void` keeps an expression-body task returning a void call (`=> go.animate(...)`) valid.
120
+ ) => (() => void) | void;
53
121
 
54
122
  /** @noSelf */
55
123
  export interface StateConfig<Ctx, E extends EventObject> {
124
+ readonly type?: "parallel";
56
125
  readonly initial?: string;
126
+ readonly restoreDepth?: number | "all";
57
127
  readonly states?: { readonly [name: string]: StateConfig<Ctx, E> };
58
128
  readonly on?: OnConfig<Ctx, E>;
59
129
  readonly after?: { readonly [seconds: number]: string };
130
+ readonly always?: AlwaysSpec<Ctx>;
60
131
  readonly enter?: StateHook<Ctx, E>;
61
132
  readonly exit?: StateHook<Ctx, E>;
62
133
  readonly update?: UpdateHook<Ctx, E>;
63
- readonly invoke?: InvokeStart<Ctx, E>;
134
+ /** Started on each entry; see `TaskStart`. */
135
+ readonly task?: TaskStart<Ctx, E>;
136
+ /** Renamed to `task`. */
137
+ readonly invoke?: never;
64
138
  }
65
139
 
66
140
  export interface MachineConfig<Ctx, E extends EventObject> extends StateConfig<Ctx, E> {
@@ -89,11 +163,34 @@ type PathsBelow<S, D extends number> = S extends { readonly states: infer Childr
89
163
 
90
164
  export type StatePath<C> = `/${PathsBelow<C, 4>}`;
91
165
 
166
+ type NextCount = [never, 2, 3, 4, 5, 6];
167
+
168
+ type RestoreCountBelow<Children, D extends number> = {
169
+ [K in keyof Children]: RestoreCount<Children[K], D>;
170
+ }[keyof Children];
171
+
172
+ // The counts a state's subtree can hold: a compound state is one level plus its deepest child,
173
+ // a parallel state adds no level of its own, and a leaf holds none.
174
+ type RestoreCount<S, D extends number> = S extends { readonly states: infer Children }
175
+ ? [D] extends [never]
176
+ ? number
177
+ : S extends { readonly type: "parallel" }
178
+ ? RestoreCountBelow<Children, PathDepth[D]>
179
+ :
180
+ | 1
181
+ | (RestoreCountBelow<Children, PathDepth[D]> extends infer N
182
+ ? N extends number
183
+ ? number extends N
184
+ ? number
185
+ : NextCount[N]
186
+ : never
187
+ : never)
188
+ : never;
189
+
92
190
  interface TransitionCheck<T> {
93
- readonly target?: T;
94
- readonly guard?: unknown;
95
- readonly actions?: unknown;
96
- readonly reenter?: unknown;
191
+ readonly to?: T;
192
+ readonly when?: unknown;
193
+ readonly run?: unknown;
97
194
  }
98
195
 
99
196
  type SpecCheck<T> = T | TransitionCheck<T> | readonly TransitionCheck<T>[];
@@ -101,9 +198,17 @@ type SpecCheck<T> = T | TransitionCheck<T> | readonly TransitionCheck<T>[];
101
198
  // Hooks are listed as unknown so a hooks-only leaf still shares a property with this
102
199
  // all-optional type; without them TypeScript rejects it as a weak-type mismatch.
103
200
  type PathCheck<S, Self extends string, All extends string, Ev extends string> = {
104
- readonly initial?: S extends { readonly states: infer Children }
105
- ? `${Self}/${keyof Children & string}`
106
- : never;
201
+ readonly type?: unknown;
202
+ readonly initial?: S extends { readonly type: "parallel" }
203
+ ? never
204
+ : S extends { readonly states: infer Children }
205
+ ? `${Self}/${keyof Children & string}`
206
+ : never;
207
+ readonly restoreDepth?: S extends { readonly type: "parallel" }
208
+ ? never
209
+ : S extends { readonly states: object }
210
+ ? RestoreCount<S, 4> | "all"
211
+ : never;
107
212
  readonly states?: S extends { readonly states: infer Children }
108
213
  ? {
109
214
  readonly [K in keyof Children]: PathCheck<Children[K], `${Self}/${K & string}`, All, Ev>;
@@ -115,14 +220,15 @@ type PathCheck<S, Self extends string, All extends string, Ev extends string> =
115
220
  readonly after?: S extends { readonly after: infer After }
116
221
  ? { readonly [K in keyof After]: All }
117
222
  : unknown;
223
+ readonly always?: S extends { readonly always: unknown } ? SpecCheck<All> : unknown;
118
224
  readonly enter?: unknown;
119
225
  readonly exit?: unknown;
120
226
  readonly update?: unknown;
121
- readonly invoke?: unknown;
227
+ readonly task?: unknown;
122
228
  };
123
229
 
124
230
  export interface MachineConfigError {
125
- readonly "hsm: an initial or target names an unknown state path, or an on key an unknown event": never;
231
+ readonly "hsm: an initial or a to names an unknown state path, or an on key an unknown event": never;
126
232
  }
127
233
 
128
234
  export type DefinedMachine<Ctx, E extends EventObject, C> =
@@ -134,28 +240,33 @@ type Guard<Ctx, E extends EventObject> = (ctx: Ctx, event: E) => boolean;
134
240
 
135
241
  interface Transition<Ctx, E extends EventObject> {
136
242
  readonly target: number;
137
- readonly guard: Guard<Ctx, E> | undefined;
138
- readonly actions: readonly TransitionAction<Ctx, E>[];
139
- readonly reenter: boolean;
243
+ readonly when: Guard<Ctx, E> | undefined;
244
+ readonly run: readonly MoveAction<Ctx, E>[];
140
245
  }
141
246
 
142
247
  interface Compiled<Ctx, E extends EventObject> {
143
248
  count: number;
144
- maxDepth: number;
145
249
  readonly configs: StateConfig<Ctx, E>[];
146
250
  readonly paths: string[];
147
251
  readonly parent: number[];
148
252
  readonly depth: number[];
253
+ readonly children: number[][];
254
+ readonly parallel: boolean[];
149
255
  readonly initialChild: number[];
256
+ readonly restoreDepth: number[];
257
+ readonly levels: number[];
258
+ readonly records: boolean[];
150
259
  readonly onIndex: { [type: string]: number }[];
151
260
  readonly transitions: Transition<Ctx, E>[][];
152
261
  readonly afterDelays: number[][];
153
262
  readonly afterTargets: number[][];
263
+ readonly always: Transition<Ctx, E>[][];
154
264
  readonly pathIndex: { [path: string]: number };
155
265
  }
156
266
 
157
267
  const ROOT = 0;
158
268
  const NO_STATE = -1;
269
+ const ALWAYS_LIMIT = 10;
159
270
 
160
271
  function describePath(path: string): string {
161
272
  return path === "" ? "(root)" : path;
@@ -185,6 +296,46 @@ function resolveTarget<Ctx, E extends EventObject>(
185
296
  return index;
186
297
  }
187
298
 
299
+ // A structural cast keeps each read a plain method call: native in JavaScript, string.byte in Lua.
300
+ interface StringUnits {
301
+ charCodeAt(index: number): number;
302
+ byte(index: number): number;
303
+ }
304
+
305
+ // JavaScript strings hold UTF-16 code units; Lua strings hold UTF-8 bytes.
306
+ const UTF16_UNITS = "\u{E000}".length === 1;
307
+
308
+ function unitAt(text: string, index: number): number {
309
+ const units = text as unknown as StringUnits;
310
+ if (UTF16_UNITS) {
311
+ return units.charCodeAt(index);
312
+ }
313
+ return units.byte(index + 1);
314
+ }
315
+
316
+ // Moves surrogates above the rest of the BMP, so UTF-16 units compare in code-point order.
317
+ // UTF-8 bytes already do, and every byte is below the surrogate range.
318
+ function codePointRank(unit: number): number {
319
+ if (unit >= 0xd800 && unit < 0xe000) {
320
+ return unit + 0x2000;
321
+ }
322
+ if (unit >= 0xe000) {
323
+ return unit - 0x800;
324
+ }
325
+ return unit;
326
+ }
327
+
328
+ function precedes(left: string, right: string): boolean {
329
+ for (let i = 0; i < left.length && i < right.length; i++) {
330
+ const leftUnit = unitAt(left, i);
331
+ const rightUnit = unitAt(right, i);
332
+ if (leftUnit !== rightUnit) {
333
+ return codePointRank(leftUnit) < codePointRank(rightUnit);
334
+ }
335
+ }
336
+ return left.length < right.length;
337
+ }
338
+
188
339
  function registerState<Ctx, E extends EventObject>(
189
340
  compiled: Compiled<Ctx, E>,
190
341
  config: StateConfig<Ctx, E>,
@@ -193,26 +344,51 @@ function registerState<Ctx, E extends EventObject>(
193
344
  ): void {
194
345
  const index = compiled.count;
195
346
  compiled.count = index + 1;
196
- const depth = parentIndex === NO_STATE ? 0 : (compiled.depth[parentIndex] as number) + 1;
197
- if (depth > compiled.maxDepth) {
198
- compiled.maxDepth = depth;
199
- }
200
347
  compiled.configs[index] = config;
201
348
  compiled.paths[index] = path;
202
349
  compiled.parent[index] = parentIndex;
203
- compiled.depth[index] = depth;
350
+ compiled.depth[index] =
351
+ parentIndex === NO_STATE ? 0 : (compiled.depth[parentIndex] as number) + 1;
352
+ compiled.children[index] = [];
353
+ compiled.parallel[index] = config.type === "parallel";
204
354
  compiled.initialChild[index] = NO_STATE;
355
+ compiled.restoreDepth[index] = 0;
356
+ compiled.levels[index] = 0;
357
+ compiled.records[index] = false;
205
358
  compiled.pathIndex[path] = index;
206
359
  const children = config.states;
207
360
  if (children === undefined) {
208
361
  return;
209
362
  }
363
+ let deepest = 0;
210
364
  for (const name in children) {
211
365
  if (name === "" || name.indexOf("/") !== -1) {
212
366
  throw `hsm: state "${describePath(path)}" has a child named "${name}"; state names must be non-empty and contain no "/"`;
213
367
  }
214
- registerState(compiled, children[name] as StateConfig<Ctx, E>, index, `${path}/${name}`);
368
+ const child = compiled.count;
369
+ const childPath = `${path}/${name}`;
370
+ registerState(compiled, children[name] as StateConfig<Ctx, E>, index, childPath);
371
+ // Region order is child-name order by code point, the same in both runtimes: neither Lua's
372
+ // pairs nor JavaScript keeps the written order.
373
+ const siblings = compiled.children[index] as number[];
374
+ let slot = siblings.length;
375
+ while (
376
+ slot > 0 &&
377
+ precedes(childPath, compiled.paths[siblings[slot - 1] as number] as string)
378
+ ) {
379
+ siblings[slot] = siblings[slot - 1] as number;
380
+ slot--;
381
+ }
382
+ siblings[slot] = child;
383
+ const childLevels = compiled.levels[child] as number;
384
+ if (childLevels > deepest) {
385
+ deepest = childLevels;
386
+ }
215
387
  }
388
+ if (compiled.parallel[index] !== true) {
389
+ deepest++;
390
+ }
391
+ compiled.levels[index] = deepest;
216
392
  }
217
393
 
218
394
  function hasChildren<Ctx, E extends EventObject>(config: StateConfig<Ctx, E>): boolean {
@@ -233,12 +409,31 @@ function compileInitial<Ctx, E extends EventObject>(
233
409
  const config = compiled.configs[index] as StateConfig<Ctx, E>;
234
410
  const path = compiled.paths[index] as string;
235
411
  const initial = config.initial;
412
+ if (compiled.parallel[index] === true) {
413
+ if (index === ROOT) {
414
+ throw `hsm: state "${describePath(path)}" is parallel; put the regions in a child state`;
415
+ }
416
+ if (initial !== undefined) {
417
+ throw `hsm: parallel state "${path}" has initial "${initial}"; every child is entered`;
418
+ }
419
+ if (config.restoreDepth !== undefined) {
420
+ throw `hsm: parallel state "${path}" has restoreDepth; only a compound state resumes a child`;
421
+ }
422
+ if (!hasChildren(config)) {
423
+ throw `hsm: parallel state "${path}" has no child states`;
424
+ }
425
+ return;
426
+ }
236
427
  if (!hasChildren(config)) {
237
428
  if (initial !== undefined) {
238
429
  throw `hsm: state "${describePath(path)}" has initial "${initial}" but no child states`;
239
430
  }
431
+ if (config.restoreDepth !== undefined) {
432
+ throw `hsm: state "${describePath(path)}" has restoreDepth but no child states`;
433
+ }
240
434
  return;
241
435
  }
436
+ compileRestoreDepth(compiled, index);
242
437
  if (initial === undefined) {
243
438
  throw `hsm: compound state "${describePath(path)}" has no initial`;
244
439
  }
@@ -249,6 +444,47 @@ function compileInitial<Ctx, E extends EventObject>(
249
444
  compiled.initialChild[index] = child;
250
445
  }
251
446
 
447
+ function compileRestoreDepth<Ctx, E extends EventObject>(
448
+ compiled: Compiled<Ctx, E>,
449
+ index: number,
450
+ ): void {
451
+ const depth = (compiled.configs[index] as StateConfig<Ctx, E>).restoreDepth;
452
+ if (depth === undefined) {
453
+ return;
454
+ }
455
+ const path = describePath(compiled.paths[index] as string);
456
+ const levels = compiled.levels[index] as number;
457
+ if (depth === "all") {
458
+ compiled.restoreDepth[index] = levels;
459
+ return;
460
+ }
461
+ if (typeof depth !== "number" || Math.floor(depth) !== depth || depth < 1) {
462
+ throw `hsm: state "${path}" has restoreDepth ${depth}; use a whole number from 1, or "all"`;
463
+ }
464
+ if (depth > levels) {
465
+ throw `hsm: state "${path}" has restoreDepth ${depth}, but only ${levels} ${levels === 1 ? "level" : "levels"} of child states below it`;
466
+ }
467
+ compiled.restoreDepth[index] = depth;
468
+ }
469
+
470
+ // A state records its last child only while some restoreDepth above or on it reaches its level.
471
+ function compileRecords<Ctx, E extends EventObject>(compiled: Compiled<Ctx, E>): void {
472
+ const reach: number[] = [];
473
+ for (let index = 0; index < compiled.count; index++) {
474
+ const owner = compiled.parent[index] as number;
475
+ let inherited = 0;
476
+ if (owner !== NO_STATE) {
477
+ inherited = reach[owner] as number;
478
+ if (compiled.parallel[owner] !== true) {
479
+ inherited--;
480
+ }
481
+ }
482
+ const current = Math.max(compiled.restoreDepth[index] as number, inherited);
483
+ reach[index] = current;
484
+ compiled.records[index] = current > 0 && compiled.parallel[index] !== true;
485
+ }
486
+ }
487
+
252
488
  function compileTransition<Ctx, E extends EventObject>(
253
489
  compiled: Compiled<Ctx, E>,
254
490
  source: number,
@@ -257,25 +493,23 @@ function compileTransition<Ctx, E extends EventObject>(
257
493
  if (typeof spec === "string") {
258
494
  return {
259
495
  target: resolveTarget(compiled, source, spec),
260
- guard: undefined,
261
- actions: [],
262
- reenter: false,
496
+ when: undefined,
497
+ run: [],
263
498
  };
264
499
  }
265
- const actions: TransitionAction<Ctx, E>[] = [];
266
- const declared = spec.actions;
500
+ const run: MoveAction<Ctx, E>[] = [];
501
+ const declared = spec.run;
267
502
  if (typeof declared === "function") {
268
- actions.push(declared);
503
+ run.push(declared);
269
504
  } else if (declared !== undefined) {
270
505
  for (let i = 0; i < declared.length; i++) {
271
- actions.push(declared[i] as TransitionAction<Ctx, E>);
506
+ run.push(declared[i] as MoveAction<Ctx, E>);
272
507
  }
273
508
  }
274
509
  return {
275
- target: spec.target === undefined ? NO_STATE : resolveTarget(compiled, source, spec.target),
276
- guard: spec.guard,
277
- actions,
278
- reenter: spec.reenter === true,
510
+ target: spec.to === undefined ? NO_STATE : resolveTarget(compiled, source, spec.to),
511
+ when: spec.when,
512
+ run,
279
513
  };
280
514
  }
281
515
 
@@ -310,6 +544,29 @@ function compileOn<Ctx, E extends EventObject>(compiled: Compiled<Ctx, E>, index
310
544
  }
311
545
  }
312
546
 
547
+ function compileAlways<Ctx, E extends EventObject>(
548
+ compiled: Compiled<Ctx, E>,
549
+ index: number,
550
+ ): void {
551
+ const list: Transition<Ctx, E>[] = [];
552
+ compiled.always[index] = list;
553
+ const spec = (compiled.configs[index] as StateConfig<Ctx, E>).always;
554
+ if (spec === undefined) {
555
+ return;
556
+ }
557
+ const candidates: readonly (string | AlwaysConfig<Ctx>)[] =
558
+ typeof spec === "string" || (spec as readonly AlwaysConfig<Ctx>[])[0] === undefined
559
+ ? [spec as string | AlwaysConfig<Ctx>]
560
+ : (spec as readonly AlwaysConfig<Ctx>[]);
561
+ for (let i = 0; i < candidates.length; i++) {
562
+ const candidate = candidates[i] as string | AlwaysConfig<Ctx>;
563
+ if (typeof candidate !== "string" && candidate.to === undefined) {
564
+ throw `hsm: state "${describePath(compiled.paths[index] as string)}" has an always transition with no "to"`;
565
+ }
566
+ list.push(compileTransition(compiled, index, candidate as string | TransitionConfig<Ctx, E>));
567
+ }
568
+ }
569
+
313
570
  const NOT_A_DELAY = -1;
314
571
 
315
572
  // Number-literal keys arrive as numbers in Lua and as canonical number strings in JavaScript.
@@ -360,16 +617,21 @@ function compileAfter<Ctx, E extends EventObject>(compiled: Compiled<Ctx, E>, in
360
617
  function compile<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>): Compiled<Ctx, E> {
361
618
  const compiled: Compiled<Ctx, E> = {
362
619
  count: 0,
363
- maxDepth: 0,
364
620
  configs: [],
365
621
  paths: [],
366
622
  parent: [],
367
623
  depth: [],
624
+ children: [],
625
+ parallel: [],
368
626
  initialChild: [],
627
+ restoreDepth: [],
628
+ levels: [],
629
+ records: [],
369
630
  onIndex: [],
370
631
  transitions: [],
371
632
  afterDelays: [],
372
633
  afterTargets: [],
634
+ always: [],
373
635
  pathIndex: {},
374
636
  };
375
637
  registerState(compiled, config, NO_STATE, "");
@@ -377,7 +639,9 @@ function compile<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>): Com
377
639
  compileInitial(compiled, index);
378
640
  compileOn(compiled, index);
379
641
  compileAfter(compiled, index);
642
+ compileAlways(compiled, index);
380
643
  }
644
+ compileRecords(compiled);
381
645
  return compiled;
382
646
  }
383
647
 
@@ -398,165 +662,551 @@ function transitionDomain<Ctx, E extends EventObject>(
398
662
  compiled: Compiled<Ctx, E>,
399
663
  source: number,
400
664
  target: number,
401
- reenter: boolean,
402
665
  ): number {
403
- if (source === ROOT || (!reenter && isAncestorOrSelf(compiled, source, target))) {
404
- return source;
666
+ let domain = source;
667
+ if (source !== ROOT && (target === source || !isAncestorOrSelf(compiled, source, target))) {
668
+ domain = compiled.parent[source] as number;
669
+ while (domain !== ROOT && (domain === target || !isAncestorOrSelf(compiled, domain, target))) {
670
+ domain = compiled.parent[domain] as number;
671
+ }
405
672
  }
406
- let domain = compiled.parent[source] as number;
407
- while (domain !== ROOT && (domain === target || !isAncestorOrSelf(compiled, domain, target))) {
673
+ // Regions enter and exit together, so a move spanning a parallel state re-enters it.
674
+ while (compiled.parallel[domain] === true) {
408
675
  domain = compiled.parent[domain] as number;
409
676
  }
410
677
  return domain;
411
678
  }
412
679
 
413
- function createMachine<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>): Machine<Ctx, E> {
414
- const compiled = compile(config);
415
- const { paths, parent, depth, initialChild, configs, onIndex, transitions } = compiled;
416
- const { afterDelays, afterTargets } = compiled;
680
+ interface Definition<Ctx, E extends EventObject> {
681
+ compiled: Compiled<Ctx, E>;
682
+ generation: number;
683
+ }
684
+
685
+ interface Registered<Ctx, E extends EventObject> {
686
+ readonly definition: Definition<Ctx, E>;
687
+ readonly machine: Machine<Ctx, E>;
688
+ }
417
689
 
690
+ const registered: { [key: string]: Registered<unknown, EventObject> } = {};
691
+
692
+ function createMachine<Ctx, E extends EventObject>(
693
+ definition: Definition<Ctx, E>,
694
+ ): Machine<Ctx, E> {
418
695
  function start(ctx: Ctx): MachineInstance<Ctx, E> {
419
- const slots = compiled.maxDepth + 1;
420
- const active: number[] = [];
696
+ let compiled = definition.compiled;
697
+ let generation = definition.generation;
698
+ let paths = compiled.paths;
699
+ let parent = compiled.parent;
700
+ let children = compiled.children;
701
+ let parallel = compiled.parallel;
702
+ let initialChild = compiled.initialChild;
703
+ let restoreDepth = compiled.restoreDepth;
704
+ let records = compiled.records;
705
+ let configs = compiled.configs;
706
+ let onIndex = compiled.onIndex;
707
+ let transitions = compiled.transitions;
708
+ let afterDelays = compiled.afterDelays;
709
+ let afterTargets = compiled.afterTargets;
710
+ let always = compiled.always;
711
+ const activeChild: number[] = [];
712
+ const isActive: boolean[] = [];
421
713
  const elapsed: number[] = [];
422
714
  const fired: number[] = [];
423
- const scratch: number[] = [];
424
715
  const entryId: number[] = [];
425
- for (let i = 0; i < slots; i++) {
426
- active[i] = NO_STATE;
427
- elapsed[i] = 0;
428
- fired[i] = 0;
429
- scratch[i] = NO_STATE;
430
- entryId[i] = 0;
431
- }
716
+ const cleanup: ((() => void) | undefined)[] = [];
717
+ // Keyed by path, not index, so a remembered child outlives a rebind's recompilation.
718
+ const remembered: { [parentPath: string]: string } = {};
719
+ let slotCount = 0;
720
+ growSlots();
721
+ const leaves: string[] = [];
432
722
  let entryCount = 0;
433
723
  const queue: (E | undefined)[] = [];
434
724
  let queueHead = 0;
435
725
  let queueTail = 0;
436
- let leafDepth = NO_STATE;
437
726
  let running = true;
438
727
  let busy = false;
439
728
  let stopRequested = false;
729
+ let moved = false;
730
+ let listeners: MoveListener<E>[] = [];
440
731
 
441
732
  const instance = {
442
733
  ctx,
443
734
  path: undefined as string | undefined,
735
+ leaves,
444
736
  matches,
445
737
  send,
446
738
  update,
447
739
  stop,
740
+ onMove,
448
741
  };
449
742
 
743
+ function clearSlot(state: number): void {
744
+ activeChild[state] = NO_STATE;
745
+ isActive[state] = false;
746
+ elapsed[state] = 0;
747
+ fired[state] = 0;
748
+ entryId[state] = 0;
749
+ cleanup[state] = undefined;
750
+ }
751
+
752
+ function growSlots(): void {
753
+ while (slotCount < compiled.count) {
754
+ clearSlot(slotCount);
755
+ slotCount++;
756
+ }
757
+ }
758
+
759
+ function collectActive(state: number, order: number[]): void {
760
+ order.push(state);
761
+ const list = children[state] as number[];
762
+ for (let i = 0; i < list.length; i++) {
763
+ const child = list[i] as number;
764
+ if (isActive[child] === true) {
765
+ collectActive(child, order);
766
+ }
767
+ }
768
+ }
769
+
770
+ // Kept states hold their timers and entry id; a state whose path is gone, or whose
771
+ // parent was dropped, is dropped without exit hooks, since its config is gone.
772
+ function rebind(): void {
773
+ const from = instance.path as string;
774
+ const order: number[] = [];
775
+ collectActive(ROOT, order);
776
+ const keptPaths: string[] = [];
777
+ const keptElapsed: number[] = [];
778
+ const keptFired: number[] = [];
779
+ const keptEntryId: number[] = [];
780
+ const keptCleanup: ((() => void) | undefined)[] = [];
781
+ for (let i = 0; i < order.length; i++) {
782
+ const state = order[i] as number;
783
+ keptPaths[i] = paths[state] as string;
784
+ keptElapsed[i] = elapsed[state] as number;
785
+ keptFired[i] = fired[state] as number;
786
+ keptEntryId[i] = entryId[state] as number;
787
+ keptCleanup[i] = cleanup[state];
788
+ }
789
+ for (let state = 0; state < slotCount; state++) {
790
+ clearSlot(state);
791
+ }
792
+ compiled = definition.compiled;
793
+ generation = definition.generation;
794
+ paths = compiled.paths;
795
+ parent = compiled.parent;
796
+ children = compiled.children;
797
+ parallel = compiled.parallel;
798
+ initialChild = compiled.initialChild;
799
+ restoreDepth = compiled.restoreDepth;
800
+ records = compiled.records;
801
+ configs = compiled.configs;
802
+ onIndex = compiled.onIndex;
803
+ transitions = compiled.transitions;
804
+ afterDelays = compiled.afterDelays;
805
+ afterTargets = compiled.afterTargets;
806
+ always = compiled.always;
807
+ growSlots();
808
+ const dropped: number[] = [];
809
+ for (let i = 0; i < order.length; i++) {
810
+ const state = lookupPath(compiled, keptPaths[i] as string);
811
+ const owner = state === undefined ? NO_STATE : (parent[state] as number);
812
+ if (
813
+ state === undefined ||
814
+ (owner !== NO_STATE &&
815
+ (isActive[owner] !== true ||
816
+ (parallel[owner] !== true && activeChild[owner] !== NO_STATE)))
817
+ ) {
818
+ dropped.push(i);
819
+ continue;
820
+ }
821
+ isActive[state] = true;
822
+ if (owner !== NO_STATE && parallel[owner] !== true) {
823
+ activeChild[owner] = state;
824
+ }
825
+ elapsed[state] = keptElapsed[i] as number;
826
+ fired[state] = keptFired[i] as number;
827
+ entryId[state] = keptEntryId[i] as number;
828
+ cleanup[state] = keptCleanup[i];
829
+ }
830
+ for (let i = dropped.length - 1; i >= 0; i--) {
831
+ const run = keptCleanup[dropped[i] as number];
832
+ if (run !== undefined) {
833
+ run();
834
+ }
835
+ }
836
+ const entriesBefore = entryCount;
837
+ enterMissing(ROOT);
838
+ if (dropped.length === 0 && entryCount === entriesBefore) {
839
+ return;
840
+ }
841
+ refreshLeaves();
842
+ if (listeners.length > 0) {
843
+ report(from, instance.path, "reload", undefined);
844
+ }
845
+ settleAlways();
846
+ }
847
+
848
+ // Copy-on-write: a report walks the list it started with, so a listener removed mid-report
849
+ // never makes it skip another.
850
+ function onMove(listener: MoveListener<E>): () => void {
851
+ const added: MoveListener<E>[] = [];
852
+ for (let i = 0; i < listeners.length; i++) {
853
+ added.push(listeners[i] as MoveListener<E>);
854
+ }
855
+ added.push(listener);
856
+ listeners = added;
857
+ let removed = false;
858
+ return () => {
859
+ if (removed) {
860
+ return;
861
+ }
862
+ removed = true;
863
+ const kept: MoveListener<E>[] = [];
864
+ let skipped = false;
865
+ for (let i = 0; i < listeners.length; i++) {
866
+ const current = listeners[i] as MoveListener<E>;
867
+ if (current !== listener || skipped) {
868
+ kept.push(current);
869
+ } else {
870
+ skipped = true;
871
+ }
872
+ }
873
+ listeners = kept;
874
+ };
875
+ }
876
+
877
+ function report(
878
+ from: string,
879
+ to: string | undefined,
880
+ cause: MoveCause,
881
+ event: E | undefined,
882
+ ): void {
883
+ const current = listeners;
884
+ for (let i = 0; i < current.length; i++) {
885
+ (current[i] as MoveListener<E>)(from, to, cause, event);
886
+ }
887
+ }
888
+
889
+ function collectLeaves(state: number, count: number): number {
890
+ if (parallel[state] === true) {
891
+ const list = children[state] as number[];
892
+ let total = count;
893
+ for (let i = 0; i < list.length; i++) {
894
+ total = collectLeaves(list[i] as number, total);
895
+ }
896
+ return total;
897
+ }
898
+ const child = activeChild[state] as number;
899
+ if (child !== NO_STATE) {
900
+ return collectLeaves(child, count);
901
+ }
902
+ leaves[count] = paths[state] as string;
903
+ return count + 1;
904
+ }
905
+
906
+ function refreshLeaves(): void {
907
+ const count = isActive[ROOT] === true ? collectLeaves(ROOT, 0) : 0;
908
+ while (leaves.length > count) {
909
+ leaves.pop();
910
+ }
911
+ instance.path = leaves[0];
912
+ }
913
+
450
914
  function enterState(state: number): void {
451
- const level = depth[state] as number;
452
- active[level] = state;
453
- elapsed[level] = 0;
454
- fired[level] = 0;
455
- leafDepth = level;
915
+ const owner = parent[state] as number;
916
+ if (owner !== NO_STATE && parallel[owner] !== true) {
917
+ activeChild[owner] = state;
918
+ }
919
+ isActive[state] = true;
920
+ activeChild[state] = NO_STATE;
921
+ elapsed[state] = 0;
922
+ fired[state] = 0;
456
923
  entryCount++;
457
924
  const id = entryCount;
458
- entryId[level] = id;
925
+ entryId[state] = id;
459
926
  const config = configs[state] as StateConfig<Ctx, E>;
460
927
  const hook = config.enter;
461
928
  if (hook !== undefined) {
462
929
  hook(ctx, instance);
463
930
  }
464
- const invoke = config.invoke;
465
- if (invoke !== undefined) {
466
- let settled = false;
467
- invoke(
931
+ const task = config.task;
932
+ if (task !== undefined) {
933
+ // A path, not an index, so a finish held across a rebind still finds its entry.
934
+ const path = paths[state] as string;
935
+ let finished = false;
936
+ const result = task(
468
937
  ctx,
469
938
  (event: E) => {
470
- if (settled || !running || stopRequested || entryId[level] !== id) {
939
+ const current = lookupPath(compiled, path);
940
+ if (
941
+ finished ||
942
+ !running ||
943
+ stopRequested ||
944
+ current === undefined ||
945
+ entryId[current] !== id
946
+ ) {
471
947
  return;
472
948
  }
473
- settled = true;
949
+ finished = true;
474
950
  send(event);
475
951
  },
476
952
  instance,
477
953
  );
954
+ if (typeof result === "function") {
955
+ cleanup[state] = result;
956
+ }
478
957
  }
479
958
  }
480
959
 
481
- function exitTo(level: number): void {
482
- while (leafDepth > level) {
483
- const state = active[leafDepth] as number;
484
- active[leafDepth] = NO_STATE;
485
- entryId[leafDepth] = 0;
486
- leafDepth--;
487
- const hook = (configs[state] as StateConfig<Ctx, E>).exit;
488
- if (hook !== undefined) {
489
- hook(ctx, instance);
960
+ function exitSubtree(state: number): void {
961
+ if (parallel[state] === true) {
962
+ const list = children[state] as number[];
963
+ for (let i = list.length - 1; i >= 0; i--) {
964
+ exitSubtree(list[i] as number);
965
+ }
966
+ }
967
+ const child = activeChild[state] as number;
968
+ if (child !== NO_STATE) {
969
+ exitSubtree(child);
970
+ }
971
+ isActive[state] = false;
972
+ entryId[state] = 0;
973
+ const owner = parent[state] as number;
974
+ if (owner !== NO_STATE) {
975
+ activeChild[owner] = NO_STATE;
976
+ if (records[owner] === true) {
977
+ remembered[paths[owner] as string] = paths[state] as string;
490
978
  }
491
979
  }
980
+ const hook = (configs[state] as StateConfig<Ctx, E>).exit;
981
+ if (hook !== undefined) {
982
+ hook(ctx, instance);
983
+ }
984
+ const run = cleanup[state];
985
+ if (run !== undefined) {
986
+ cleanup[state] = undefined;
987
+ run();
988
+ }
492
989
  }
493
990
 
494
- function enterDown(from: number, target: number): void {
495
- let count = 0;
496
- let state = target;
497
- while (state !== from) {
498
- scratch[count] = state;
499
- count++;
500
- state = parent[state] as number;
991
+ function defaultChild(state: number, restore: boolean): number {
992
+ if (restore) {
993
+ const path = remembered[paths[state] as string];
994
+ if (path !== undefined) {
995
+ const child = lookupPath(compiled, path);
996
+ if (child !== undefined && parent[child] === state) {
997
+ return child;
998
+ }
999
+ }
1000
+ }
1001
+ return initialChild[state] as number;
1002
+ }
1003
+
1004
+ // budget counts the levels still restored from an ancestor's restoreDepth; a parallel
1005
+ // state passes it to every region without spending a level.
1006
+ function enterDefaults(state: number, budget: number): void {
1007
+ if (parallel[state] === true) {
1008
+ const list = children[state] as number[];
1009
+ for (let i = 0; i < list.length; i++) {
1010
+ enterState(list[i] as number);
1011
+ enterDefaults(list[i] as number, budget);
1012
+ }
1013
+ return;
501
1014
  }
502
- while (count > 0) {
503
- count--;
504
- enterState(scratch[count] as number);
505
- scratch[count] = NO_STATE;
1015
+ const remaining = Math.max(restoreDepth[state] as number, budget);
1016
+ const child = defaultChild(state, remaining > 0);
1017
+ if (child !== NO_STATE) {
1018
+ enterState(child);
1019
+ enterDefaults(child, remaining - 1);
506
1020
  }
507
- state = target;
508
- while (initialChild[state] !== NO_STATE) {
509
- state = initialChild[state] as number;
510
- enterState(state);
1021
+ }
1022
+
1023
+ function childToward(state: number, target: number): number {
1024
+ let child = target;
1025
+ while (parent[child] !== state) {
1026
+ child = parent[child] as number;
1027
+ }
1028
+ return child;
1029
+ }
1030
+
1031
+ function enterToward(state: number, target: number): void {
1032
+ enterState(state);
1033
+ if (state === target) {
1034
+ enterDefaults(state, 0);
1035
+ return;
1036
+ }
1037
+ const next = childToward(state, target);
1038
+ if (parallel[state] !== true) {
1039
+ enterToward(next, target);
1040
+ return;
1041
+ }
1042
+ const list = children[state] as number[];
1043
+ for (let i = 0; i < list.length; i++) {
1044
+ const region = list[i] as number;
1045
+ if (region === next) {
1046
+ enterToward(region, target);
1047
+ } else {
1048
+ enterState(region);
1049
+ enterDefaults(region, 0);
1050
+ }
1051
+ }
1052
+ }
1053
+
1054
+ function enterMissing(state: number): void {
1055
+ if (parallel[state] === true) {
1056
+ const list = children[state] as number[];
1057
+ for (let i = 0; i < list.length; i++) {
1058
+ const region = list[i] as number;
1059
+ if (isActive[region] === true) {
1060
+ enterMissing(region);
1061
+ } else {
1062
+ enterState(region);
1063
+ enterDefaults(region, 0);
1064
+ }
1065
+ }
1066
+ return;
1067
+ }
1068
+ const child = activeChild[state] as number;
1069
+ if (child !== NO_STATE) {
1070
+ enterMissing(child);
1071
+ return;
1072
+ }
1073
+ enterDefaults(state, 0);
1074
+ }
1075
+
1076
+ function firstLeaf(state: number): string {
1077
+ let leaf = state;
1078
+ while (true) {
1079
+ const child =
1080
+ parallel[leaf] === true
1081
+ ? ((children[leaf] as number[])[0] as number)
1082
+ : (activeChild[leaf] as number);
1083
+ if (child === NO_STATE) {
1084
+ return paths[leaf] as string;
1085
+ }
1086
+ leaf = child;
511
1087
  }
512
- instance.path = paths[active[leafDepth] as number] as string;
513
1088
  }
514
1089
 
515
1090
  function transition(
516
1091
  source: number,
517
1092
  target: number,
518
- reenter: boolean,
519
- actions: readonly TransitionAction<Ctx, E>[] | undefined,
1093
+ run: readonly MoveAction<Ctx, E>[] | undefined,
520
1094
  event: E | undefined,
1095
+ cause: MoveCause,
521
1096
  ): void {
522
1097
  if (target === NO_STATE) {
523
- runActions(actions, event);
1098
+ runActions(run, event);
524
1099
  return;
525
1100
  }
526
- const domain = transitionDomain(compiled, source, target, reenter);
527
- exitTo(depth[domain] as number);
528
- runActions(actions, event);
529
- enterDown(domain, target);
1101
+ const domain = transitionDomain(compiled, source, target);
1102
+ const from = firstLeaf(domain);
1103
+ const child = activeChild[domain] as number;
1104
+ if (child !== NO_STATE) {
1105
+ exitSubtree(child);
1106
+ }
1107
+ runActions(run, event);
1108
+ if (target === domain) {
1109
+ enterDefaults(domain, 0);
1110
+ } else {
1111
+ enterToward(childToward(domain, target), target);
1112
+ }
1113
+ refreshLeaves();
1114
+ moved = true;
1115
+ if (listeners.length > 0) {
1116
+ report(from, firstLeaf(domain), cause, event);
1117
+ }
1118
+ }
1119
+
1120
+ function settleAlways(): void {
1121
+ for (let moves = 0; !stopRequested; moves++) {
1122
+ if (!takeAlways(ROOT, moves === ALWAYS_LIMIT)) {
1123
+ return;
1124
+ }
1125
+ }
1126
+ }
1127
+
1128
+ function takeAlways(state: number, overLimit: boolean): boolean {
1129
+ if (parallel[state] === true) {
1130
+ const list = children[state] as number[];
1131
+ for (let i = 0; i < list.length; i++) {
1132
+ if (takeAlways(list[i] as number, overLimit)) {
1133
+ return true;
1134
+ }
1135
+ }
1136
+ }
1137
+ const child = activeChild[state] as number;
1138
+ if (child !== NO_STATE && takeAlways(child, overLimit)) {
1139
+ return true;
1140
+ }
1141
+ const list = always[state] as Transition<Ctx, E>[];
1142
+ for (let i = 0; i < list.length; i++) {
1143
+ const candidate = list[i] as Transition<Ctx, E>;
1144
+ const when = candidate.when as ((ctx: Ctx) => boolean) | undefined;
1145
+ if (when === undefined || when(ctx)) {
1146
+ if (overLimit) {
1147
+ clearQueue();
1148
+ busy = false;
1149
+ throw `hsm: state "${describePath(paths[state] as string)}" took ${ALWAYS_LIMIT} always transitions in a row; check for an always loop`;
1150
+ }
1151
+ transition(state, candidate.target, undefined, undefined, "always");
1152
+ return true;
1153
+ }
1154
+ }
1155
+ return false;
530
1156
  }
531
1157
 
532
1158
  function runActions(
533
- actions: readonly TransitionAction<Ctx, E>[] | undefined,
1159
+ run: readonly MoveAction<Ctx, E>[] | undefined,
534
1160
  event: E | undefined,
535
1161
  ): void {
536
- if (actions === undefined || event === undefined) {
1162
+ if (run === undefined || event === undefined) {
537
1163
  return;
538
1164
  }
539
- for (let i = 0; i < actions.length; i++) {
540
- (actions[i] as TransitionAction<Ctx, E>)(ctx, event, instance);
1165
+ for (let i = 0; i < run.length; i++) {
1166
+ (run[i] as MoveAction<Ctx, E>)(ctx, event, instance);
541
1167
  }
542
1168
  }
543
1169
 
544
- function processEvent(event: E): void {
545
- for (let level = leafDepth; level >= 0; level--) {
546
- const state = active[level] as number;
547
- const listIndex = (onIndex[state] as { [type: string]: number })[event.type];
548
- if (typeof listIndex !== "number") {
549
- continue;
1170
+ function takeEvent(state: number, event: E): boolean {
1171
+ const listIndex = (onIndex[state] as { [type: string]: number })[event.type];
1172
+ if (typeof listIndex !== "number") {
1173
+ return false;
1174
+ }
1175
+ const list = transitions[listIndex] as Transition<Ctx, E>[];
1176
+ for (let i = 0; i < list.length; i++) {
1177
+ const candidate = list[i] as Transition<Ctx, E>;
1178
+ if (candidate.when === undefined || candidate.when(ctx, event)) {
1179
+ transition(state, candidate.target, candidate.run, event, "event");
1180
+ return true;
550
1181
  }
551
- const list = transitions[listIndex] as Transition<Ctx, E>[];
1182
+ }
1183
+ return false;
1184
+ }
1185
+
1186
+ // Each region takes at most one transition; the event bubbles past the parallel
1187
+ // state only when none did, and a move that left or re-entered it ends the offer.
1188
+ function offerEvent(state: number, event: E): boolean {
1189
+ if (parallel[state] === true) {
1190
+ const id = entryId[state];
1191
+ const list = children[state] as number[];
1192
+ let taken = false;
552
1193
  for (let i = 0; i < list.length; i++) {
553
- const candidate = list[i] as Transition<Ctx, E>;
554
- if (candidate.guard === undefined || candidate.guard(ctx, event)) {
555
- transition(state, candidate.target, candidate.reenter, candidate.actions, event);
556
- return;
1194
+ if (offerEvent(list[i] as number, event)) {
1195
+ taken = true;
1196
+ if (stopRequested || entryId[state] !== id) {
1197
+ return true;
1198
+ }
557
1199
  }
558
1200
  }
1201
+ if (taken) {
1202
+ return true;
1203
+ }
1204
+ }
1205
+ const child = activeChild[state] as number;
1206
+ if (child !== NO_STATE && offerEvent(child, event)) {
1207
+ return true;
559
1208
  }
1209
+ return takeEvent(state, event);
560
1210
  }
561
1211
 
562
1212
  function clearQueue(): void {
@@ -572,9 +1222,13 @@ function createMachine<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>
572
1222
  running = false;
573
1223
  busy = true;
574
1224
  clearQueue();
575
- exitTo(NO_STATE);
576
- instance.path = undefined;
1225
+ const from = instance.path as string;
1226
+ exitSubtree(ROOT);
1227
+ refreshLeaves();
577
1228
  busy = false;
1229
+ if (listeners.length > 0) {
1230
+ report(from, undefined, "stop", undefined);
1231
+ }
578
1232
  }
579
1233
 
580
1234
  function endStep(): void {
@@ -582,7 +1236,11 @@ function createMachine<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>
582
1236
  const event = queue[queueHead] as E;
583
1237
  queue[queueHead] = undefined;
584
1238
  queueHead++;
585
- processEvent(event);
1239
+ moved = false;
1240
+ offerEvent(ROOT, event);
1241
+ if (moved) {
1242
+ settleAlways();
1243
+ }
586
1244
  }
587
1245
  clearQueue();
588
1246
  busy = false;
@@ -601,48 +1259,86 @@ function createMachine<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>
601
1259
  return;
602
1260
  }
603
1261
  busy = true;
1262
+ if (generation !== definition.generation) {
1263
+ rebind();
1264
+ }
604
1265
  endStep();
605
1266
  }
606
1267
 
607
- function fireUpdateHooks(dt: number): boolean {
608
- for (let level = leafDepth; level >= 0; level--) {
609
- const state = active[level] as number;
610
- const hook = (configs[state] as StateConfig<Ctx, E>).update;
611
- if (hook === undefined) {
612
- continue;
1268
+ function addElapsed(state: number, dt: number): void {
1269
+ elapsed[state] = (elapsed[state] as number) + dt;
1270
+ if (parallel[state] === true) {
1271
+ const list = children[state] as number[];
1272
+ for (let i = 0; i < list.length; i++) {
1273
+ addElapsed(list[i] as number, dt);
613
1274
  }
614
- const target = hook(ctx, dt, instance);
615
- if (stopRequested) {
616
- return true;
1275
+ }
1276
+ const child = activeChild[state] as number;
1277
+ if (child !== NO_STATE) {
1278
+ addElapsed(child, dt);
1279
+ }
1280
+ }
1281
+
1282
+ function tickRegion(state: number, dt: number): boolean {
1283
+ return fireUpdateHooks(state, dt) || fireTimers(state);
1284
+ }
1285
+
1286
+ // A parallel state's timers fire only after its regions tick, and only if none moved.
1287
+ function fireUpdateHooks(state: number, dt: number): boolean {
1288
+ if (parallel[state] === true) {
1289
+ const id = entryId[state];
1290
+ const list = children[state] as number[];
1291
+ let regionMoved = false;
1292
+ for (let i = 0; i < list.length; i++) {
1293
+ if (tickRegion(list[i] as number, dt)) {
1294
+ regionMoved = true;
1295
+ if (stopRequested || entryId[state] !== id) {
1296
+ return true;
1297
+ }
1298
+ }
617
1299
  }
618
- if (typeof target === "string") {
619
- transition(state, resolveTarget(compiled, state, target), false, undefined, undefined);
1300
+ if (regionMoved) {
620
1301
  return true;
621
1302
  }
622
1303
  }
1304
+ const child = activeChild[state] as number;
1305
+ if (child !== NO_STATE && fireUpdateHooks(child, dt)) {
1306
+ return true;
1307
+ }
1308
+ const hook = (configs[state] as StateConfig<Ctx, E>).update;
1309
+ if (hook === undefined) {
1310
+ return false;
1311
+ }
1312
+ const target = hook(ctx, dt, instance);
1313
+ if (stopRequested) {
1314
+ return true;
1315
+ }
1316
+ if (typeof target === "string") {
1317
+ transition(state, resolveTarget(compiled, state, target), undefined, undefined, "update");
1318
+ return true;
1319
+ }
623
1320
  return false;
624
1321
  }
625
1322
 
626
- function fireTimers(dt: number): void {
627
- for (let level = 0; level <= leafDepth; level++) {
628
- elapsed[level] = (elapsed[level] as number) + dt;
629
- }
630
- for (let level = leafDepth; level >= 0; level--) {
631
- const state = active[level] as number;
632
- const delays = afterDelays[state] as number[];
633
- const next = fired[level] as number;
634
- if (next < delays.length && (delays[next] as number) <= (elapsed[level] as number)) {
635
- fired[level] = next + 1;
636
- transition(
637
- state,
638
- (afterTargets[state] as number[])[next] as number,
639
- false,
640
- undefined,
641
- undefined,
642
- );
643
- return;
644
- }
1323
+ function fireTimers(state: number): boolean {
1324
+ const child = activeChild[state] as number;
1325
+ if (child !== NO_STATE && fireTimers(child)) {
1326
+ return true;
645
1327
  }
1328
+ const delays = afterDelays[state] as number[];
1329
+ const next = fired[state] as number;
1330
+ if (next < delays.length && (delays[next] as number) <= (elapsed[state] as number)) {
1331
+ fired[state] = next + 1;
1332
+ transition(
1333
+ state,
1334
+ (afterTargets[state] as number[])[next] as number,
1335
+ undefined,
1336
+ undefined,
1337
+ "after",
1338
+ );
1339
+ return true;
1340
+ }
1341
+ return false;
646
1342
  }
647
1343
 
648
1344
  function update(dt: number): void {
@@ -650,8 +1346,16 @@ function createMachine<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>
650
1346
  return;
651
1347
  }
652
1348
  busy = true;
653
- if (!fireUpdateHooks(dt)) {
654
- fireTimers(dt);
1349
+ if (generation !== definition.generation) {
1350
+ rebind();
1351
+ }
1352
+ if (!stopRequested) {
1353
+ addElapsed(ROOT, dt);
1354
+ moved = false;
1355
+ tickRegion(ROOT, dt);
1356
+ if (moved) {
1357
+ settleAlways();
1358
+ }
655
1359
  }
656
1360
  endStep();
657
1361
  }
@@ -668,17 +1372,15 @@ function createMachine<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>
668
1372
  }
669
1373
 
670
1374
  function matches(path: string): boolean {
671
- for (let level = 1; level <= leafDepth; level++) {
672
- if (paths[active[level] as number] === path) {
673
- return true;
674
- }
675
- }
676
- return false;
1375
+ const state = lookupPath(compiled, path);
1376
+ return state !== undefined && state !== ROOT && isActive[state] === true;
677
1377
  }
678
1378
 
679
1379
  busy = true;
680
1380
  enterState(ROOT);
681
- enterDown(ROOT, ROOT);
1381
+ enterDefaults(ROOT, 0);
1382
+ refreshLeaves();
1383
+ settleAlways();
682
1384
  endStep();
683
1385
  return instance;
684
1386
  }
@@ -686,11 +1388,31 @@ function createMachine<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>
686
1388
  return { start };
687
1389
  }
688
1390
 
1391
+ function define<Ctx, E extends EventObject>(
1392
+ key: string | undefined,
1393
+ config: MachineConfig<Ctx, E>,
1394
+ ): Machine<Ctx, E> {
1395
+ const compiled = compile(config);
1396
+ if (key === undefined) {
1397
+ return createMachine({ compiled, generation: 0 });
1398
+ }
1399
+ const existing = registered[key] as unknown as Registered<Ctx, E> | undefined;
1400
+ if (existing !== undefined) {
1401
+ const definition = existing.definition;
1402
+ definition.compiled = compiled;
1403
+ definition.generation = definition.generation + 1;
1404
+ return existing.machine;
1405
+ }
1406
+ const definition: Definition<Ctx, E> = { compiled, generation: 0 };
1407
+ const machine = createMachine(definition);
1408
+ registered[key] = { definition, machine } as unknown as Registered<unknown, EventObject>;
1409
+ return machine;
1410
+ }
1411
+
689
1412
  // Ctx and E are given explicitly and the config is inferred, so the config takes a second call.
690
- export function defineMachine<Ctx, E extends EventObject>(): <
691
- const C extends MachineConfig<Ctx, E>,
692
- >(
693
- config: C,
694
- ) => DefinedMachine<Ctx, E, C> {
695
- return createMachine as never;
1413
+ // A key makes a later definition under it (a hot-reloaded module re-running) rebind live instances.
1414
+ export function defineMachine<Ctx, E extends EventObject>(
1415
+ key?: string,
1416
+ ): <const C extends MachineConfig<Ctx, E>>(config: C) => DefinedMachine<Ctx, E, C> {
1417
+ return ((config: MachineConfig<Ctx, E>) => define(key, config)) as never;
696
1418
  }