@defold-typescript/transpiler 0.43.0 → 0.44.1
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/async.ts +73 -0
- package/dist/hsm/debug.ts +119 -0
- package/dist/hsm/defold.ts +14 -8
- package/dist/hsm/index.ts +895 -173
- package/dist/hsm-builtin.d.ts +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +46 -7
- package/dist/transpile.d.ts +2 -1
- package/package.json +3 -2
- package/src/hsm-builtin.ts +55 -4
- package/src/index.ts +1 -0
- package/src/transpile.ts +14 -4
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
|
|
33
|
+
export type MoveAction<Ctx, E extends EventObject, V extends E = E> = (
|
|
16
34
|
ctx: Ctx,
|
|
17
35
|
event: V,
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
107
|
+
machine: MachineInstance<Ctx, E>,
|
|
45
108
|
) => string | undefined;
|
|
46
109
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
94
|
-
readonly
|
|
95
|
-
readonly
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
227
|
+
readonly task?: unknown;
|
|
122
228
|
};
|
|
123
229
|
|
|
124
230
|
export interface MachineConfigError {
|
|
125
|
-
readonly "hsm: an initial or
|
|
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
|
|
138
|
-
readonly
|
|
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] =
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
reenter: false,
|
|
496
|
+
when: undefined,
|
|
497
|
+
run: [],
|
|
263
498
|
};
|
|
264
499
|
}
|
|
265
|
-
const
|
|
266
|
-
const declared = spec.
|
|
500
|
+
const run: MoveAction<Ctx, E>[] = [];
|
|
501
|
+
const declared = spec.run;
|
|
267
502
|
if (typeof declared === "function") {
|
|
268
|
-
|
|
503
|
+
run.push(declared);
|
|
269
504
|
} else if (declared !== undefined) {
|
|
270
505
|
for (let i = 0; i < declared.length; i++) {
|
|
271
|
-
|
|
506
|
+
run.push(declared[i] as MoveAction<Ctx, E>);
|
|
272
507
|
}
|
|
273
508
|
}
|
|
274
509
|
return {
|
|
275
|
-
target: spec.
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
404
|
-
|
|
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
|
-
|
|
407
|
-
while (domain
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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[
|
|
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
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
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(
|
|
1098
|
+
runActions(run, event);
|
|
524
1099
|
return;
|
|
525
1100
|
}
|
|
526
|
-
const domain = transitionDomain(compiled, source, target
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
1159
|
+
run: readonly MoveAction<Ctx, E>[] | undefined,
|
|
534
1160
|
event: E | undefined,
|
|
535
1161
|
): void {
|
|
536
|
-
if (
|
|
1162
|
+
if (run === undefined || event === undefined) {
|
|
537
1163
|
return;
|
|
538
1164
|
}
|
|
539
|
-
for (let i = 0; i <
|
|
540
|
-
(
|
|
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
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
576
|
-
|
|
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
|
-
|
|
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
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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 (
|
|
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(
|
|
627
|
-
|
|
628
|
-
|
|
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 (
|
|
654
|
-
|
|
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
|
-
|
|
672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
}
|