@defold-typescript/transpiler 0.42.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/emitted-requires.d.ts +1 -1
- package/dist/hsm/async.ts +73 -0
- package/dist/hsm/debug.ts +119 -0
- package/dist/hsm/defold.ts +39 -0
- package/dist/hsm/index.ts +1418 -0
- package/dist/hsm-builtin.d.ts +26 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +340 -130
- package/dist/transpile.d.ts +11 -0
- package/package.json +4 -3
- package/src/emitted-requires.ts +6 -4
- package/src/hsm-builtin.ts +246 -0
- package/src/index.ts +11 -1
- package/src/session.ts +2 -0
- package/src/transpile.ts +118 -26
|
@@ -0,0 +1,1418 @@
|
|
|
1
|
+
export interface EventObject {
|
|
2
|
+
readonly type: string;
|
|
3
|
+
}
|
|
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
|
+
|
|
15
|
+
/** @noSelf */
|
|
16
|
+
export interface MachineInstance<Ctx, E extends EventObject, P extends string = string> {
|
|
17
|
+
readonly ctx: Ctx;
|
|
18
|
+
readonly path: P | undefined;
|
|
19
|
+
readonly leaves: readonly P[];
|
|
20
|
+
readonly matches: (path: P) => boolean;
|
|
21
|
+
readonly send: (event: E) => void;
|
|
22
|
+
readonly update: (dt: number) => void;
|
|
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;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type MoveAction<Ctx, E extends EventObject, V extends E = E> = (
|
|
34
|
+
ctx: Ctx,
|
|
35
|
+
event: V,
|
|
36
|
+
machine: MachineInstance<Ctx, E>,
|
|
37
|
+
) => void;
|
|
38
|
+
|
|
39
|
+
/** @noSelf */
|
|
40
|
+
export interface TransitionConfig<Ctx, E extends EventObject, V extends E = E> {
|
|
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;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type TransitionSpec<Ctx, E extends EventObject, V extends E = E> =
|
|
69
|
+
| string
|
|
70
|
+
| TransitionConfig<Ctx, E, V>
|
|
71
|
+
// Lua cannot tell [] from {}, so an empty list would compile to a transition with no to.
|
|
72
|
+
| readonly [TransitionConfig<Ctx, E, V>, ...TransitionConfig<Ctx, E, V>[]];
|
|
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
|
+
|
|
90
|
+
export type OnConfig<Ctx, E extends EventObject> = {
|
|
91
|
+
readonly [K in E["type"]]?: TransitionSpec<Ctx, E, Extract<E, { type: K }>>;
|
|
92
|
+
};
|
|
93
|
+
|
|
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;
|
|
103
|
+
|
|
104
|
+
export type UpdateHook<Ctx, E extends EventObject> = (
|
|
105
|
+
ctx: Ctx,
|
|
106
|
+
dt: number,
|
|
107
|
+
machine: MachineInstance<Ctx, E>,
|
|
108
|
+
) => string | undefined;
|
|
109
|
+
|
|
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> = (
|
|
116
|
+
ctx: Ctx,
|
|
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;
|
|
121
|
+
|
|
122
|
+
/** @noSelf */
|
|
123
|
+
export interface StateConfig<Ctx, E extends EventObject> {
|
|
124
|
+
readonly type?: "parallel";
|
|
125
|
+
readonly initial?: string;
|
|
126
|
+
readonly restoreDepth?: number | "all";
|
|
127
|
+
readonly states?: { readonly [name: string]: StateConfig<Ctx, E> };
|
|
128
|
+
readonly on?: OnConfig<Ctx, E>;
|
|
129
|
+
readonly after?: { readonly [seconds: number]: string };
|
|
130
|
+
readonly always?: AlwaysSpec<Ctx>;
|
|
131
|
+
readonly enter?: StateHook<Ctx, E>;
|
|
132
|
+
readonly exit?: StateHook<Ctx, E>;
|
|
133
|
+
readonly update?: UpdateHook<Ctx, E>;
|
|
134
|
+
/** Started on each entry; see `TaskStart`. */
|
|
135
|
+
readonly task?: TaskStart<Ctx, E>;
|
|
136
|
+
/** Renamed to `task`. */
|
|
137
|
+
readonly invoke?: never;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface MachineConfig<Ctx, E extends EventObject> extends StateConfig<Ctx, E> {
|
|
141
|
+
readonly initial: string;
|
|
142
|
+
readonly states: { readonly [name: string]: StateConfig<Ctx, E> };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** @noSelf */
|
|
146
|
+
export interface Machine<Ctx, E extends EventObject, P extends string = string> {
|
|
147
|
+
readonly start: (ctx: Ctx) => MachineInstance<Ctx, E, P>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type PathDepth = [never, 0, 1, 2, 3];
|
|
151
|
+
|
|
152
|
+
type PathsBelow<S, D extends number> = S extends { readonly states: infer Children }
|
|
153
|
+
? {
|
|
154
|
+
[K in keyof Children & string]:
|
|
155
|
+
| K
|
|
156
|
+
| `${K}/${D extends 1
|
|
157
|
+
? Children[K] extends { readonly states: object }
|
|
158
|
+
? string
|
|
159
|
+
: never
|
|
160
|
+
: PathsBelow<Children[K], PathDepth[D]>}`;
|
|
161
|
+
}[keyof Children & string]
|
|
162
|
+
: never;
|
|
163
|
+
|
|
164
|
+
export type StatePath<C> = `/${PathsBelow<C, 4>}`;
|
|
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
|
+
|
|
190
|
+
interface TransitionCheck<T> {
|
|
191
|
+
readonly to?: T;
|
|
192
|
+
readonly when?: unknown;
|
|
193
|
+
readonly run?: unknown;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
type SpecCheck<T> = T | TransitionCheck<T> | readonly TransitionCheck<T>[];
|
|
197
|
+
|
|
198
|
+
// Hooks are listed as unknown so a hooks-only leaf still shares a property with this
|
|
199
|
+
// all-optional type; without them TypeScript rejects it as a weak-type mismatch.
|
|
200
|
+
type PathCheck<S, Self extends string, All extends string, Ev extends string> = {
|
|
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;
|
|
212
|
+
readonly states?: S extends { readonly states: infer Children }
|
|
213
|
+
? {
|
|
214
|
+
readonly [K in keyof Children]: PathCheck<Children[K], `${Self}/${K & string}`, All, Ev>;
|
|
215
|
+
}
|
|
216
|
+
: unknown;
|
|
217
|
+
readonly on?: S extends { readonly on: infer On }
|
|
218
|
+
? { readonly [K in keyof On]: K extends Ev ? SpecCheck<All> : never }
|
|
219
|
+
: unknown;
|
|
220
|
+
readonly after?: S extends { readonly after: infer After }
|
|
221
|
+
? { readonly [K in keyof After]: All }
|
|
222
|
+
: unknown;
|
|
223
|
+
readonly always?: S extends { readonly always: unknown } ? SpecCheck<All> : unknown;
|
|
224
|
+
readonly enter?: unknown;
|
|
225
|
+
readonly exit?: unknown;
|
|
226
|
+
readonly update?: unknown;
|
|
227
|
+
readonly task?: unknown;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export interface MachineConfigError {
|
|
231
|
+
readonly "hsm: an initial or a to names an unknown state path, or an on key an unknown event": never;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export type DefinedMachine<Ctx, E extends EventObject, C> =
|
|
235
|
+
C extends PathCheck<C, "", StatePath<C>, E["type"]>
|
|
236
|
+
? Machine<Ctx, E, StatePath<C>>
|
|
237
|
+
: MachineConfigError;
|
|
238
|
+
|
|
239
|
+
type Guard<Ctx, E extends EventObject> = (ctx: Ctx, event: E) => boolean;
|
|
240
|
+
|
|
241
|
+
interface Transition<Ctx, E extends EventObject> {
|
|
242
|
+
readonly target: number;
|
|
243
|
+
readonly when: Guard<Ctx, E> | undefined;
|
|
244
|
+
readonly run: readonly MoveAction<Ctx, E>[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
interface Compiled<Ctx, E extends EventObject> {
|
|
248
|
+
count: number;
|
|
249
|
+
readonly configs: StateConfig<Ctx, E>[];
|
|
250
|
+
readonly paths: string[];
|
|
251
|
+
readonly parent: number[];
|
|
252
|
+
readonly depth: number[];
|
|
253
|
+
readonly children: number[][];
|
|
254
|
+
readonly parallel: boolean[];
|
|
255
|
+
readonly initialChild: number[];
|
|
256
|
+
readonly restoreDepth: number[];
|
|
257
|
+
readonly levels: number[];
|
|
258
|
+
readonly records: boolean[];
|
|
259
|
+
readonly onIndex: { [type: string]: number }[];
|
|
260
|
+
readonly transitions: Transition<Ctx, E>[][];
|
|
261
|
+
readonly afterDelays: number[][];
|
|
262
|
+
readonly afterTargets: number[][];
|
|
263
|
+
readonly always: Transition<Ctx, E>[][];
|
|
264
|
+
readonly pathIndex: { [path: string]: number };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const ROOT = 0;
|
|
268
|
+
const NO_STATE = -1;
|
|
269
|
+
const ALWAYS_LIMIT = 10;
|
|
270
|
+
|
|
271
|
+
function describePath(path: string): string {
|
|
272
|
+
return path === "" ? "(root)" : path;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function lookupPath<Ctx, E extends EventObject>(
|
|
276
|
+
compiled: Compiled<Ctx, E>,
|
|
277
|
+
path: string,
|
|
278
|
+
): number | undefined {
|
|
279
|
+
const index = compiled.pathIndex[path];
|
|
280
|
+
return typeof index === "number" ? index : undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function resolveTarget<Ctx, E extends EventObject>(
|
|
284
|
+
compiled: Compiled<Ctx, E>,
|
|
285
|
+
source: number,
|
|
286
|
+
target: string,
|
|
287
|
+
): number {
|
|
288
|
+
const sourcePath = describePath(compiled.paths[source] as string);
|
|
289
|
+
if (target.charAt(0) !== "/") {
|
|
290
|
+
throw `hsm: state "${sourcePath}" targets "${target}", which is not a full path starting with "/"`;
|
|
291
|
+
}
|
|
292
|
+
const index = lookupPath(compiled, target);
|
|
293
|
+
if (index === undefined) {
|
|
294
|
+
throw `hsm: state "${sourcePath}" targets unknown state "${target}"`;
|
|
295
|
+
}
|
|
296
|
+
return index;
|
|
297
|
+
}
|
|
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
|
+
|
|
339
|
+
function registerState<Ctx, E extends EventObject>(
|
|
340
|
+
compiled: Compiled<Ctx, E>,
|
|
341
|
+
config: StateConfig<Ctx, E>,
|
|
342
|
+
parentIndex: number,
|
|
343
|
+
path: string,
|
|
344
|
+
): void {
|
|
345
|
+
const index = compiled.count;
|
|
346
|
+
compiled.count = index + 1;
|
|
347
|
+
compiled.configs[index] = config;
|
|
348
|
+
compiled.paths[index] = path;
|
|
349
|
+
compiled.parent[index] = parentIndex;
|
|
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";
|
|
354
|
+
compiled.initialChild[index] = NO_STATE;
|
|
355
|
+
compiled.restoreDepth[index] = 0;
|
|
356
|
+
compiled.levels[index] = 0;
|
|
357
|
+
compiled.records[index] = false;
|
|
358
|
+
compiled.pathIndex[path] = index;
|
|
359
|
+
const children = config.states;
|
|
360
|
+
if (children === undefined) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
let deepest = 0;
|
|
364
|
+
for (const name in children) {
|
|
365
|
+
if (name === "" || name.indexOf("/") !== -1) {
|
|
366
|
+
throw `hsm: state "${describePath(path)}" has a child named "${name}"; state names must be non-empty and contain no "/"`;
|
|
367
|
+
}
|
|
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
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (compiled.parallel[index] !== true) {
|
|
389
|
+
deepest++;
|
|
390
|
+
}
|
|
391
|
+
compiled.levels[index] = deepest;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function hasChildren<Ctx, E extends EventObject>(config: StateConfig<Ctx, E>): boolean {
|
|
395
|
+
const children = config.states;
|
|
396
|
+
if (children === undefined) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
for (const _name in children) {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function compileInitial<Ctx, E extends EventObject>(
|
|
406
|
+
compiled: Compiled<Ctx, E>,
|
|
407
|
+
index: number,
|
|
408
|
+
): void {
|
|
409
|
+
const config = compiled.configs[index] as StateConfig<Ctx, E>;
|
|
410
|
+
const path = compiled.paths[index] as string;
|
|
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
|
+
}
|
|
427
|
+
if (!hasChildren(config)) {
|
|
428
|
+
if (initial !== undefined) {
|
|
429
|
+
throw `hsm: state "${describePath(path)}" has initial "${initial}" but no child states`;
|
|
430
|
+
}
|
|
431
|
+
if (config.restoreDepth !== undefined) {
|
|
432
|
+
throw `hsm: state "${describePath(path)}" has restoreDepth but no child states`;
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
compileRestoreDepth(compiled, index);
|
|
437
|
+
if (initial === undefined) {
|
|
438
|
+
throw `hsm: compound state "${describePath(path)}" has no initial`;
|
|
439
|
+
}
|
|
440
|
+
const child = lookupPath(compiled, initial);
|
|
441
|
+
if (child === undefined || compiled.parent[child] !== index) {
|
|
442
|
+
throw `hsm: state "${describePath(path)}" has initial "${initial}", which is not one of its children`;
|
|
443
|
+
}
|
|
444
|
+
compiled.initialChild[index] = child;
|
|
445
|
+
}
|
|
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
|
+
|
|
488
|
+
function compileTransition<Ctx, E extends EventObject>(
|
|
489
|
+
compiled: Compiled<Ctx, E>,
|
|
490
|
+
source: number,
|
|
491
|
+
spec: string | TransitionConfig<Ctx, E>,
|
|
492
|
+
): Transition<Ctx, E> {
|
|
493
|
+
if (typeof spec === "string") {
|
|
494
|
+
return {
|
|
495
|
+
target: resolveTarget(compiled, source, spec),
|
|
496
|
+
when: undefined,
|
|
497
|
+
run: [],
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const run: MoveAction<Ctx, E>[] = [];
|
|
501
|
+
const declared = spec.run;
|
|
502
|
+
if (typeof declared === "function") {
|
|
503
|
+
run.push(declared);
|
|
504
|
+
} else if (declared !== undefined) {
|
|
505
|
+
for (let i = 0; i < declared.length; i++) {
|
|
506
|
+
run.push(declared[i] as MoveAction<Ctx, E>);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
target: spec.to === undefined ? NO_STATE : resolveTarget(compiled, source, spec.to),
|
|
511
|
+
when: spec.when,
|
|
512
|
+
run,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function compileOn<Ctx, E extends EventObject>(compiled: Compiled<Ctx, E>, index: number): void {
|
|
517
|
+
const table: { [type: string]: number } = {};
|
|
518
|
+
compiled.onIndex[index] = table;
|
|
519
|
+
const on = (compiled.configs[index] as StateConfig<Ctx, E>).on as
|
|
520
|
+
| { readonly [type: string]: TransitionSpec<Ctx, E> | undefined }
|
|
521
|
+
| undefined;
|
|
522
|
+
if (on === undefined) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
for (const type in on) {
|
|
526
|
+
const spec = on[type];
|
|
527
|
+
if (spec === undefined) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const list: Transition<Ctx, E>[] = [];
|
|
531
|
+
if (
|
|
532
|
+
typeof spec === "string" ||
|
|
533
|
+
(spec as readonly TransitionConfig<Ctx, E>[])[0] === undefined
|
|
534
|
+
) {
|
|
535
|
+
list.push(compileTransition(compiled, index, spec as string | TransitionConfig<Ctx, E>));
|
|
536
|
+
} else {
|
|
537
|
+
const candidates = spec as readonly TransitionConfig<Ctx, E>[];
|
|
538
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
539
|
+
list.push(compileTransition(compiled, index, candidates[i] as TransitionConfig<Ctx, E>));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
table[type] = compiled.transitions.length;
|
|
543
|
+
compiled.transitions.push(list);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
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
|
+
|
|
570
|
+
const NOT_A_DELAY = -1;
|
|
571
|
+
|
|
572
|
+
// Number-literal keys arrive as numbers in Lua and as canonical number strings in JavaScript.
|
|
573
|
+
// A string key counts only when it reads back unchanged, which rejects "" (0 in JavaScript).
|
|
574
|
+
function parseDelay(key: string | number): number {
|
|
575
|
+
if (typeof key === "number") {
|
|
576
|
+
return key;
|
|
577
|
+
}
|
|
578
|
+
let delay = NOT_A_DELAY;
|
|
579
|
+
try {
|
|
580
|
+
delay = (key as unknown as number) * 1;
|
|
581
|
+
} catch {
|
|
582
|
+
// Lua raises on arithmetic with a non-numeric string; delay keeps NOT_A_DELAY.
|
|
583
|
+
}
|
|
584
|
+
if (`${delay}` !== key) {
|
|
585
|
+
return NOT_A_DELAY;
|
|
586
|
+
}
|
|
587
|
+
return delay;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function compileAfter<Ctx, E extends EventObject>(compiled: Compiled<Ctx, E>, index: number): void {
|
|
591
|
+
const delays: number[] = [];
|
|
592
|
+
const targets: number[] = [];
|
|
593
|
+
compiled.afterDelays[index] = delays;
|
|
594
|
+
compiled.afterTargets[index] = targets;
|
|
595
|
+
const after = (compiled.configs[index] as StateConfig<Ctx, E>).after;
|
|
596
|
+
if (after === undefined) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const path = compiled.paths[index] as string;
|
|
600
|
+
for (const key in after) {
|
|
601
|
+
const delay = parseDelay(key);
|
|
602
|
+
if (!(delay >= 0)) {
|
|
603
|
+
throw `hsm: state "${describePath(path)}" has an after delay "${key}" that is not a non-negative number`;
|
|
604
|
+
}
|
|
605
|
+
const target = resolveTarget(compiled, index, after[key as unknown as number] as string);
|
|
606
|
+
let slot = delays.length;
|
|
607
|
+
while (slot > 0 && (delays[slot - 1] as number) > delay) {
|
|
608
|
+
delays[slot] = delays[slot - 1] as number;
|
|
609
|
+
targets[slot] = targets[slot - 1] as number;
|
|
610
|
+
slot--;
|
|
611
|
+
}
|
|
612
|
+
delays[slot] = delay;
|
|
613
|
+
targets[slot] = target;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function compile<Ctx, E extends EventObject>(config: MachineConfig<Ctx, E>): Compiled<Ctx, E> {
|
|
618
|
+
const compiled: Compiled<Ctx, E> = {
|
|
619
|
+
count: 0,
|
|
620
|
+
configs: [],
|
|
621
|
+
paths: [],
|
|
622
|
+
parent: [],
|
|
623
|
+
depth: [],
|
|
624
|
+
children: [],
|
|
625
|
+
parallel: [],
|
|
626
|
+
initialChild: [],
|
|
627
|
+
restoreDepth: [],
|
|
628
|
+
levels: [],
|
|
629
|
+
records: [],
|
|
630
|
+
onIndex: [],
|
|
631
|
+
transitions: [],
|
|
632
|
+
afterDelays: [],
|
|
633
|
+
afterTargets: [],
|
|
634
|
+
always: [],
|
|
635
|
+
pathIndex: {},
|
|
636
|
+
};
|
|
637
|
+
registerState(compiled, config, NO_STATE, "");
|
|
638
|
+
for (let index = 0; index < compiled.count; index++) {
|
|
639
|
+
compileInitial(compiled, index);
|
|
640
|
+
compileOn(compiled, index);
|
|
641
|
+
compileAfter(compiled, index);
|
|
642
|
+
compileAlways(compiled, index);
|
|
643
|
+
}
|
|
644
|
+
compileRecords(compiled);
|
|
645
|
+
return compiled;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function isAncestorOrSelf<Ctx, E extends EventObject>(
|
|
649
|
+
compiled: Compiled<Ctx, E>,
|
|
650
|
+
ancestor: number,
|
|
651
|
+
state: number,
|
|
652
|
+
): boolean {
|
|
653
|
+
const ancestorDepth = compiled.depth[ancestor] as number;
|
|
654
|
+
let current = state;
|
|
655
|
+
while ((compiled.depth[current] as number) > ancestorDepth) {
|
|
656
|
+
current = compiled.parent[current] as number;
|
|
657
|
+
}
|
|
658
|
+
return current === ancestor;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function transitionDomain<Ctx, E extends EventObject>(
|
|
662
|
+
compiled: Compiled<Ctx, E>,
|
|
663
|
+
source: number,
|
|
664
|
+
target: number,
|
|
665
|
+
): number {
|
|
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
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// Regions enter and exit together, so a move spanning a parallel state re-enters it.
|
|
674
|
+
while (compiled.parallel[domain] === true) {
|
|
675
|
+
domain = compiled.parent[domain] as number;
|
|
676
|
+
}
|
|
677
|
+
return domain;
|
|
678
|
+
}
|
|
679
|
+
|
|
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
|
+
}
|
|
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> {
|
|
695
|
+
function start(ctx: Ctx): MachineInstance<Ctx, E> {
|
|
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[] = [];
|
|
713
|
+
const elapsed: number[] = [];
|
|
714
|
+
const fired: number[] = [];
|
|
715
|
+
const entryId: number[] = [];
|
|
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[] = [];
|
|
722
|
+
let entryCount = 0;
|
|
723
|
+
const queue: (E | undefined)[] = [];
|
|
724
|
+
let queueHead = 0;
|
|
725
|
+
let queueTail = 0;
|
|
726
|
+
let running = true;
|
|
727
|
+
let busy = false;
|
|
728
|
+
let stopRequested = false;
|
|
729
|
+
let moved = false;
|
|
730
|
+
let listeners: MoveListener<E>[] = [];
|
|
731
|
+
|
|
732
|
+
const instance = {
|
|
733
|
+
ctx,
|
|
734
|
+
path: undefined as string | undefined,
|
|
735
|
+
leaves,
|
|
736
|
+
matches,
|
|
737
|
+
send,
|
|
738
|
+
update,
|
|
739
|
+
stop,
|
|
740
|
+
onMove,
|
|
741
|
+
};
|
|
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
|
+
|
|
914
|
+
function enterState(state: number): void {
|
|
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;
|
|
923
|
+
entryCount++;
|
|
924
|
+
const id = entryCount;
|
|
925
|
+
entryId[state] = id;
|
|
926
|
+
const config = configs[state] as StateConfig<Ctx, E>;
|
|
927
|
+
const hook = config.enter;
|
|
928
|
+
if (hook !== undefined) {
|
|
929
|
+
hook(ctx, instance);
|
|
930
|
+
}
|
|
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(
|
|
937
|
+
ctx,
|
|
938
|
+
(event: E) => {
|
|
939
|
+
const current = lookupPath(compiled, path);
|
|
940
|
+
if (
|
|
941
|
+
finished ||
|
|
942
|
+
!running ||
|
|
943
|
+
stopRequested ||
|
|
944
|
+
current === undefined ||
|
|
945
|
+
entryId[current] !== id
|
|
946
|
+
) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
finished = true;
|
|
950
|
+
send(event);
|
|
951
|
+
},
|
|
952
|
+
instance,
|
|
953
|
+
);
|
|
954
|
+
if (typeof result === "function") {
|
|
955
|
+
cleanup[state] = result;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
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;
|
|
978
|
+
}
|
|
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
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
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;
|
|
1014
|
+
}
|
|
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);
|
|
1020
|
+
}
|
|
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;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function transition(
|
|
1091
|
+
source: number,
|
|
1092
|
+
target: number,
|
|
1093
|
+
run: readonly MoveAction<Ctx, E>[] | undefined,
|
|
1094
|
+
event: E | undefined,
|
|
1095
|
+
cause: MoveCause,
|
|
1096
|
+
): void {
|
|
1097
|
+
if (target === NO_STATE) {
|
|
1098
|
+
runActions(run, event);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
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;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function runActions(
|
|
1159
|
+
run: readonly MoveAction<Ctx, E>[] | undefined,
|
|
1160
|
+
event: E | undefined,
|
|
1161
|
+
): void {
|
|
1162
|
+
if (run === undefined || event === undefined) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
for (let i = 0; i < run.length; i++) {
|
|
1166
|
+
(run[i] as MoveAction<Ctx, E>)(ctx, event, instance);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
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;
|
|
1181
|
+
}
|
|
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;
|
|
1193
|
+
for (let i = 0; i < list.length; i++) {
|
|
1194
|
+
if (offerEvent(list[i] as number, event)) {
|
|
1195
|
+
taken = true;
|
|
1196
|
+
if (stopRequested || entryId[state] !== id) {
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
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;
|
|
1208
|
+
}
|
|
1209
|
+
return takeEvent(state, event);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function clearQueue(): void {
|
|
1213
|
+
while (queueHead < queueTail) {
|
|
1214
|
+
queue[queueHead] = undefined;
|
|
1215
|
+
queueHead++;
|
|
1216
|
+
}
|
|
1217
|
+
queueHead = 0;
|
|
1218
|
+
queueTail = 0;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function stopNow(): void {
|
|
1222
|
+
running = false;
|
|
1223
|
+
busy = true;
|
|
1224
|
+
clearQueue();
|
|
1225
|
+
const from = instance.path as string;
|
|
1226
|
+
exitSubtree(ROOT);
|
|
1227
|
+
refreshLeaves();
|
|
1228
|
+
busy = false;
|
|
1229
|
+
if (listeners.length > 0) {
|
|
1230
|
+
report(from, undefined, "stop", undefined);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function endStep(): void {
|
|
1235
|
+
while (!stopRequested && queueHead < queueTail) {
|
|
1236
|
+
const event = queue[queueHead] as E;
|
|
1237
|
+
queue[queueHead] = undefined;
|
|
1238
|
+
queueHead++;
|
|
1239
|
+
moved = false;
|
|
1240
|
+
offerEvent(ROOT, event);
|
|
1241
|
+
if (moved) {
|
|
1242
|
+
settleAlways();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
clearQueue();
|
|
1246
|
+
busy = false;
|
|
1247
|
+
if (stopRequested) {
|
|
1248
|
+
stopNow();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function send(event: E): void {
|
|
1253
|
+
if (!running || stopRequested) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
queue[queueTail] = event;
|
|
1257
|
+
queueTail++;
|
|
1258
|
+
if (busy) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
busy = true;
|
|
1262
|
+
if (generation !== definition.generation) {
|
|
1263
|
+
rebind();
|
|
1264
|
+
}
|
|
1265
|
+
endStep();
|
|
1266
|
+
}
|
|
1267
|
+
|
|
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);
|
|
1274
|
+
}
|
|
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
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (regionMoved) {
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
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
|
+
}
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function fireTimers(state: number): boolean {
|
|
1324
|
+
const child = activeChild[state] as number;
|
|
1325
|
+
if (child !== NO_STATE && fireTimers(child)) {
|
|
1326
|
+
return true;
|
|
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;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function update(dt: number): void {
|
|
1345
|
+
if (!running || busy || stopRequested) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
busy = true;
|
|
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
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
endStep();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function stop(): void {
|
|
1364
|
+
if (!running) {
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
if (busy) {
|
|
1368
|
+
stopRequested = true;
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
stopNow();
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function matches(path: string): boolean {
|
|
1375
|
+
const state = lookupPath(compiled, path);
|
|
1376
|
+
return state !== undefined && state !== ROOT && isActive[state] === true;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
busy = true;
|
|
1380
|
+
enterState(ROOT);
|
|
1381
|
+
enterDefaults(ROOT, 0);
|
|
1382
|
+
refreshLeaves();
|
|
1383
|
+
settleAlways();
|
|
1384
|
+
endStep();
|
|
1385
|
+
return instance;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
return { start };
|
|
1389
|
+
}
|
|
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
|
+
|
|
1412
|
+
// Ctx and E are given explicitly and the config is inferred, so the config takes a second call.
|
|
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;
|
|
1418
|
+
}
|