ihsm 0.0.23 → 0.1.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/README.md +105 -113
- package/lib/cjs/index.d.ts +5 -1394
- package/lib/cjs/index.js +53 -764
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/internal/runtime.d.ts +293 -0
- package/lib/cjs/internal/runtime.js +1906 -0
- package/lib/cjs/internal/runtime.js.map +1 -0
- package/lib/cjs/internal/types.d.ts +348 -0
- package/lib/cjs/internal/types.js +9 -0
- package/lib/cjs/internal/types.js.map +1 -0
- package/lib/cjs/test-only.d.ts +5 -0
- package/lib/cjs/test-only.js +21 -0
- package/lib/cjs/test-only.js.map +1 -0
- package/lib/cjs/testing.d.ts +38 -91
- package/lib/cjs/testing.js +72 -38
- package/lib/cjs/testing.js.map +1 -1
- package/lib/cjs/transition-routines.d.ts +3 -0
- package/lib/cjs/transition-routines.js +11 -0
- package/lib/cjs/transition-routines.js.map +1 -0
- package/lib/cjs/types.d.ts +5 -0
- package/lib/cjs/{internal/defs.private.js → types.js} +1 -1
- package/lib/cjs/types.js.map +1 -0
- package/lib/esm/index.d.ts +5 -1394
- package/lib/esm/index.js +3 -742
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/internal/runtime.d.ts +293 -0
- package/lib/esm/internal/runtime.js +1847 -0
- package/lib/esm/internal/runtime.js.map +1 -0
- package/lib/esm/internal/types.d.ts +348 -0
- package/lib/esm/internal/types.js +6 -0
- package/lib/esm/internal/types.js.map +1 -0
- package/lib/esm/test-only.d.ts +5 -0
- package/lib/esm/test-only.js +15 -0
- package/lib/esm/test-only.js.map +1 -0
- package/lib/esm/testing.d.ts +38 -91
- package/lib/esm/testing.js +72 -38
- package/lib/esm/testing.js.map +1 -1
- package/lib/esm/transition-routines.d.ts +3 -0
- package/lib/esm/transition-routines.js +3 -0
- package/lib/esm/transition-routines.js.map +1 -0
- package/lib/esm/types.d.ts +5 -0
- package/lib/esm/types.js +2 -0
- package/lib/esm/types.js.map +1 -0
- package/package.json +22 -2
- package/lib/cjs/internal/defs.private.d.ts +0 -41
- package/lib/cjs/internal/defs.private.js.map +0 -1
- package/lib/cjs/internal/dispatch.debug.d.ts +0 -4
- package/lib/cjs/internal/dispatch.debug.js +0 -332
- package/lib/cjs/internal/dispatch.debug.js.map +0 -1
- package/lib/cjs/internal/dispatch.production.d.ts +0 -6
- package/lib/cjs/internal/dispatch.production.js +0 -241
- package/lib/cjs/internal/dispatch.production.js.map +0 -1
- package/lib/cjs/internal/dispatch.trace.d.ts +0 -4
- package/lib/cjs/internal/dispatch.trace.js +0 -418
- package/lib/cjs/internal/dispatch.trace.js.map +0 -1
- package/lib/cjs/internal/hsm.d.ts +0 -60
- package/lib/cjs/internal/hsm.js +0 -215
- package/lib/cjs/internal/hsm.js.map +0 -1
- package/lib/cjs/internal/lookup.d.ts +0 -15
- package/lib/cjs/internal/lookup.js +0 -32
- package/lib/cjs/internal/lookup.js.map +0 -1
- package/lib/cjs/internal/utils.d.ts +0 -26
- package/lib/cjs/internal/utils.js +0 -63
- package/lib/cjs/internal/utils.js.map +0 -1
- package/lib/esm/internal/defs.private.d.ts +0 -41
- package/lib/esm/internal/defs.private.js +0 -2
- package/lib/esm/internal/defs.private.js.map +0 -1
- package/lib/esm/internal/dispatch.debug.d.ts +0 -4
- package/lib/esm/internal/dispatch.debug.js +0 -328
- package/lib/esm/internal/dispatch.debug.js.map +0 -1
- package/lib/esm/internal/dispatch.production.d.ts +0 -6
- package/lib/esm/internal/dispatch.production.js +0 -237
- package/lib/esm/internal/dispatch.production.js.map +0 -1
- package/lib/esm/internal/dispatch.trace.d.ts +0 -4
- package/lib/esm/internal/dispatch.trace.js +0 -414
- package/lib/esm/internal/dispatch.trace.js.map +0 -1
- package/lib/esm/internal/hsm.d.ts +0 -60
- package/lib/esm/internal/hsm.js +0 -211
- package/lib/esm/internal/hsm.js.map +0 -1
- package/lib/esm/internal/lookup.d.ts +0 -15
- package/lib/esm/internal/lookup.js +0 -29
- package/lib/esm/internal/lookup.js.map +0 -1
- package/lib/esm/internal/utils.d.ts +0 -26
- package/lib/esm/internal/utils.js +0 -52
- package/lib/esm/internal/utils.js.map +0 -1
|
@@ -0,0 +1,1906 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.kParentLink = exports.kHandlerMachine = exports.defaultInitialize = exports.defaultTraceWriter = exports.Machine = exports.HsmObject = exports.RuntimeTransitionResolver = exports.TransitionTableError = exports.dispatchContext = exports.SelfCallDeadlockError = exports.kMachine = exports.CallTimeoutError = exports.ReservedNames = exports.StateGraph = exports.ProtocolCollisionError = exports.FatalErrorState = exports.InitializationError = exports.FatalError = exports.InitialStateError = exports.UnhandledEventError = exports.EventHandlerError = exports.TransitionError = exports.RuntimeError = exports.HsmError = exports.TopState = exports.RequestingPort = exports.Port = exports.TraceLevel = void 0;
|
|
4
|
+
exports.asError = asError;
|
|
5
|
+
exports.quoteUnknown = quoteUnknown;
|
|
6
|
+
exports.quoteError = quoteError;
|
|
7
|
+
exports.getInitialState = getInitialState;
|
|
8
|
+
exports.hasInitialState = hasInitialState;
|
|
9
|
+
exports.getTransitionKey = getTransitionKey;
|
|
10
|
+
exports.defineStateName = defineStateName;
|
|
11
|
+
exports.getStateName = getStateName;
|
|
12
|
+
exports.lookupEventHandler = lookupEventHandler;
|
|
13
|
+
exports.InitialState = InitialState;
|
|
14
|
+
exports.registerStateNames = registerStateNames;
|
|
15
|
+
exports.cacheProtocolIndex = cacheProtocolIndex;
|
|
16
|
+
exports.protocolIndexFor = protocolIndexFor;
|
|
17
|
+
exports.buildProtocolIndex = buildProtocolIndex;
|
|
18
|
+
exports.isServiceCallOptions = isServiceCallOptions;
|
|
19
|
+
exports.splitServiceArgs = splitServiceArgs;
|
|
20
|
+
exports.serviceCallWithTimeout = serviceCallWithTimeout;
|
|
21
|
+
exports.createActorHandle = createActorHandle;
|
|
22
|
+
exports.getSelfNotificationsProto = getSelfNotificationsProto;
|
|
23
|
+
exports.createSelfNotifications = createSelfNotifications;
|
|
24
|
+
exports.planTransitionClasses = planTransitionClasses;
|
|
25
|
+
exports.executeTransitionRoutine = executeTransitionRoutine;
|
|
26
|
+
exports.createTransitionTracer = createTransitionTracer;
|
|
27
|
+
exports.transitionTraceLines = transitionTraceLines;
|
|
28
|
+
exports.executePendingTransition = executePendingTransition;
|
|
29
|
+
exports.createInitTask = createInitTask;
|
|
30
|
+
exports.createNotificationTask = createNotificationTask;
|
|
31
|
+
exports.createServiceTask = createServiceTask;
|
|
32
|
+
exports.isRequestingPort = isRequestingPort;
|
|
33
|
+
exports.defaultDispatchErrorCallback = defaultDispatchErrorCallback;
|
|
34
|
+
exports.spawnActor = spawnActor;
|
|
35
|
+
exports.makeActor = makeActor;
|
|
36
|
+
exports.asParentActor = asParentActor;
|
|
37
|
+
exports.makeChildActor = makeChildActor;
|
|
38
|
+
const types_1 = require("./types");
|
|
39
|
+
//#region TraceLevel
|
|
40
|
+
var TraceLevel;
|
|
41
|
+
(function (TraceLevel) {
|
|
42
|
+
TraceLevel[TraceLevel["PRODUCTION"] = 0] = "PRODUCTION";
|
|
43
|
+
TraceLevel[TraceLevel["DEBUG"] = 1] = "DEBUG";
|
|
44
|
+
TraceLevel[TraceLevel["VERBOSE_DEBUG"] = 2] = "VERBOSE_DEBUG";
|
|
45
|
+
})(TraceLevel || (exports.TraceLevel = TraceLevel = {}));
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region utils
|
|
48
|
+
/** @internal */
|
|
49
|
+
function asError(err) {
|
|
50
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
51
|
+
}
|
|
52
|
+
/** @internal */
|
|
53
|
+
function quoteUnknown(err) {
|
|
54
|
+
return quoteError(asError(err));
|
|
55
|
+
}
|
|
56
|
+
/** @internal */
|
|
57
|
+
function quoteError(err) {
|
|
58
|
+
return `${err.name}${err.message ? `: ${err.message}` : ' with no error message'}`;
|
|
59
|
+
}
|
|
60
|
+
/** @internal */
|
|
61
|
+
function getInitialState(State) {
|
|
62
|
+
return State._initialState;
|
|
63
|
+
}
|
|
64
|
+
/** @internal */
|
|
65
|
+
function hasInitialState(State) {
|
|
66
|
+
return Object.prototype.hasOwnProperty.call(State, '_initialState');
|
|
67
|
+
}
|
|
68
|
+
/** @internal */
|
|
69
|
+
function getTransitionKey(FromState, ToState) {
|
|
70
|
+
return `${getStateName(FromState)}=>${getStateName(ToState)}`;
|
|
71
|
+
}
|
|
72
|
+
function defineStateName(state, displayName) {
|
|
73
|
+
Object.defineProperty(state, '_stateName', {
|
|
74
|
+
value: displayName,
|
|
75
|
+
writable: false,
|
|
76
|
+
configurable: false,
|
|
77
|
+
enumerable: false,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** @internal — prefers an own explicit name registered for minified browser bundles. */
|
|
81
|
+
function getStateName(state) {
|
|
82
|
+
if (Object.prototype.hasOwnProperty.call(state, '_stateName')) {
|
|
83
|
+
return state._stateName;
|
|
84
|
+
}
|
|
85
|
+
return state.name;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region ports
|
|
89
|
+
/** Production port: timers, randomness, and deferred self-notifications for one machine.
|
|
90
|
+
* @typeParam T - Root state **constructor** (`typeof DoorTop`), not the instance type. */
|
|
91
|
+
class Port {
|
|
92
|
+
actor;
|
|
93
|
+
_deferFactory;
|
|
94
|
+
_timerSeq = 0;
|
|
95
|
+
_timeoutHandles = new Map();
|
|
96
|
+
_intervalHandles = new Map();
|
|
97
|
+
/** Schedule a one-shot callback after `millis` milliseconds (platform timer). */
|
|
98
|
+
setTimeout(callback, millis) {
|
|
99
|
+
const id = ++this._timerSeq;
|
|
100
|
+
const handle = globalThis.setTimeout(() => {
|
|
101
|
+
this._timeoutHandles.delete(id);
|
|
102
|
+
callback();
|
|
103
|
+
}, Math.max(0, millis ?? 0));
|
|
104
|
+
this._timeoutHandles.set(id, handle);
|
|
105
|
+
return id;
|
|
106
|
+
}
|
|
107
|
+
/** Cancel a timer previously returned by {@link Port.setTimeout}. */
|
|
108
|
+
clearTimeout(id) {
|
|
109
|
+
if (id === undefined) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const handle = this._timeoutHandles.get(id);
|
|
113
|
+
if (handle !== undefined) {
|
|
114
|
+
globalThis.clearTimeout(handle);
|
|
115
|
+
this._timeoutHandles.delete(id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Schedule a repeating callback every `millis` milliseconds. */
|
|
119
|
+
setInterval(callback, millis) {
|
|
120
|
+
const id = ++this._timerSeq;
|
|
121
|
+
const handle = globalThis.setInterval(callback, Math.max(0, millis ?? 0));
|
|
122
|
+
this._intervalHandles.set(id, handle);
|
|
123
|
+
return id;
|
|
124
|
+
}
|
|
125
|
+
/** Cancel an interval previously returned by {@link Port.setInterval}. */
|
|
126
|
+
clearInterval(id) {
|
|
127
|
+
if (id === undefined) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const handle = this._intervalHandles.get(id);
|
|
131
|
+
if (handle !== undefined) {
|
|
132
|
+
globalThis.clearInterval(handle);
|
|
133
|
+
this._intervalHandles.delete(id);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Pseudorandom number in `[0, 1)` — delegates to `Math.random()`. */
|
|
137
|
+
random() {
|
|
138
|
+
return Math.random();
|
|
139
|
+
}
|
|
140
|
+
/** Cryptographic-quality random in `[0, 1)` when the platform provides it. */
|
|
141
|
+
cryptoRandom() {
|
|
142
|
+
const crypto = globalThis.crypto;
|
|
143
|
+
return crypto.random?.() ?? Math.random();
|
|
144
|
+
}
|
|
145
|
+
/** Generate a UUID v4 string via `crypto.randomUUID()`. */
|
|
146
|
+
randomUUID() {
|
|
147
|
+
return globalThis.crypto.randomUUID();
|
|
148
|
+
}
|
|
149
|
+
/** Fill `array` with cryptographically strong random bytes. */
|
|
150
|
+
getRandomValues(array) {
|
|
151
|
+
globalThis.crypto.getRandomValues(array);
|
|
152
|
+
return array;
|
|
153
|
+
}
|
|
154
|
+
/** @internal Wired by {@link Machine.bindPort} — do not call from application code. */
|
|
155
|
+
bindDeferredNotifications(factory) {
|
|
156
|
+
this._deferFactory = factory;
|
|
157
|
+
}
|
|
158
|
+
defer(ms) {
|
|
159
|
+
if (this._deferFactory === undefined) {
|
|
160
|
+
throw new Error('ihsm: port.defer requires actor binding — pass the port to makeActor / makeTestActor');
|
|
161
|
+
}
|
|
162
|
+
return this._deferFactory(ms);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
exports.Port = Port;
|
|
166
|
+
const kRequestingPort = Symbol('ihsm.requestingPort');
|
|
167
|
+
class RequestingPort extends Port {
|
|
168
|
+
}
|
|
169
|
+
exports.RequestingPort = RequestingPort;
|
|
170
|
+
RequestingPort[kRequestingPort] = true;
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region Errors
|
|
173
|
+
class TopState {
|
|
174
|
+
ctx;
|
|
175
|
+
hsm;
|
|
176
|
+
notify;
|
|
177
|
+
notifyNow;
|
|
178
|
+
constructor() {
|
|
179
|
+
throw new Error('Fatal error: States cannot be instantiated');
|
|
180
|
+
}
|
|
181
|
+
onExit() { }
|
|
182
|
+
onEntry() { }
|
|
183
|
+
onError(error) {
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
onUnhandled(error) {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
exports.TopState = TopState;
|
|
191
|
+
class HsmError extends Error {
|
|
192
|
+
name;
|
|
193
|
+
topStateName;
|
|
194
|
+
stateName;
|
|
195
|
+
context;
|
|
196
|
+
cause;
|
|
197
|
+
constructor(name, hsm, message, cause) {
|
|
198
|
+
super(message);
|
|
199
|
+
this.name = name;
|
|
200
|
+
this.topStateName = hsm.topStateName;
|
|
201
|
+
this.stateName = hsm.currentStateName;
|
|
202
|
+
this.context = hsm.ctx;
|
|
203
|
+
this.cause = cause;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
exports.HsmError = HsmError;
|
|
207
|
+
class RuntimeError extends HsmError {
|
|
208
|
+
eventName;
|
|
209
|
+
eventPayload;
|
|
210
|
+
constructor(errorName, hsm, message, cause) {
|
|
211
|
+
super(errorName, hsm, message, cause);
|
|
212
|
+
this.eventName = hsm.eventName;
|
|
213
|
+
this.eventPayload = hsm.eventPayload;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
exports.RuntimeError = RuntimeError;
|
|
217
|
+
class TransitionError extends RuntimeError {
|
|
218
|
+
failedStateName;
|
|
219
|
+
failedCallback;
|
|
220
|
+
fromStateName;
|
|
221
|
+
toStateName;
|
|
222
|
+
constructor(hsm, cause, failedStateName, failedCallback, fromStateName, toStateName) {
|
|
223
|
+
super('TransitionError', hsm, `${failedStateName}.${failedCallback}() has failed while executing a transition from ${fromStateName} to ${toStateName}`, cause);
|
|
224
|
+
this.failedStateName = failedStateName;
|
|
225
|
+
this.failedCallback = failedCallback;
|
|
226
|
+
this.fromStateName = fromStateName;
|
|
227
|
+
this.toStateName = toStateName;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
exports.TransitionError = TransitionError;
|
|
231
|
+
class EventHandlerError extends RuntimeError {
|
|
232
|
+
constructor(hsm, cause) {
|
|
233
|
+
super('EventHandlerError', hsm, `an error was thrown while executing event handler #${hsm.eventName} in state ${hsm.currentStateName}`, cause);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
exports.EventHandlerError = EventHandlerError;
|
|
237
|
+
class UnhandledEventError extends RuntimeError {
|
|
238
|
+
constructor(hsm) {
|
|
239
|
+
super('UnhandledEventError', hsm, `event #${hsm.eventName} was unhandled in state ${hsm.currentStateName}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
exports.UnhandledEventError = UnhandledEventError;
|
|
243
|
+
class InitialStateError extends Error {
|
|
244
|
+
targetStateName;
|
|
245
|
+
constructor(targetState) {
|
|
246
|
+
super(`State '${getStateName(Object.getPrototypeOf(targetState.prototype).constructor)}' must not have more than one initial state`);
|
|
247
|
+
this.name = 'InitialStateError';
|
|
248
|
+
this.targetStateName = getStateName(targetState);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
exports.InitialStateError = InitialStateError;
|
|
252
|
+
class FatalError extends RuntimeError {
|
|
253
|
+
constructor(hsm, cause) {
|
|
254
|
+
super('FatalError', hsm, `onError() has thrown ${quoteError(cause)}`, cause);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
exports.FatalError = FatalError;
|
|
258
|
+
class InitializationError extends HsmError {
|
|
259
|
+
failedState;
|
|
260
|
+
constructor(hsm, failedState, cause) {
|
|
261
|
+
super('InitializationError', hsm, `state ${getStateName(failedState)} has thrown ${quoteError(cause)} during initialization`, cause);
|
|
262
|
+
this.failedState = failedState;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
exports.InitializationError = InitializationError;
|
|
266
|
+
class FatalErrorState extends TopState {
|
|
267
|
+
}
|
|
268
|
+
exports.FatalErrorState = FatalErrorState;
|
|
269
|
+
defineStateName(TopState, 'TopState');
|
|
270
|
+
defineStateName(FatalErrorState, 'FatalErrorState');
|
|
271
|
+
/** @internal */
|
|
272
|
+
function lookupEventHandler(hsm, eventName) {
|
|
273
|
+
let state = hsm.currentState;
|
|
274
|
+
while (true) {
|
|
275
|
+
const prototype = state.prototype;
|
|
276
|
+
if (Object.prototype.hasOwnProperty.call(prototype, eventName)) {
|
|
277
|
+
return prototype[eventName];
|
|
278
|
+
}
|
|
279
|
+
if (state === TopState) {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
state = Object.getPrototypeOf(state);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function InitialState(TargetState) {
|
|
286
|
+
const ParentOfTargetState = Object.getPrototypeOf(TargetState.prototype).constructor;
|
|
287
|
+
if (hasInitialState(ParentOfTargetState))
|
|
288
|
+
throw new InitialStateError(TargetState);
|
|
289
|
+
Object.defineProperty(TargetState, '_isInitialState', {
|
|
290
|
+
value: true,
|
|
291
|
+
writable: false,
|
|
292
|
+
configurable: false,
|
|
293
|
+
enumerable: false,
|
|
294
|
+
});
|
|
295
|
+
Object.defineProperty(ParentOfTargetState, '_initialState', {
|
|
296
|
+
value: TargetState,
|
|
297
|
+
writable: false,
|
|
298
|
+
configurable: false,
|
|
299
|
+
enumerable: false,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function registerStateNames(exports) {
|
|
303
|
+
for (const [exportName, value] of Object.entries(exports)) {
|
|
304
|
+
if (typeof value !== 'function')
|
|
305
|
+
continue;
|
|
306
|
+
const prototype = value.prototype;
|
|
307
|
+
if (typeof prototype !== 'object' || prototype === null || !TopState.prototype.isPrototypeOf(prototype))
|
|
308
|
+
continue;
|
|
309
|
+
defineStateName(value, exportName);
|
|
310
|
+
StateGraph.forRoot(findRootState(value)).register(value);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region protocol-index
|
|
315
|
+
//#region Protocol collision errors
|
|
316
|
+
/** Thrown at construction when `Config`, state handlers, and the protocol index disagree. */
|
|
317
|
+
class ProtocolCollisionError extends Error {
|
|
318
|
+
stateClass;
|
|
319
|
+
symbol;
|
|
320
|
+
// prettier-ignore
|
|
321
|
+
constructor(message, stateClass, symbol) {
|
|
322
|
+
super(message);
|
|
323
|
+
this.stateClass = stateClass;
|
|
324
|
+
this.symbol = symbol;
|
|
325
|
+
this.name = 'ProtocolCollisionError';
|
|
326
|
+
}
|
|
327
|
+
static reservedOnState(stateClass, symbol) {
|
|
328
|
+
return new ProtocolCollisionError(`ihsm: state class "${stateClass}" defines reserved symbol "${symbol}" — rename the protocol method; reserved symbols are: ${exports.ReservedNames.join(', ')}`, stateClass, symbol);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
exports.ProtocolCollisionError = ProtocolCollisionError;
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region State graph
|
|
334
|
+
const stateGraphKey = Symbol('ihsm.stateGraph');
|
|
335
|
+
function findRootState(state) {
|
|
336
|
+
let current = state;
|
|
337
|
+
while (true) {
|
|
338
|
+
const parent = Object.getPrototypeOf(current);
|
|
339
|
+
if (parent === TopState) {
|
|
340
|
+
return current;
|
|
341
|
+
}
|
|
342
|
+
const grandparent = Object.getPrototypeOf(parent);
|
|
343
|
+
if (grandparent === TopState) {
|
|
344
|
+
return current;
|
|
345
|
+
}
|
|
346
|
+
current = parent;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function handlerBucket(handler) {
|
|
350
|
+
const source = Function.prototype.toString.call(handler);
|
|
351
|
+
if (handler.constructor.name === 'AsyncFunction' || /\basync\b/.test(source)) {
|
|
352
|
+
return 'services';
|
|
353
|
+
}
|
|
354
|
+
if (/\breturn\s+[^(;]/.test(source)) {
|
|
355
|
+
return 'services';
|
|
356
|
+
}
|
|
357
|
+
return 'notifications';
|
|
358
|
+
}
|
|
359
|
+
/** Per–root-state registry of state classes for protocol scanning. */
|
|
360
|
+
class StateGraph {
|
|
361
|
+
states = new Set();
|
|
362
|
+
register(state) {
|
|
363
|
+
this.states.add(state);
|
|
364
|
+
}
|
|
365
|
+
collect(topState) {
|
|
366
|
+
if (this.states.size > 0) {
|
|
367
|
+
return [...this.states];
|
|
368
|
+
}
|
|
369
|
+
return StateGraph.collectAlongPrototypeChain(topState);
|
|
370
|
+
}
|
|
371
|
+
static forRoot(root) {
|
|
372
|
+
const host = root;
|
|
373
|
+
let graph = host[stateGraphKey];
|
|
374
|
+
if (graph === undefined) {
|
|
375
|
+
graph = new StateGraph();
|
|
376
|
+
host[stateGraphKey] = graph;
|
|
377
|
+
}
|
|
378
|
+
return graph;
|
|
379
|
+
}
|
|
380
|
+
static collectAlongPrototypeChain(topState) {
|
|
381
|
+
const collected = new Set();
|
|
382
|
+
let current = topState;
|
|
383
|
+
while (current !== undefined && current !== TopState) {
|
|
384
|
+
collected.add(current);
|
|
385
|
+
current = Object.getPrototypeOf(current);
|
|
386
|
+
if (current === TopState)
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
return [...collected];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
exports.StateGraph = StateGraph;
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region Protocol index cache
|
|
395
|
+
const indexByRoot = new WeakMap();
|
|
396
|
+
function cacheProtocolIndex(topState, index) {
|
|
397
|
+
indexByRoot.set(topState, index);
|
|
398
|
+
return index;
|
|
399
|
+
}
|
|
400
|
+
function protocolIndexFor(topState) {
|
|
401
|
+
return indexByRoot.get(topState);
|
|
402
|
+
}
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region Protocol index
|
|
405
|
+
const reservedSet = new Set(['ctx', 'hsm', 'notify', 'notifyNow', 'onEntry', 'onExit', 'onError', 'onUnhandled']);
|
|
406
|
+
exports.ReservedNames = ['ctx', 'hsm', 'notify', 'notifyNow', 'onEntry', 'onExit', 'onError', 'onUnhandled'];
|
|
407
|
+
const lifecycleHooks = new Set(['onEntry', 'onExit', 'onError', 'onUnhandled']);
|
|
408
|
+
class ProtocolIndexImpl {
|
|
409
|
+
slots;
|
|
410
|
+
constructor(slots) {
|
|
411
|
+
this.slots = slots;
|
|
412
|
+
}
|
|
413
|
+
get(name) {
|
|
414
|
+
return this.slots.get(name);
|
|
415
|
+
}
|
|
416
|
+
*entries(kind) {
|
|
417
|
+
for (const [name, slot] of this.slots) {
|
|
418
|
+
if (kind === 'root' && (slot.bucket === 'services' || slot.bucket === 'notifications')) {
|
|
419
|
+
yield [name, slot];
|
|
420
|
+
}
|
|
421
|
+
else if (kind === 'inbound' && slot.bucket !== 'internalServices') {
|
|
422
|
+
yield [name, slot];
|
|
423
|
+
}
|
|
424
|
+
else if (kind === 'child' || kind === 'test') {
|
|
425
|
+
yield [name, slot];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/** Build a protocol index by scanning handler methods on the state graph (`async` → services, otherwise notifications). */
|
|
431
|
+
function buildProtocolIndex(topState) {
|
|
432
|
+
const states = StateGraph.forRoot(findRootState(topState)).collect(topState);
|
|
433
|
+
for (const state of states) {
|
|
434
|
+
const prototype = state.prototype;
|
|
435
|
+
for (const name of Object.getOwnPropertyNames(prototype)) {
|
|
436
|
+
if (!reservedSet.has(name) || lifecycleHooks.has(name) || name === 'constructor')
|
|
437
|
+
continue;
|
|
438
|
+
if (typeof prototype[name] === 'function') {
|
|
439
|
+
throw ProtocolCollisionError.reservedOnState(getStateName(state), name);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const slots = new Map();
|
|
444
|
+
const seen = new Set();
|
|
445
|
+
for (const state of states) {
|
|
446
|
+
const prototype = state.prototype;
|
|
447
|
+
for (const name of Object.getOwnPropertyNames(prototype)) {
|
|
448
|
+
if (reservedSet.has(name) || lifecycleHooks.has(name) || name === 'constructor')
|
|
449
|
+
continue;
|
|
450
|
+
const handler = prototype[name];
|
|
451
|
+
if (typeof handler !== 'function' || seen.has(name))
|
|
452
|
+
continue;
|
|
453
|
+
seen.add(name);
|
|
454
|
+
const bucket = handlerBucket(handler);
|
|
455
|
+
slots.set(name, { bucket, name });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return new ProtocolIndexImpl(slots);
|
|
459
|
+
}
|
|
460
|
+
//#endregion
|
|
461
|
+
//#region handles
|
|
462
|
+
/** Thrown when a service client call exceeds `{ timeoutMs }`. */
|
|
463
|
+
class CallTimeoutError extends Error {
|
|
464
|
+
method;
|
|
465
|
+
// prettier-ignore
|
|
466
|
+
constructor(method) {
|
|
467
|
+
super(`ihsm: service "${method}" timed out`);
|
|
468
|
+
this.method = method;
|
|
469
|
+
this.name = 'CallTimeoutError';
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
exports.CallTimeoutError = CallTimeoutError;
|
|
473
|
+
exports.kMachine = Symbol('ihsm.machine');
|
|
474
|
+
function isServiceCallOptions(value) {
|
|
475
|
+
if (value === null || typeof value !== 'object') {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const record = value;
|
|
479
|
+
if (!('timeoutMs' in record)) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
const timeoutMs = record.timeoutMs;
|
|
483
|
+
return timeoutMs === undefined || (typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs >= 0);
|
|
484
|
+
}
|
|
485
|
+
function splitServiceArgs(args) {
|
|
486
|
+
if (args.length === 0) {
|
|
487
|
+
return { callArgs: [], timeoutMs: undefined };
|
|
488
|
+
}
|
|
489
|
+
const last = args[args.length - 1];
|
|
490
|
+
if (!isServiceCallOptions(last)) {
|
|
491
|
+
return { callArgs: [...args], timeoutMs: undefined };
|
|
492
|
+
}
|
|
493
|
+
const timeoutMs = last.timeoutMs;
|
|
494
|
+
if (timeoutMs === undefined) {
|
|
495
|
+
return { callArgs: [...args], timeoutMs: undefined };
|
|
496
|
+
}
|
|
497
|
+
return { callArgs: args.slice(0, -1), timeoutMs };
|
|
498
|
+
}
|
|
499
|
+
function serviceCallWithTimeout(promise, method, timeoutMs) {
|
|
500
|
+
if (timeoutMs === 0) {
|
|
501
|
+
return Promise.reject(new CallTimeoutError(method));
|
|
502
|
+
}
|
|
503
|
+
return new Promise((resolve, reject) => {
|
|
504
|
+
const timer = setTimeout(() => {
|
|
505
|
+
reject(new CallTimeoutError(method));
|
|
506
|
+
}, timeoutMs);
|
|
507
|
+
promise.then(value => {
|
|
508
|
+
clearTimeout(timer);
|
|
509
|
+
resolve(value);
|
|
510
|
+
}, err => {
|
|
511
|
+
clearTimeout(timer);
|
|
512
|
+
reject(err);
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const facetProtoCache = new WeakMap();
|
|
517
|
+
/**
|
|
518
|
+
* Build (and cache) the frozen prototype for one facet of one embodiment.
|
|
519
|
+
* Delivery mode is fixed by the facet — `call` dispatches services, `notify`
|
|
520
|
+
* the default queue, `notifyNow` the priority queue — so the runtime no longer
|
|
521
|
+
* needs to infer it per handler at the call site.
|
|
522
|
+
*/
|
|
523
|
+
function getFacetProto(topState, index, kind, facet) {
|
|
524
|
+
let map = facetProtoCache.get(topState);
|
|
525
|
+
if (map === undefined) {
|
|
526
|
+
map = new Map();
|
|
527
|
+
facetProtoCache.set(topState, map);
|
|
528
|
+
}
|
|
529
|
+
const cacheKey = `${kind}:${facet}`;
|
|
530
|
+
let proto = map.get(cacheKey);
|
|
531
|
+
if (proto === undefined) {
|
|
532
|
+
const built = Object.create(null);
|
|
533
|
+
// Dispatch mode is fixed by the facet, not by the handler's signature, so
|
|
534
|
+
// every member visible to this embodiment kind is exposed on every facet.
|
|
535
|
+
// The static types (`NotifyFacet` / `CallFacet`) are the gate that decides
|
|
536
|
+
// which members are legal to reach through which facet; the runtime never
|
|
537
|
+
// needs to guess service-vs-notification from the handler's return type.
|
|
538
|
+
const queue = facet === 'notifyNow' ? 'priority' : 'default';
|
|
539
|
+
for (const [name] of index.entries(kind)) {
|
|
540
|
+
if (facet === 'call') {
|
|
541
|
+
built[name] = function (...args) {
|
|
542
|
+
const { callArgs, timeoutMs } = splitServiceArgs(args);
|
|
543
|
+
const promise = this[exports.kMachine].dispatchService(name, callArgs);
|
|
544
|
+
return timeoutMs === undefined ? promise : serviceCallWithTimeout(promise, name, timeoutMs);
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
built[name] = function (...args) {
|
|
549
|
+
this[exports.kMachine].dispatchNotification(name, args, queue);
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
proto = Object.freeze(built);
|
|
554
|
+
map.set(cacheKey, proto);
|
|
555
|
+
}
|
|
556
|
+
return proto;
|
|
557
|
+
}
|
|
558
|
+
function createFacet(machine, topState, index, kind, facet) {
|
|
559
|
+
const facetHandle = Object.create(getFacetProto(topState, index, kind, facet));
|
|
560
|
+
Object.defineProperty(facetHandle, exports.kMachine, { value: machine, enumerable: false });
|
|
561
|
+
return facetHandle;
|
|
562
|
+
}
|
|
563
|
+
/** @internal */
|
|
564
|
+
function createActorHandle(machine, topState, index, kind) {
|
|
565
|
+
// Faceted surface only — protocol members live under `notify` / `notifyNow`
|
|
566
|
+
// / `call`. There are no flat methods on the handle, so `actor.theEvent()`
|
|
567
|
+
// is a compile-time and runtime error; callers must go through a facet.
|
|
568
|
+
const handle = {};
|
|
569
|
+
Object.defineProperty(handle, exports.kMachine, { value: machine, enumerable: false });
|
|
570
|
+
if (kind === 'test') {
|
|
571
|
+
Object.defineProperty(handle, 'ctx', {
|
|
572
|
+
enumerable: true,
|
|
573
|
+
get() {
|
|
574
|
+
return machine.ctx;
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
Object.defineProperty(handle, 'notify', { value: createFacet(machine, topState, index, kind, 'notify'), enumerable: true });
|
|
579
|
+
Object.defineProperty(handle, 'notifyNow', { value: createFacet(machine, topState, index, kind, 'notifyNow'), enumerable: true });
|
|
580
|
+
Object.defineProperty(handle, 'call', { value: createFacet(machine, topState, index, kind, 'call'), enumerable: true });
|
|
581
|
+
handle.hsm = machine.actorHsmFor(kind);
|
|
582
|
+
return handle;
|
|
583
|
+
}
|
|
584
|
+
const selfProtoCache = new WeakMap();
|
|
585
|
+
/** @internal */
|
|
586
|
+
function getSelfNotificationsProto(topState, index, queue) {
|
|
587
|
+
let map = selfProtoCache.get(topState);
|
|
588
|
+
if (map === undefined) {
|
|
589
|
+
map = new Map();
|
|
590
|
+
selfProtoCache.set(topState, map);
|
|
591
|
+
}
|
|
592
|
+
let proto = map.get(queue);
|
|
593
|
+
if (proto === undefined) {
|
|
594
|
+
const built = Object.create(null);
|
|
595
|
+
// Self-send always uses notification dispatch (you cannot await a service
|
|
596
|
+
// on yourself); `SelfNotifications<C>` is the static gate for which members
|
|
597
|
+
// are reachable, so expose every member visible to the handler embodiment.
|
|
598
|
+
for (const [name] of index.entries('inbound')) {
|
|
599
|
+
built[name] = function (...args) {
|
|
600
|
+
this[exports.kMachine].dispatchNotification(name, args, queue);
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
proto = Object.freeze(built);
|
|
604
|
+
map.set(queue, proto);
|
|
605
|
+
}
|
|
606
|
+
return proto;
|
|
607
|
+
}
|
|
608
|
+
function createSelfNotifications(machine, topState, index, queue) {
|
|
609
|
+
const handle = Object.create(getSelfNotificationsProto(topState, index, queue));
|
|
610
|
+
Object.defineProperty(handle, exports.kMachine, { value: machine, enumerable: false });
|
|
611
|
+
return handle;
|
|
612
|
+
}
|
|
613
|
+
//#region dispatch-guard
|
|
614
|
+
/// <reference types="node" />
|
|
615
|
+
/** Thrown in debug builds when a service targets the machine currently dispatching. */
|
|
616
|
+
class SelfCallDeadlockError extends Error {
|
|
617
|
+
constructor() {
|
|
618
|
+
super('ihsm: awaiting a service on your own machine from inside your own dispatch deadlocks');
|
|
619
|
+
this.name = 'SelfCallDeadlockError';
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
exports.SelfCallDeadlockError = SelfCallDeadlockError;
|
|
623
|
+
/**
|
|
624
|
+
* Lazy Node AsyncLocalStorage for non-production deadlock detection.
|
|
625
|
+
* State lives in a closure — no exported mutable slot.
|
|
626
|
+
*/
|
|
627
|
+
exports.dispatchContext = (() => {
|
|
628
|
+
let storage = undefined;
|
|
629
|
+
function get() {
|
|
630
|
+
if (storage !== undefined) {
|
|
631
|
+
return storage ?? undefined;
|
|
632
|
+
}
|
|
633
|
+
if (typeof process === 'undefined' || process.versions?.node === undefined) {
|
|
634
|
+
storage = null;
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
// Dynamic require — no unconditional `node:async_hooks` import (browser-safe bundle).
|
|
639
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
640
|
+
const hooks = require('node:async_hooks');
|
|
641
|
+
storage = new hooks.AsyncLocalStorage();
|
|
642
|
+
return storage;
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
storage = null;
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function resetInit() {
|
|
650
|
+
storage = undefined;
|
|
651
|
+
}
|
|
652
|
+
function markUnavailable() {
|
|
653
|
+
storage = null;
|
|
654
|
+
}
|
|
655
|
+
return { get, resetInit, markUnavailable };
|
|
656
|
+
})();
|
|
657
|
+
//#region transition-routines
|
|
658
|
+
/** Thrown when a generated transition table's graph hash does not match the scanned hierarchy. */
|
|
659
|
+
class TransitionTableError extends Error {
|
|
660
|
+
constructor(message) {
|
|
661
|
+
super(message);
|
|
662
|
+
this.name = 'TransitionTableError';
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
exports.TransitionTableError = TransitionTableError;
|
|
666
|
+
/** Compute the LCA transition path (same algorithm as `dispatch.production.ts`). */
|
|
667
|
+
function planTransitionClasses(srcState, destState) {
|
|
668
|
+
const src = srcState;
|
|
669
|
+
let dst = destState;
|
|
670
|
+
let srcPath = [];
|
|
671
|
+
const end = TopState;
|
|
672
|
+
const srcIndex = new Map();
|
|
673
|
+
const dstPath = [];
|
|
674
|
+
let cur = src;
|
|
675
|
+
let i = 0;
|
|
676
|
+
while (cur !== end) {
|
|
677
|
+
srcPath.push(cur);
|
|
678
|
+
srcIndex.set(cur, i);
|
|
679
|
+
cur = Object.getPrototypeOf(cur);
|
|
680
|
+
++i;
|
|
681
|
+
}
|
|
682
|
+
cur = dst;
|
|
683
|
+
while (cur !== end) {
|
|
684
|
+
const index = srcIndex.get(cur);
|
|
685
|
+
if (index !== undefined) {
|
|
686
|
+
srcPath = srcPath.slice(0, index);
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
dstPath.unshift(cur);
|
|
690
|
+
cur = Object.getPrototypeOf(cur);
|
|
691
|
+
}
|
|
692
|
+
while (hasInitialState(dst)) {
|
|
693
|
+
dst = getInitialState(dst);
|
|
694
|
+
dstPath.push(dst);
|
|
695
|
+
}
|
|
696
|
+
let finalState;
|
|
697
|
+
if (dstPath.length !== 0) {
|
|
698
|
+
finalState = dstPath[dstPath.length - 1];
|
|
699
|
+
}
|
|
700
|
+
else if (srcPath.length !== 0) {
|
|
701
|
+
finalState = Object.getPrototypeOf(srcPath[srcPath.length - 1]);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
finalState = undefined;
|
|
705
|
+
}
|
|
706
|
+
return { exit: srcPath, entry: dstPath, finalState };
|
|
707
|
+
}
|
|
708
|
+
async function invokeLifecycleHook(hsm, instance, state, hook, fromStateName, toStateName, style, tracer) {
|
|
709
|
+
const statePrototype = state.prototype;
|
|
710
|
+
const stateName = getStateName(state);
|
|
711
|
+
const hasHook = Object.prototype.hasOwnProperty.call(statePrototype, hook);
|
|
712
|
+
if ((style === 'verbose' || style === 'debug') && !hasHook) {
|
|
713
|
+
if (style === 'verbose') {
|
|
714
|
+
tracer?.traceHookSkipped(stateName, hook);
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
const res = statePrototype[hook].call(instance);
|
|
720
|
+
if (res) {
|
|
721
|
+
await res;
|
|
722
|
+
}
|
|
723
|
+
if (style === 'verbose') {
|
|
724
|
+
tracer?.traceHookDone(stateName, hook);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
catch (cause) {
|
|
728
|
+
if (style === 'verbose') {
|
|
729
|
+
tracer?.traceHookError(stateName, hook, cause);
|
|
730
|
+
}
|
|
731
|
+
throw new TransitionError(hsm, asError(cause), stateName, hook, fromStateName, toStateName);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Execute a planned transition path with production or verbose semantics.
|
|
736
|
+
*
|
|
737
|
+
* Used by the runtime dispatch layer, generated transition tables (`@ihsm/tools`), and oracle tests.
|
|
738
|
+
*/
|
|
739
|
+
async function executeTransitionRoutine(hsm, instance, plan, srcState, dstState, options = {}) {
|
|
740
|
+
const style = options.style ?? 'production';
|
|
741
|
+
const tracer = options.tracer;
|
|
742
|
+
const fromStateName = getStateName(srcState);
|
|
743
|
+
const toStateName = getStateName(dstState);
|
|
744
|
+
if (style === 'verbose' || style === 'debug') {
|
|
745
|
+
tracer?.traceTransitionStart(fromStateName, toStateName);
|
|
746
|
+
}
|
|
747
|
+
for (const state of plan.exit) {
|
|
748
|
+
await invokeLifecycleHook(hsm, instance, state, 'onExit', fromStateName, toStateName, style, tracer);
|
|
749
|
+
}
|
|
750
|
+
for (const state of plan.entry) {
|
|
751
|
+
await invokeLifecycleHook(hsm, instance, state, 'onEntry', fromStateName, toStateName, style, tracer);
|
|
752
|
+
}
|
|
753
|
+
const applyState = (next) => {
|
|
754
|
+
if (options.setCurrentState) {
|
|
755
|
+
options.setCurrentState(next);
|
|
756
|
+
}
|
|
757
|
+
else if ('currentState' in hsm) {
|
|
758
|
+
hsm.currentState = next;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
if (style === 'verbose') {
|
|
762
|
+
const finalState = plan.entry.length !== 0 ? plan.entry[plan.entry.length - 1] : plan.exit.length !== 0 ? Object.getPrototypeOf(plan.exit[plan.exit.length - 1]) : srcState;
|
|
763
|
+
tracer?.traceTransitionDone(getStateName(finalState));
|
|
764
|
+
applyState(finalState);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (style === 'debug' && plan.finalState) {
|
|
768
|
+
tracer?.traceTransitionDone(getStateName(plan.finalState));
|
|
769
|
+
applyState(plan.finalState);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (plan.finalState) {
|
|
773
|
+
applyState(plan.finalState);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function createTransitionTracer(hsm) {
|
|
777
|
+
return {
|
|
778
|
+
traceTransitionStart(fromStateName, toStateName) {
|
|
779
|
+
hsm._tracePush(`transition from ${fromStateName} to ${toStateName}`, `started transition from ${fromStateName} to ${toStateName} `);
|
|
780
|
+
},
|
|
781
|
+
traceHookDone(stateName, hook) {
|
|
782
|
+
hsm._traceWrite(`${stateName}.${hook}() done`);
|
|
783
|
+
},
|
|
784
|
+
traceHookSkipped(stateName, hook) {
|
|
785
|
+
hsm._traceWrite(`${stateName}.${hook}() skipped: default empty implementation`);
|
|
786
|
+
},
|
|
787
|
+
traceHookError(stateName, hook, cause) {
|
|
788
|
+
hsm._tracePopError(`${stateName}.${hook}() has thrown ${quoteUnknown(cause)}`);
|
|
789
|
+
},
|
|
790
|
+
traceTransitionDone(finalStateName) {
|
|
791
|
+
hsm._tracePopDone(`final state is ${finalStateName}`);
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
/** Collect canonical transition trace lines (for oracle comparison). */
|
|
796
|
+
function transitionTraceLines(lines) {
|
|
797
|
+
return lines
|
|
798
|
+
.map(line => {
|
|
799
|
+
const idx = line.indexOf(': ');
|
|
800
|
+
return idx >= 0 ? line.slice(idx + 2) : line;
|
|
801
|
+
})
|
|
802
|
+
.filter(line => line.startsWith('started transition from ') || line.endsWith('.onExit() done') || line.endsWith('.onEntry() done') || line.includes('.onExit() skipped:') || line.includes('.onEntry() skipped:') || line.includes('.onExit() has thrown') || line.includes('.onEntry() has thrown') || line.startsWith('done: final state is ') || (line.startsWith('failure: ') && line.includes('.onExit() has thrown')) || (line.startsWith('failure: ') && line.includes('.onEntry() has thrown')));
|
|
803
|
+
}
|
|
804
|
+
//#region actor-dispatch
|
|
805
|
+
class RuntimeTransitionRoutine {
|
|
806
|
+
plan;
|
|
807
|
+
constructor(plan) {
|
|
808
|
+
this.plan = plan;
|
|
809
|
+
}
|
|
810
|
+
async execute(hsm, srcState, dstState) {
|
|
811
|
+
const style = hsm.traceLevel === TraceLevel.PRODUCTION ? 'production' : hsm.traceLevel === TraceLevel.DEBUG ? 'debug' : 'verbose';
|
|
812
|
+
await executeTransitionRoutine(hsm, hsm._instance, this.plan, srcState, dstState, {
|
|
813
|
+
style,
|
|
814
|
+
...(style !== 'production' ? { tracer: createTransitionTracer(hsm) } : {}),
|
|
815
|
+
setCurrentState: state => {
|
|
816
|
+
hsm.currentState = state;
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
class RuntimeTransitionResolver {
|
|
822
|
+
cache = new Map();
|
|
823
|
+
hasCached(src, dest) {
|
|
824
|
+
return this.cache.has(getTransitionKey(src, dest));
|
|
825
|
+
}
|
|
826
|
+
resolve(src, dest) {
|
|
827
|
+
const key = getTransitionKey(src, dest);
|
|
828
|
+
let routine = this.cache.get(key);
|
|
829
|
+
if (routine === undefined) {
|
|
830
|
+
routine = new RuntimeTransitionRoutine(planTransitionClasses(src, dest));
|
|
831
|
+
this.cache.set(key, routine);
|
|
832
|
+
}
|
|
833
|
+
return routine;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
exports.RuntimeTransitionResolver = RuntimeTransitionResolver;
|
|
837
|
+
/** @internal */
|
|
838
|
+
async function executePendingTransition(host, resolver) {
|
|
839
|
+
if (host._transitionState === undefined) {
|
|
840
|
+
if (host.traceLevel === TraceLevel.VERBOSE_DEBUG) {
|
|
841
|
+
host._traceWrite('no transition requested');
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
const srcState = host.currentState;
|
|
847
|
+
const destState = host._transitionState;
|
|
848
|
+
if (host.traceLevel === TraceLevel.VERBOSE_DEBUG) {
|
|
849
|
+
host._traceWrite(`requested transition from ${getStateName(srcState)} to ${getStateName(destState)} `);
|
|
850
|
+
const runtimeResolver = resolver;
|
|
851
|
+
if (runtimeResolver.hasCached(srcState, destState)) {
|
|
852
|
+
host._traceWrite(`transition cache hit for ${getStateName(srcState)} to ${getStateName(destState)} `);
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
host._traceWrite(`transition cache miss for ${getStateName(srcState)} to ${getStateName(destState)} `);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
await resolver.resolve(srcState, destState).execute(host, srcState, destState);
|
|
860
|
+
}
|
|
861
|
+
catch (transitionError) {
|
|
862
|
+
host.currentState = FatalErrorState;
|
|
863
|
+
throw transitionError;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
finally {
|
|
867
|
+
host._transitionState = undefined;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
//#region Service / notification dispatch tasks
|
|
871
|
+
async function completePendingTransitions(host, resolver, onComplete) {
|
|
872
|
+
await executePendingTransition(host, resolver);
|
|
873
|
+
onComplete();
|
|
874
|
+
}
|
|
875
|
+
async function doError(host, resolver, err, onComplete) {
|
|
876
|
+
host._transitionState = undefined;
|
|
877
|
+
const messageHandler = host.currentState.prototype.onError;
|
|
878
|
+
try {
|
|
879
|
+
const result = messageHandler.call(host._instance, new EventHandlerError(host, err));
|
|
880
|
+
if (result) {
|
|
881
|
+
await result;
|
|
882
|
+
}
|
|
883
|
+
await completePendingTransitions(host, resolver, onComplete);
|
|
884
|
+
}
|
|
885
|
+
catch (recoveryErr) {
|
|
886
|
+
if (recoveryErr instanceof TransitionError) {
|
|
887
|
+
throw new FatalError(host, recoveryErr);
|
|
888
|
+
}
|
|
889
|
+
const recoveryError = asError(recoveryErr);
|
|
890
|
+
host.transition(FatalErrorState);
|
|
891
|
+
await completePendingTransitions(host, resolver, onComplete);
|
|
892
|
+
throw new FatalError(host, recoveryError);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
async function doUnhandledEvent(host, resolver, error, onComplete) {
|
|
896
|
+
try {
|
|
897
|
+
const result = host.currentState.prototype.onUnhandled.call(host._instance, error);
|
|
898
|
+
if (result) {
|
|
899
|
+
await result;
|
|
900
|
+
}
|
|
901
|
+
await completePendingTransitions(host, resolver, onComplete);
|
|
902
|
+
}
|
|
903
|
+
catch (recoveryErr) {
|
|
904
|
+
if (recoveryErr instanceof TransitionError) {
|
|
905
|
+
host.currentState = FatalErrorState;
|
|
906
|
+
throw recoveryErr;
|
|
907
|
+
}
|
|
908
|
+
await doError(host, resolver, asError(recoveryErr), onComplete);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async function invokeHandler(host, resolver, name, args, options = {}) {
|
|
912
|
+
const recover = options.recover ?? false;
|
|
913
|
+
const finishEvent = () => {
|
|
914
|
+
host._currentEventName = undefined;
|
|
915
|
+
host._currentEventPayload = undefined;
|
|
916
|
+
};
|
|
917
|
+
host._currentEventName = name;
|
|
918
|
+
host._currentEventPayload = [...args];
|
|
919
|
+
try {
|
|
920
|
+
const eventHandler = lookupEventHandler(host, name);
|
|
921
|
+
if (!eventHandler) {
|
|
922
|
+
await doUnhandledEvent(host, resolver, new UnhandledEventError(host), finishEvent);
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
const result = eventHandler.call(host._instance, ...args);
|
|
927
|
+
const settled = result instanceof Promise ? await result : result;
|
|
928
|
+
await completePendingTransitions(host, resolver, finishEvent);
|
|
929
|
+
return settled;
|
|
930
|
+
}
|
|
931
|
+
catch (recoveryErr) {
|
|
932
|
+
if (recoveryErr instanceof UnhandledEventError) {
|
|
933
|
+
await doUnhandledEvent(host, resolver, recoveryErr, finishEvent);
|
|
934
|
+
return undefined;
|
|
935
|
+
}
|
|
936
|
+
if (recoveryErr instanceof TransitionError) {
|
|
937
|
+
finishEvent();
|
|
938
|
+
throw recoveryErr;
|
|
939
|
+
}
|
|
940
|
+
if (recover) {
|
|
941
|
+
await doError(host, resolver, asError(recoveryErr), finishEvent);
|
|
942
|
+
return undefined;
|
|
943
|
+
}
|
|
944
|
+
finishEvent();
|
|
945
|
+
throw asError(recoveryErr);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
catch (err) {
|
|
949
|
+
finishEvent();
|
|
950
|
+
throw err;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async function executeInitProduction(hsm) {
|
|
954
|
+
let currState = hsm.topState;
|
|
955
|
+
try {
|
|
956
|
+
while (true) {
|
|
957
|
+
const proto = currState.prototype;
|
|
958
|
+
if (proto.hasOwnProperty('onEntry')) {
|
|
959
|
+
proto.onEntry.call(hsm._instance);
|
|
960
|
+
}
|
|
961
|
+
if (hasInitialState(currState)) {
|
|
962
|
+
currState = getInitialState(currState);
|
|
963
|
+
}
|
|
964
|
+
else
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
hsm.currentState = currState;
|
|
968
|
+
}
|
|
969
|
+
catch (cause) {
|
|
970
|
+
if (cause instanceof TransitionError) {
|
|
971
|
+
throw cause;
|
|
972
|
+
}
|
|
973
|
+
hsm.currentState = FatalErrorState;
|
|
974
|
+
throw new InitializationError(hsm, currState, asError(cause));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async function executeInitDebug(hsm) {
|
|
978
|
+
hsm._traceWrite('begin initialization');
|
|
979
|
+
try {
|
|
980
|
+
let currState = hsm.topState;
|
|
981
|
+
hsm._tracePush(`initialize`, `started initialization from ${getStateName(hsm.topState)}`);
|
|
982
|
+
try {
|
|
983
|
+
while (true) {
|
|
984
|
+
if (Object.prototype.hasOwnProperty.call(currState.prototype, 'onEntry')) {
|
|
985
|
+
currState.prototype['onEntry'].call(hsm._instance);
|
|
986
|
+
}
|
|
987
|
+
if (hasInitialState(currState)) {
|
|
988
|
+
currState = getInitialState(currState);
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
hsm._tracePopDone(`final state is ${getStateName(currState)}`);
|
|
995
|
+
hsm.currentState = currState;
|
|
996
|
+
}
|
|
997
|
+
catch (cause) {
|
|
998
|
+
if (cause instanceof TransitionError) {
|
|
999
|
+
throw cause;
|
|
1000
|
+
}
|
|
1001
|
+
hsm._tracePopError(`initialization failed from top state '${getStateName(hsm.topState)}' as ${getStateName(currState)}.onEntry() handler has raised ${quoteUnknown(cause)}; final state is ${getStateName(FatalErrorState)}`);
|
|
1002
|
+
hsm.currentState = FatalErrorState;
|
|
1003
|
+
throw new InitializationError(hsm, currState, asError(cause));
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
finally {
|
|
1007
|
+
hsm._traceWrite('end initialization');
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async function executeInitVerbose(hsm) {
|
|
1011
|
+
hsm._traceWrite('begin initialization');
|
|
1012
|
+
try {
|
|
1013
|
+
let currState = hsm.topState;
|
|
1014
|
+
hsm._tracePush(`initialize`, `started initialization from ${getStateName(hsm.topState)}`);
|
|
1015
|
+
try {
|
|
1016
|
+
while (true) {
|
|
1017
|
+
if (Object.prototype.hasOwnProperty.call(currState.prototype, 'onEntry')) {
|
|
1018
|
+
currState.prototype['onEntry'].call(hsm._instance);
|
|
1019
|
+
hsm._traceWrite(`${getStateName(currState)}.onEntry() done`);
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
hsm._traceWrite(`skip ${getStateName(currState)}.onEntry(): default empty implementation`);
|
|
1023
|
+
}
|
|
1024
|
+
if (hasInitialState(currState)) {
|
|
1025
|
+
const newInitialState = getInitialState(currState);
|
|
1026
|
+
hsm._traceWrite(`${getStateName(currState)} initial state is ${getStateName(newInitialState)}`);
|
|
1027
|
+
currState = newInitialState;
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
hsm._traceWrite(`${getStateName(currState)} has no initial state; final state is ${getStateName(currState)}`);
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
hsm._tracePopDone(`final state is ${getStateName(currState)}`);
|
|
1035
|
+
hsm.currentState = currState;
|
|
1036
|
+
}
|
|
1037
|
+
catch (cause) {
|
|
1038
|
+
if (cause instanceof TransitionError) {
|
|
1039
|
+
throw cause;
|
|
1040
|
+
}
|
|
1041
|
+
hsm._tracePopError(`initialization failed from top state '${getStateName(hsm.topState)}' as ${getStateName(currState)}.onEntry() handler has raised ${quoteUnknown(cause)}; final state is ${getStateName(FatalErrorState)}`);
|
|
1042
|
+
hsm.currentState = FatalErrorState;
|
|
1043
|
+
throw new InitializationError(hsm, currState, asError(cause));
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
finally {
|
|
1047
|
+
hsm._traceWrite('end initialization');
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async function dispatchEventProduction(hsm, resolver, eventName, ...eventPayload) {
|
|
1051
|
+
await invokeHandler(hsm, resolver, eventName, eventPayload, { recover: true });
|
|
1052
|
+
}
|
|
1053
|
+
function debugFinishEventDispatch(hsm) {
|
|
1054
|
+
hsm._traceWrite(`end event dispatch`);
|
|
1055
|
+
hsm._currentEventName = undefined;
|
|
1056
|
+
hsm._currentEventPayload = undefined;
|
|
1057
|
+
}
|
|
1058
|
+
async function debugDoError(hsm, resolver, err, onComplete) {
|
|
1059
|
+
hsm._transitionState = undefined;
|
|
1060
|
+
hsm._tracePush(`error recovery`, `started error recovery`);
|
|
1061
|
+
try {
|
|
1062
|
+
hsm._tracePush('execute', 'started #onError handler execution');
|
|
1063
|
+
const result = hsm.currentState.prototype.onError.call(hsm._instance, new EventHandlerError(hsm, err));
|
|
1064
|
+
if (result) {
|
|
1065
|
+
await result;
|
|
1066
|
+
}
|
|
1067
|
+
hsm._tracePopDone('error handler execution successful');
|
|
1068
|
+
await completePendingTransitions(hsm, resolver, () => {
|
|
1069
|
+
hsm._tracePopDone('error recovery successful');
|
|
1070
|
+
onComplete();
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
catch (recoveryErr) {
|
|
1074
|
+
hsm._tracePopError(`error handler execution failure: ${quoteUnknown(recoveryErr)}`);
|
|
1075
|
+
if (recoveryErr instanceof TransitionError) {
|
|
1076
|
+
hsm._tracePopError(`error recovery failure: ${quoteUnknown(recoveryErr)}`);
|
|
1077
|
+
throw new FatalError(hsm, recoveryErr);
|
|
1078
|
+
}
|
|
1079
|
+
const recoveryError = asError(recoveryErr);
|
|
1080
|
+
hsm.transition(FatalErrorState);
|
|
1081
|
+
await completePendingTransitions(hsm, resolver, () => {
|
|
1082
|
+
hsm._tracePopError(`error recovery failure: ${quoteUnknown(recoveryError)}`);
|
|
1083
|
+
onComplete();
|
|
1084
|
+
});
|
|
1085
|
+
throw new FatalError(hsm, recoveryError);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
async function debugDoUnhandledEvent(hsm, resolver, error, onComplete) {
|
|
1089
|
+
hsm._tracePush('unhandled recovery', `started unhandled event recovery`);
|
|
1090
|
+
try {
|
|
1091
|
+
hsm._tracePush('execute', 'started #onUnhandled handler execution');
|
|
1092
|
+
const result = hsm.currentState.prototype.onUnhandled.call(hsm._instance, error);
|
|
1093
|
+
if (result) {
|
|
1094
|
+
await result;
|
|
1095
|
+
}
|
|
1096
|
+
hsm._tracePopDone('unhandled handler execution successful');
|
|
1097
|
+
await completePendingTransitions(hsm, resolver, () => {
|
|
1098
|
+
hsm._tracePopDone('unhandled event recovery successful');
|
|
1099
|
+
onComplete();
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
catch (recoveryErr) {
|
|
1103
|
+
hsm._tracePopError(`unhandled event recovery failure: ${quoteUnknown(recoveryErr)}`);
|
|
1104
|
+
if (recoveryErr instanceof TransitionError) {
|
|
1105
|
+
hsm.currentState = FatalErrorState;
|
|
1106
|
+
hsm._tracePopError(`unhandled event recovery failure: ${quoteUnknown(recoveryErr)}`);
|
|
1107
|
+
throw recoveryErr;
|
|
1108
|
+
}
|
|
1109
|
+
try {
|
|
1110
|
+
await debugDoError(hsm, resolver, asError(recoveryErr), () => {
|
|
1111
|
+
hsm._tracePopDone('unhandled event recovery successful');
|
|
1112
|
+
onComplete();
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
catch (nestedErr) {
|
|
1116
|
+
hsm._tracePopError(`unhandled event recovery failure: ${quoteUnknown(nestedErr)}`);
|
|
1117
|
+
throw nestedErr;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
async function dispatchEventDebug(hsm, resolver, eventName, ...eventPayload) {
|
|
1122
|
+
const eventLabel = String(eventName);
|
|
1123
|
+
hsm._traceWrite(`begin event dispatch of #${eventLabel}`);
|
|
1124
|
+
hsm._tracePush(`#${eventLabel}`, `started event dispatch`);
|
|
1125
|
+
hsm._currentEventName = eventLabel;
|
|
1126
|
+
hsm._currentEventPayload = eventPayload;
|
|
1127
|
+
try {
|
|
1128
|
+
const eventHandler = lookupEventHandler(hsm, eventName);
|
|
1129
|
+
if (!eventHandler) {
|
|
1130
|
+
try {
|
|
1131
|
+
await debugDoUnhandledEvent(hsm, resolver, new UnhandledEventError(hsm), () => {
|
|
1132
|
+
hsm._tracePopDone('event dispatch successful');
|
|
1133
|
+
debugFinishEventDispatch(hsm);
|
|
1134
|
+
});
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
catch (recoveryErr) {
|
|
1138
|
+
hsm._tracePopError(`event dispatch failed: ${quoteUnknown(recoveryErr)}`);
|
|
1139
|
+
debugFinishEventDispatch(hsm);
|
|
1140
|
+
throw recoveryErr;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
try {
|
|
1144
|
+
hsm._tracePush('execute', 'started event handler execution');
|
|
1145
|
+
const result = eventHandler.call(hsm._instance, ...eventPayload);
|
|
1146
|
+
if (result) {
|
|
1147
|
+
await result;
|
|
1148
|
+
}
|
|
1149
|
+
hsm._tracePopDone('event handler execution successful');
|
|
1150
|
+
await completePendingTransitions(hsm, resolver, () => {
|
|
1151
|
+
hsm._tracePopDone(`event dispatch successful`);
|
|
1152
|
+
debugFinishEventDispatch(hsm);
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
catch (recoveryErr) {
|
|
1156
|
+
hsm._tracePopError(quoteUnknown(recoveryErr));
|
|
1157
|
+
if (recoveryErr instanceof UnhandledEventError) {
|
|
1158
|
+
try {
|
|
1159
|
+
await debugDoUnhandledEvent(hsm, resolver, recoveryErr, () => {
|
|
1160
|
+
hsm._tracePopDone('event dispatch successful');
|
|
1161
|
+
debugFinishEventDispatch(hsm);
|
|
1162
|
+
});
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
catch (nestedErr) {
|
|
1166
|
+
hsm._tracePopError(`event dispatch failed: ${quoteUnknown(nestedErr)}`);
|
|
1167
|
+
debugFinishEventDispatch(hsm);
|
|
1168
|
+
throw nestedErr;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
else if (recoveryErr instanceof TransitionError) {
|
|
1172
|
+
hsm._tracePopError(`event dispatch failed: ${quoteUnknown(recoveryErr)}`);
|
|
1173
|
+
debugFinishEventDispatch(hsm);
|
|
1174
|
+
throw recoveryErr;
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
try {
|
|
1178
|
+
await debugDoError(hsm, resolver, asError(recoveryErr), () => {
|
|
1179
|
+
hsm._tracePopDone('event dispatch successful');
|
|
1180
|
+
debugFinishEventDispatch(hsm);
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
catch (nestedErr) {
|
|
1184
|
+
hsm._tracePopError(`event dispatch failed: ${quoteUnknown(nestedErr)}`);
|
|
1185
|
+
debugFinishEventDispatch(hsm);
|
|
1186
|
+
throw nestedErr;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
catch (err) {
|
|
1192
|
+
debugFinishEventDispatch(hsm);
|
|
1193
|
+
throw err;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
function verboseFinishEventDispatch(hsm) {
|
|
1197
|
+
hsm._traceWrite(`end event dispatch`);
|
|
1198
|
+
hsm._currentEventName = undefined;
|
|
1199
|
+
hsm._currentEventPayload = undefined;
|
|
1200
|
+
}
|
|
1201
|
+
async function verboseDoError(hsm, resolver, err, onComplete) {
|
|
1202
|
+
hsm._transitionState = undefined;
|
|
1203
|
+
hsm._tracePush(`error recovery`, `started error recovery`);
|
|
1204
|
+
hsm._tracePush(`lookup`, `started lookup of #onError event handler`);
|
|
1205
|
+
let errorLookupState = hsm.currentState;
|
|
1206
|
+
let messageHandler;
|
|
1207
|
+
while (errorLookupState != TopState) {
|
|
1208
|
+
const errorPrototype = errorLookupState.prototype;
|
|
1209
|
+
if (Object.prototype.hasOwnProperty.call(errorPrototype, 'onError')) {
|
|
1210
|
+
hsm._tracePopDone(`found in state ${getStateName(errorLookupState)}`);
|
|
1211
|
+
messageHandler = errorPrototype['onError'];
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
hsm._traceWrite(`not found in state ${getStateName(errorLookupState)}`);
|
|
1215
|
+
errorLookupState = Object.getPrototypeOf(errorLookupState);
|
|
1216
|
+
}
|
|
1217
|
+
if (messageHandler === undefined) {
|
|
1218
|
+
hsm._tracePopDone(`found in state ${getStateName(TopState)}`);
|
|
1219
|
+
messageHandler = TopState.prototype.onError;
|
|
1220
|
+
}
|
|
1221
|
+
try {
|
|
1222
|
+
hsm._tracePush('execute', 'started #onError handler execution');
|
|
1223
|
+
const result = messageHandler.call(hsm._instance, new EventHandlerError(hsm, err));
|
|
1224
|
+
if (result) {
|
|
1225
|
+
await result;
|
|
1226
|
+
}
|
|
1227
|
+
hsm._tracePopDone('error handler execution successful');
|
|
1228
|
+
await completePendingTransitions(hsm, resolver, () => {
|
|
1229
|
+
hsm._tracePopDone('error recovery successful');
|
|
1230
|
+
onComplete();
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
catch (recoveryErr) {
|
|
1234
|
+
hsm._tracePopError(`error handler execution failure: ${quoteUnknown(recoveryErr)}`);
|
|
1235
|
+
if (recoveryErr instanceof TransitionError) {
|
|
1236
|
+
hsm._tracePopError(`error recovery failure: ${quoteUnknown(recoveryErr)}`);
|
|
1237
|
+
throw recoveryErr;
|
|
1238
|
+
}
|
|
1239
|
+
const recoveryError = asError(recoveryErr);
|
|
1240
|
+
hsm.transition(FatalErrorState);
|
|
1241
|
+
await completePendingTransitions(hsm, resolver, () => {
|
|
1242
|
+
hsm._tracePopError(`error recovery failure: ${quoteUnknown(recoveryError)}`);
|
|
1243
|
+
onComplete();
|
|
1244
|
+
});
|
|
1245
|
+
throw new FatalError(hsm, recoveryError);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
async function verboseDoUnhandledEvent(hsm, resolver, error, onComplete) {
|
|
1249
|
+
hsm._tracePush('unhandled recovery', `started unhandled event recovery`);
|
|
1250
|
+
let unhandledLookupState = hsm.currentState;
|
|
1251
|
+
hsm._tracePush(`lookup`, `started lookup of #onUnhandled event handler`);
|
|
1252
|
+
let messageHandler;
|
|
1253
|
+
while (true) {
|
|
1254
|
+
const unhandledPrototype = unhandledLookupState.prototype;
|
|
1255
|
+
if (Object.prototype.hasOwnProperty.call(unhandledPrototype, 'onUnhandled')) {
|
|
1256
|
+
hsm._tracePopDone(`found in state ${getStateName(unhandledLookupState)}`);
|
|
1257
|
+
messageHandler = unhandledPrototype.onUnhandled;
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
hsm._traceWrite(`not found in state ${getStateName(unhandledLookupState)}`);
|
|
1261
|
+
unhandledLookupState = Object.getPrototypeOf(unhandledLookupState);
|
|
1262
|
+
if (unhandledLookupState == TopState) {
|
|
1263
|
+
hsm._tracePopDone(`found in state ${getStateName(unhandledLookupState)}`);
|
|
1264
|
+
messageHandler = unhandledPrototype.onUnhandled;
|
|
1265
|
+
break;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
try {
|
|
1269
|
+
hsm._tracePush('execute', 'started #onUnhandled handler execution');
|
|
1270
|
+
const result = messageHandler.call(hsm._instance, error);
|
|
1271
|
+
if (result) {
|
|
1272
|
+
await result;
|
|
1273
|
+
}
|
|
1274
|
+
hsm._tracePopDone('unhandled handler execution successful');
|
|
1275
|
+
await completePendingTransitions(hsm, resolver, () => {
|
|
1276
|
+
hsm._tracePopDone('unhandled event recovery successful');
|
|
1277
|
+
onComplete();
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
catch (recoveryErr) {
|
|
1281
|
+
hsm._tracePopError(`unhandled event recovery failure: ${quoteUnknown(recoveryErr)}`);
|
|
1282
|
+
if (recoveryErr instanceof TransitionError) {
|
|
1283
|
+
hsm.currentState = FatalErrorState;
|
|
1284
|
+
hsm._tracePopError(`unhandled event recovery failure: ${quoteUnknown(recoveryErr)}`);
|
|
1285
|
+
throw recoveryErr;
|
|
1286
|
+
}
|
|
1287
|
+
try {
|
|
1288
|
+
await verboseDoError(hsm, resolver, asError(recoveryErr), () => {
|
|
1289
|
+
hsm._tracePopDone('unhandled event recovery successful');
|
|
1290
|
+
onComplete();
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
catch (nestedErr) {
|
|
1294
|
+
hsm._tracePopError(`unhandled event recovery failure: ${quoteUnknown(nestedErr)}`);
|
|
1295
|
+
throw nestedErr;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
async function dispatchEventVerbose(hsm, resolver, eventName, ...eventPayload) {
|
|
1300
|
+
const eventLabel = String(eventName);
|
|
1301
|
+
hsm._traceWrite(`begin event dispatch of #${eventLabel}`);
|
|
1302
|
+
hsm._tracePush(`#${eventLabel}`, `started event dispatch`);
|
|
1303
|
+
hsm._currentEventName = eventLabel;
|
|
1304
|
+
hsm._currentEventPayload = eventPayload;
|
|
1305
|
+
try {
|
|
1306
|
+
const eventHandler = lookupEventHandler(hsm, eventName);
|
|
1307
|
+
if (!eventHandler) {
|
|
1308
|
+
hsm._traceWrite(`event #${eventLabel} is unhandled in state ${hsm.currentStateName}`);
|
|
1309
|
+
try {
|
|
1310
|
+
await verboseDoUnhandledEvent(hsm, resolver, new UnhandledEventError(hsm), () => {
|
|
1311
|
+
hsm._tracePopDone('event dispatch successful');
|
|
1312
|
+
verboseFinishEventDispatch(hsm);
|
|
1313
|
+
});
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
catch (recoveryErr) {
|
|
1317
|
+
hsm._tracePopError(`event dispatch failed: ${quoteUnknown(recoveryErr)}`);
|
|
1318
|
+
verboseFinishEventDispatch(hsm);
|
|
1319
|
+
throw recoveryErr;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
try {
|
|
1323
|
+
hsm._tracePush('execute', 'started event handler execution');
|
|
1324
|
+
const result = eventHandler.call(hsm._instance, ...eventPayload);
|
|
1325
|
+
if (result) {
|
|
1326
|
+
await result;
|
|
1327
|
+
}
|
|
1328
|
+
hsm._tracePopDone('event handler execution successful');
|
|
1329
|
+
await completePendingTransitions(hsm, resolver, () => {
|
|
1330
|
+
hsm._tracePopDone(`event dispatch successful`);
|
|
1331
|
+
verboseFinishEventDispatch(hsm);
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
catch (recoveryErr) {
|
|
1335
|
+
hsm._tracePopError(quoteUnknown(recoveryErr));
|
|
1336
|
+
if (recoveryErr instanceof UnhandledEventError) {
|
|
1337
|
+
hsm._traceWrite(`event #${eventLabel} is unhandled in state ${hsm.currentStateName}`);
|
|
1338
|
+
try {
|
|
1339
|
+
await verboseDoUnhandledEvent(hsm, resolver, recoveryErr, () => {
|
|
1340
|
+
hsm._tracePopDone('event dispatch successful');
|
|
1341
|
+
verboseFinishEventDispatch(hsm);
|
|
1342
|
+
});
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
catch (nestedErr) {
|
|
1346
|
+
hsm._tracePopError(`event dispatch failed: ${quoteUnknown(nestedErr)}`);
|
|
1347
|
+
verboseFinishEventDispatch(hsm);
|
|
1348
|
+
throw nestedErr;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
else if (recoveryErr instanceof TransitionError) {
|
|
1352
|
+
hsm._tracePopError(`event dispatch failed: ${quoteUnknown(recoveryErr)}`);
|
|
1353
|
+
verboseFinishEventDispatch(hsm);
|
|
1354
|
+
throw recoveryErr;
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
try {
|
|
1358
|
+
await verboseDoError(hsm, resolver, asError(recoveryErr), () => {
|
|
1359
|
+
hsm._tracePopDone('event dispatch successful');
|
|
1360
|
+
verboseFinishEventDispatch(hsm);
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
catch (nestedErr) {
|
|
1364
|
+
hsm._tracePopError(`event dispatch failed: ${quoteUnknown(nestedErr)}`);
|
|
1365
|
+
verboseFinishEventDispatch(hsm);
|
|
1366
|
+
throw nestedErr;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
catch (err) {
|
|
1372
|
+
verboseFinishEventDispatch(hsm);
|
|
1373
|
+
throw err;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
const productionDispatchStrategy = {
|
|
1377
|
+
executeInit: executeInitProduction,
|
|
1378
|
+
dispatchEvent: dispatchEventProduction,
|
|
1379
|
+
};
|
|
1380
|
+
const debugDispatchStrategy = {
|
|
1381
|
+
executeInit: executeInitDebug,
|
|
1382
|
+
dispatchEvent: dispatchEventDebug,
|
|
1383
|
+
};
|
|
1384
|
+
const verboseDispatchStrategy = {
|
|
1385
|
+
executeInit: executeInitVerbose,
|
|
1386
|
+
dispatchEvent: dispatchEventVerbose,
|
|
1387
|
+
};
|
|
1388
|
+
function dispatchStrategyFor(traceLevel) {
|
|
1389
|
+
switch (traceLevel) {
|
|
1390
|
+
case TraceLevel.PRODUCTION:
|
|
1391
|
+
return productionDispatchStrategy;
|
|
1392
|
+
case TraceLevel.DEBUG:
|
|
1393
|
+
return debugDispatchStrategy;
|
|
1394
|
+
case TraceLevel.VERBOSE_DEBUG:
|
|
1395
|
+
return verboseDispatchStrategy;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
//#endregion
|
|
1399
|
+
/** @internal */
|
|
1400
|
+
function createInitTask(host, resolver) {
|
|
1401
|
+
const strategy = dispatchStrategyFor(host.traceLevel);
|
|
1402
|
+
return (done) => {
|
|
1403
|
+
strategy
|
|
1404
|
+
.executeInit(host)
|
|
1405
|
+
.then(() => executePendingTransition(host, resolver))
|
|
1406
|
+
.then(() => done())
|
|
1407
|
+
.catch((err) => {
|
|
1408
|
+
host.dispatchErrorCallback(host, asError(err));
|
|
1409
|
+
done();
|
|
1410
|
+
});
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
/** @internal */
|
|
1414
|
+
function createNotificationTask(host, resolver, name, args) {
|
|
1415
|
+
const strategy = dispatchStrategyFor(host.traceLevel);
|
|
1416
|
+
return (done) => {
|
|
1417
|
+
strategy
|
|
1418
|
+
.dispatchEvent(host, resolver, name, ...args)
|
|
1419
|
+
.catch((err) => host.dispatchErrorCallback(host, asError(err)))
|
|
1420
|
+
.finally(() => done());
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
/** @internal */
|
|
1424
|
+
function createServiceTask(host, resolver, name, args, resolve, reject) {
|
|
1425
|
+
const machine = host;
|
|
1426
|
+
return (done) => {
|
|
1427
|
+
const run = () => invokeHandler(host, resolver, name, args)
|
|
1428
|
+
.then(resolve)
|
|
1429
|
+
.catch((err) => {
|
|
1430
|
+
reject(asError(err));
|
|
1431
|
+
})
|
|
1432
|
+
.catch((err) => host.dispatchErrorCallback(host, asError(err)))
|
|
1433
|
+
.finally(() => done());
|
|
1434
|
+
if (host.traceLevel === TraceLevel.PRODUCTION) {
|
|
1435
|
+
void run();
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
const storage = exports.dispatchContext.get();
|
|
1439
|
+
if (storage === undefined) {
|
|
1440
|
+
void run();
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
void storage.run({ machine }, run);
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
//#endregion
|
|
1447
|
+
//#region hsm
|
|
1448
|
+
/** @internal */
|
|
1449
|
+
class HsmObject {
|
|
1450
|
+
topState;
|
|
1451
|
+
topStateName;
|
|
1452
|
+
ctxTypeName;
|
|
1453
|
+
traceWriter;
|
|
1454
|
+
/** @internal */
|
|
1455
|
+
_instance;
|
|
1456
|
+
/** @internal */
|
|
1457
|
+
_jobs;
|
|
1458
|
+
/** @internal */
|
|
1459
|
+
_hiPriorityJobs;
|
|
1460
|
+
_isRunning = false;
|
|
1461
|
+
_transitionState;
|
|
1462
|
+
_currentEventName;
|
|
1463
|
+
_currentEventPayload;
|
|
1464
|
+
_observers;
|
|
1465
|
+
dispatchErrorCallback;
|
|
1466
|
+
_traceLevel;
|
|
1467
|
+
_traceDomainStack;
|
|
1468
|
+
constructor(TopState, instance, traceWriter, traceLevel, dispatchErrorCallback) {
|
|
1469
|
+
this._instance = instance;
|
|
1470
|
+
this._transitionState = undefined;
|
|
1471
|
+
this._traceLevel = traceLevel;
|
|
1472
|
+
this._currentEventName = undefined;
|
|
1473
|
+
this._currentEventPayload = undefined;
|
|
1474
|
+
this._traceDomainStack = [];
|
|
1475
|
+
this._jobs = [];
|
|
1476
|
+
this._hiPriorityJobs = [];
|
|
1477
|
+
this._isRunning = false;
|
|
1478
|
+
this.topState = TopState;
|
|
1479
|
+
this.topStateName = getStateName(TopState);
|
|
1480
|
+
this.ctxTypeName = Object.getPrototypeOf(instance.ctx).constructor.name;
|
|
1481
|
+
this.currentState = TopState;
|
|
1482
|
+
this.traceWriter = traceWriter;
|
|
1483
|
+
this.dispatchErrorCallback = dispatchErrorCallback;
|
|
1484
|
+
}
|
|
1485
|
+
get ctx() {
|
|
1486
|
+
return this._instance.ctx;
|
|
1487
|
+
}
|
|
1488
|
+
set ctx(ctx) {
|
|
1489
|
+
this._instance.ctx = ctx;
|
|
1490
|
+
}
|
|
1491
|
+
get port() {
|
|
1492
|
+
return this._instance.portRef;
|
|
1493
|
+
}
|
|
1494
|
+
get eventName() {
|
|
1495
|
+
return this._currentEventName ?? '';
|
|
1496
|
+
}
|
|
1497
|
+
get eventPayload() {
|
|
1498
|
+
return this._currentEventPayload ?? [];
|
|
1499
|
+
}
|
|
1500
|
+
get currentStateName() {
|
|
1501
|
+
return getStateName(Object.getPrototypeOf(this._instance).constructor);
|
|
1502
|
+
}
|
|
1503
|
+
get currentState() {
|
|
1504
|
+
return Object.getPrototypeOf(this._instance).constructor;
|
|
1505
|
+
}
|
|
1506
|
+
set currentState(newState) {
|
|
1507
|
+
Object.setPrototypeOf(this._instance, newState.prototype);
|
|
1508
|
+
}
|
|
1509
|
+
subscribe(observer) {
|
|
1510
|
+
if (this._observers === undefined)
|
|
1511
|
+
this._observers = new Set();
|
|
1512
|
+
this._observers.add(observer);
|
|
1513
|
+
return {
|
|
1514
|
+
dispose: () => {
|
|
1515
|
+
this._observers?.delete(observer);
|
|
1516
|
+
},
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
_notifyObservers(eventName, eventPayload) {
|
|
1520
|
+
if (this._observers === undefined || this._observers.size === 0)
|
|
1521
|
+
return;
|
|
1522
|
+
const message = { event: String(eventName), payload: [...eventPayload] };
|
|
1523
|
+
for (const observer of this._observers)
|
|
1524
|
+
observer(message);
|
|
1525
|
+
}
|
|
1526
|
+
recordObserverEvent(eventName, eventPayload) {
|
|
1527
|
+
this._notifyObservers(eventName, eventPayload);
|
|
1528
|
+
}
|
|
1529
|
+
transition(nextState) {
|
|
1530
|
+
this._transitionState = nextState;
|
|
1531
|
+
}
|
|
1532
|
+
unhandled() {
|
|
1533
|
+
throw new UnhandledEventError(this);
|
|
1534
|
+
}
|
|
1535
|
+
get traceLevel() {
|
|
1536
|
+
return this._traceLevel;
|
|
1537
|
+
}
|
|
1538
|
+
set traceLevel(traceLevel) {
|
|
1539
|
+
this._traceLevel = traceLevel;
|
|
1540
|
+
}
|
|
1541
|
+
sync() {
|
|
1542
|
+
return new Promise(resolve => {
|
|
1543
|
+
this.pushTask((doneCallback) => {
|
|
1544
|
+
resolve();
|
|
1545
|
+
doneCallback();
|
|
1546
|
+
});
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
pushTask(t) {
|
|
1550
|
+
this.enqueueTask(t, this._jobs);
|
|
1551
|
+
}
|
|
1552
|
+
pushHiPriorityTask(t) {
|
|
1553
|
+
this.enqueueTask(t, this._hiPriorityJobs);
|
|
1554
|
+
}
|
|
1555
|
+
unshiftHiPriorityTask(t) {
|
|
1556
|
+
this._hiPriorityJobs.unshift(t);
|
|
1557
|
+
if (this._isRunning)
|
|
1558
|
+
return;
|
|
1559
|
+
this._isRunning = true;
|
|
1560
|
+
this.dequeue();
|
|
1561
|
+
}
|
|
1562
|
+
enqueueTask(t, queue) {
|
|
1563
|
+
queue.push(t);
|
|
1564
|
+
if (this._isRunning)
|
|
1565
|
+
return;
|
|
1566
|
+
this._isRunning = true;
|
|
1567
|
+
this.dequeue();
|
|
1568
|
+
}
|
|
1569
|
+
restore(state, ctx) {
|
|
1570
|
+
this.currentState = state;
|
|
1571
|
+
this.ctx = ctx;
|
|
1572
|
+
}
|
|
1573
|
+
dequeue() {
|
|
1574
|
+
if (this._hiPriorityJobs.length == 0 && this._jobs.length == 0) {
|
|
1575
|
+
this._isRunning = false;
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const task = this._hiPriorityJobs.length > 0 ? this._hiPriorityJobs.shift() : this._jobs.shift();
|
|
1579
|
+
this.exec(task);
|
|
1580
|
+
}
|
|
1581
|
+
exec(task) {
|
|
1582
|
+
setTimeout(() => this.runTask(task).then(() => this.dequeue()), 0);
|
|
1583
|
+
}
|
|
1584
|
+
runTask(task) {
|
|
1585
|
+
return new Promise(resolve => {
|
|
1586
|
+
task(() => {
|
|
1587
|
+
this.drainHiPriority().then(resolve);
|
|
1588
|
+
});
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
drainHiPriority() {
|
|
1592
|
+
if (this._hiPriorityJobs.length === 0) {
|
|
1593
|
+
return Promise.resolve();
|
|
1594
|
+
}
|
|
1595
|
+
const task = this._hiPriorityJobs.shift();
|
|
1596
|
+
return this.runTask(task).then(() => this.drainHiPriority());
|
|
1597
|
+
}
|
|
1598
|
+
_tracePush(d, msg) {
|
|
1599
|
+
this._traceDomainStack.push(d);
|
|
1600
|
+
this.traceWriter.write(this, msg);
|
|
1601
|
+
}
|
|
1602
|
+
_tracePopDone(msg) {
|
|
1603
|
+
this.traceWriter.write(this, `done: ${msg}`);
|
|
1604
|
+
this._traceDomainStack.pop();
|
|
1605
|
+
}
|
|
1606
|
+
_tracePopError(msg) {
|
|
1607
|
+
this.traceWriter.write(this, `failure: ${msg}`);
|
|
1608
|
+
this._traceDomainStack.pop();
|
|
1609
|
+
}
|
|
1610
|
+
_traceWrite(msg) {
|
|
1611
|
+
this.traceWriter.write(this, msg);
|
|
1612
|
+
}
|
|
1613
|
+
get traceHeader() {
|
|
1614
|
+
return `${this._traceDomainStack.length === 0 ? '' : this._traceDomainStack.join('|') + '|'}`;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
exports.HsmObject = HsmObject;
|
|
1618
|
+
//#region machine
|
|
1619
|
+
class Machine extends HsmObject {
|
|
1620
|
+
transitionResolver;
|
|
1621
|
+
_dispatchStrategy;
|
|
1622
|
+
protocolIndex;
|
|
1623
|
+
handlerFacade;
|
|
1624
|
+
selfActor;
|
|
1625
|
+
selfImmediate;
|
|
1626
|
+
actorFacades = new Map();
|
|
1627
|
+
constructor(topState, instance, protocolIndex, traceWriter, traceLevel, dispatchErrorCallback, initialize, transitionResolver) {
|
|
1628
|
+
super(topState, instance, traceWriter, traceLevel, dispatchErrorCallback);
|
|
1629
|
+
Object.defineProperty(instance, types_1.kHandlerMachine, { value: this, enumerable: false, writable: false, configurable: false });
|
|
1630
|
+
this.protocolIndex = protocolIndex;
|
|
1631
|
+
cacheProtocolIndex(topState, protocolIndex);
|
|
1632
|
+
this.transitionResolver = transitionResolver ?? new RuntimeTransitionResolver();
|
|
1633
|
+
this._dispatchStrategy = dispatchStrategyFor(traceLevel);
|
|
1634
|
+
this.selfActor = createSelfNotifications(this, topState, protocolIndex, 'default');
|
|
1635
|
+
this.selfImmediate = createSelfNotifications(this, topState, protocolIndex, 'priority');
|
|
1636
|
+
this.handlerFacade = this.buildHandlerFacade(instance);
|
|
1637
|
+
instance.hsm = this.handlerFacade;
|
|
1638
|
+
Object.defineProperty(instance, 'notify', { value: this.selfActor, enumerable: true, configurable: true });
|
|
1639
|
+
Object.defineProperty(instance, 'notifyNow', { value: this.selfImmediate, enumerable: true, configurable: true });
|
|
1640
|
+
this.bindPort(instance.portRef);
|
|
1641
|
+
if (initialize) {
|
|
1642
|
+
this.pushTask(createInitTask(this, this.transitionResolver));
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
get traceLevel() {
|
|
1646
|
+
return super.traceLevel;
|
|
1647
|
+
}
|
|
1648
|
+
set traceLevel(traceLevel) {
|
|
1649
|
+
super.traceLevel = traceLevel;
|
|
1650
|
+
this._dispatchStrategy = dispatchStrategyFor(traceLevel);
|
|
1651
|
+
}
|
|
1652
|
+
dispatchService(name, args) {
|
|
1653
|
+
if (this.traceLevel !== TraceLevel.PRODUCTION) {
|
|
1654
|
+
const storage = exports.dispatchContext.get();
|
|
1655
|
+
const token = storage?.getStore();
|
|
1656
|
+
if (token?.machine === this) {
|
|
1657
|
+
throw new SelfCallDeadlockError();
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
this.recordObserverEvent(name, args);
|
|
1661
|
+
return new Promise((resolve, reject) => {
|
|
1662
|
+
this.pushTask(createServiceTask(this, this.transitionResolver, name, args, resolve, reject));
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
dispatchNotification(name, args, queue) {
|
|
1666
|
+
this.recordObserverEvent(name, args);
|
|
1667
|
+
const task = createNotificationTask(this, this.transitionResolver, name, args);
|
|
1668
|
+
if (queue === 'priority') {
|
|
1669
|
+
this.pushHiPriorityTask(task);
|
|
1670
|
+
}
|
|
1671
|
+
else {
|
|
1672
|
+
this.pushTask(task);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
actorHsmFor(kind) {
|
|
1676
|
+
let facade = this.actorFacades.get(kind);
|
|
1677
|
+
if (facade === undefined) {
|
|
1678
|
+
facade = this.buildActorHsm(kind);
|
|
1679
|
+
this.actorFacades.set(kind, facade);
|
|
1680
|
+
}
|
|
1681
|
+
return facade;
|
|
1682
|
+
}
|
|
1683
|
+
scheduleNotification(ms, name, args) {
|
|
1684
|
+
const enqueue = () => {
|
|
1685
|
+
this.dispatchNotification(name, args, 'default');
|
|
1686
|
+
};
|
|
1687
|
+
const port = this._instance.portRef;
|
|
1688
|
+
if (port === undefined) {
|
|
1689
|
+
throw new Error('ihsm: deferred notification requires a port');
|
|
1690
|
+
}
|
|
1691
|
+
port.setTimeout(enqueue, ms);
|
|
1692
|
+
}
|
|
1693
|
+
/** @internal Binds deferred self-notifications to a port instance. */
|
|
1694
|
+
bindPort(portRef) {
|
|
1695
|
+
if (portRef instanceof Port) {
|
|
1696
|
+
portRef.bindDeferredNotifications(ms => this.createDeferredSelfNotifications(ms));
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
buildHandlerFacade(instance) {
|
|
1700
|
+
const machine = this;
|
|
1701
|
+
const facade = {
|
|
1702
|
+
get ctx() {
|
|
1703
|
+
return machine.ctx;
|
|
1704
|
+
},
|
|
1705
|
+
transition: next => machine.transition(next),
|
|
1706
|
+
get port() {
|
|
1707
|
+
return instance.portRef;
|
|
1708
|
+
},
|
|
1709
|
+
unhandled: () => machine.unhandled(),
|
|
1710
|
+
get eventName() {
|
|
1711
|
+
return machine.eventName;
|
|
1712
|
+
},
|
|
1713
|
+
get eventPayload() {
|
|
1714
|
+
return machine.eventPayload;
|
|
1715
|
+
},
|
|
1716
|
+
get currentState() {
|
|
1717
|
+
return machine.currentState;
|
|
1718
|
+
},
|
|
1719
|
+
get currentStateName() {
|
|
1720
|
+
return machine.currentStateName;
|
|
1721
|
+
},
|
|
1722
|
+
get topState() {
|
|
1723
|
+
return machine.topState;
|
|
1724
|
+
},
|
|
1725
|
+
get topStateName() {
|
|
1726
|
+
return machine.topStateName;
|
|
1727
|
+
},
|
|
1728
|
+
get traceHeader() {
|
|
1729
|
+
return machine.traceHeader;
|
|
1730
|
+
},
|
|
1731
|
+
get traceLevel() {
|
|
1732
|
+
return machine.traceLevel;
|
|
1733
|
+
},
|
|
1734
|
+
set traceLevel(level) {
|
|
1735
|
+
machine.traceLevel = level;
|
|
1736
|
+
},
|
|
1737
|
+
get traceWriter() {
|
|
1738
|
+
return machine.traceWriter;
|
|
1739
|
+
},
|
|
1740
|
+
set traceWriter(writer) {
|
|
1741
|
+
machine.traceWriter = writer;
|
|
1742
|
+
},
|
|
1743
|
+
get dispatchErrorCallback() {
|
|
1744
|
+
return machine.dispatchErrorCallback;
|
|
1745
|
+
},
|
|
1746
|
+
set dispatchErrorCallback(cb) {
|
|
1747
|
+
machine.dispatchErrorCallback = cb;
|
|
1748
|
+
},
|
|
1749
|
+
};
|
|
1750
|
+
return facade;
|
|
1751
|
+
}
|
|
1752
|
+
createDeferredSelfNotifications(ms) {
|
|
1753
|
+
const machine = this;
|
|
1754
|
+
const proto = Object.create(null);
|
|
1755
|
+
for (const [name, slot] of this.protocolIndex.entries('inbound')) {
|
|
1756
|
+
if (slot.bucket === 'notifications' || slot.bucket === 'internalNotifications') {
|
|
1757
|
+
proto[name] = (...args) => {
|
|
1758
|
+
machine.scheduleNotification(ms, name, args);
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
for (const [name, slot] of this.protocolIndex.entries('root')) {
|
|
1763
|
+
if (slot.bucket === 'notifications') {
|
|
1764
|
+
proto[name] = (...args) => {
|
|
1765
|
+
machine.scheduleNotification(ms, name, args);
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return Object.create(Object.freeze(proto));
|
|
1770
|
+
}
|
|
1771
|
+
buildActorHsm(kind) {
|
|
1772
|
+
const machine = this;
|
|
1773
|
+
const includeState = kind !== 'root';
|
|
1774
|
+
const includeOwner = kind === 'child' || kind === 'test';
|
|
1775
|
+
const facade = {
|
|
1776
|
+
sync: () => machine.sync(),
|
|
1777
|
+
get currentStateName() {
|
|
1778
|
+
return machine.currentStateName;
|
|
1779
|
+
},
|
|
1780
|
+
get topStateName() {
|
|
1781
|
+
return machine.topStateName;
|
|
1782
|
+
},
|
|
1783
|
+
get traceLevel() {
|
|
1784
|
+
return machine.traceLevel;
|
|
1785
|
+
},
|
|
1786
|
+
set traceLevel(level) {
|
|
1787
|
+
machine.traceLevel = level;
|
|
1788
|
+
},
|
|
1789
|
+
get traceWriter() {
|
|
1790
|
+
return machine.traceWriter;
|
|
1791
|
+
},
|
|
1792
|
+
set traceWriter(writer) {
|
|
1793
|
+
machine.traceWriter = writer;
|
|
1794
|
+
},
|
|
1795
|
+
get traceHeader() {
|
|
1796
|
+
return machine.traceHeader;
|
|
1797
|
+
},
|
|
1798
|
+
};
|
|
1799
|
+
if (includeState) {
|
|
1800
|
+
Object.defineProperties(facade, {
|
|
1801
|
+
currentState: {
|
|
1802
|
+
enumerable: true,
|
|
1803
|
+
get() {
|
|
1804
|
+
return machine.currentState;
|
|
1805
|
+
},
|
|
1806
|
+
},
|
|
1807
|
+
topState: {
|
|
1808
|
+
enumerable: true,
|
|
1809
|
+
get() {
|
|
1810
|
+
return machine.topState;
|
|
1811
|
+
},
|
|
1812
|
+
},
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
if (includeOwner) {
|
|
1816
|
+
Object.defineProperties(facade, {
|
|
1817
|
+
restore: {
|
|
1818
|
+
enumerable: true,
|
|
1819
|
+
value: (state, ctx) => machine.restore(state, ctx),
|
|
1820
|
+
},
|
|
1821
|
+
dispatchErrorCallback: {
|
|
1822
|
+
enumerable: true,
|
|
1823
|
+
get() {
|
|
1824
|
+
return machine.dispatchErrorCallback;
|
|
1825
|
+
},
|
|
1826
|
+
set(cb) {
|
|
1827
|
+
machine.dispatchErrorCallback = cb;
|
|
1828
|
+
},
|
|
1829
|
+
},
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
return facade;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
exports.Machine = Machine;
|
|
1836
|
+
//#region factories
|
|
1837
|
+
function isRequestingPort(port) {
|
|
1838
|
+
if (port === null || typeof port !== 'object') {
|
|
1839
|
+
return false;
|
|
1840
|
+
}
|
|
1841
|
+
const ctor = Object.getPrototypeOf(port)?.constructor;
|
|
1842
|
+
if (ctor === undefined) {
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
return ctor[kRequestingPort] === true;
|
|
1846
|
+
}
|
|
1847
|
+
class ConsoleTraceWriter {
|
|
1848
|
+
write(hsm, msg) {
|
|
1849
|
+
if (typeof msg === 'string') {
|
|
1850
|
+
console.log(`${hsm.traceHeader}${hsm.currentStateName}: ${msg}`);
|
|
1851
|
+
}
|
|
1852
|
+
else {
|
|
1853
|
+
console.log(msg);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
/** @internal */
|
|
1858
|
+
exports.defaultTraceWriter = new ConsoleTraceWriter();
|
|
1859
|
+
/** @internal */
|
|
1860
|
+
exports.defaultInitialize = true;
|
|
1861
|
+
/** @internal */
|
|
1862
|
+
function defaultDispatchErrorCallback(hsm, err) {
|
|
1863
|
+
hsm.traceWriter.write(hsm, `An event dispatch has failed; error ${err.name}: ${err.message} has not been managed`);
|
|
1864
|
+
hsm.traceWriter.write(hsm, err);
|
|
1865
|
+
throw err;
|
|
1866
|
+
}
|
|
1867
|
+
/** @internal Spawn with embodiment kind — used by factories and `ihsm/testing`. */
|
|
1868
|
+
function spawnActor(kind, topState, ctx, port, options) {
|
|
1869
|
+
const { initialize = exports.defaultInitialize, traceLevel = TraceLevel.DEBUG, traceWriter = exports.defaultTraceWriter, dispatchErrorCallback = defaultDispatchErrorCallback, transitions } = options;
|
|
1870
|
+
const protocolIndex = buildProtocolIndex(topState);
|
|
1871
|
+
const boundPort = (port ?? new Port());
|
|
1872
|
+
const instance = {
|
|
1873
|
+
ctx,
|
|
1874
|
+
hsm: undefined,
|
|
1875
|
+
portRef: boundPort,
|
|
1876
|
+
};
|
|
1877
|
+
Object.setPrototypeOf(instance, topState.prototype);
|
|
1878
|
+
const machine = new Machine(topState, instance, protocolIndex, traceWriter, traceLevel, dispatchErrorCallback, initialize, transitions ?? new RuntimeTransitionResolver());
|
|
1879
|
+
const portKind = isRequestingPort(boundPort) ? 'child' : kind === 'root' ? 'inbound' : kind;
|
|
1880
|
+
boundPort.actor = createActorHandle(machine, topState, protocolIndex, portKind);
|
|
1881
|
+
return createActorHandle(machine, topState, protocolIndex, kind);
|
|
1882
|
+
}
|
|
1883
|
+
/** Production black-box — public protocol only (generated handle). */
|
|
1884
|
+
function makeActor(topState, ctx, port, options = {}) {
|
|
1885
|
+
return spawnActor('root', topState, ctx, port, options);
|
|
1886
|
+
}
|
|
1887
|
+
function asParentActor(handler) {
|
|
1888
|
+
const machine = handler[types_1.kHandlerMachine];
|
|
1889
|
+
if (machine === undefined) {
|
|
1890
|
+
throw new Error('ihsm: asParentActor requires an active handler machine');
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
top: machine.topState,
|
|
1894
|
+
[types_1.kParentLink]: machine,
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
/** Parent composes a child machine — returns full child protocol shell with `parent` set. */
|
|
1898
|
+
function makeChildActor(parent, childTop, childCtx, port, options = {}) {
|
|
1899
|
+
const child = spawnActor('child', childTop, childCtx, port, options);
|
|
1900
|
+
Object.defineProperty(child, 'parent', { value: parent, enumerable: true, writable: false, configurable: true });
|
|
1901
|
+
return child;
|
|
1902
|
+
}
|
|
1903
|
+
var types_2 = require("./types");
|
|
1904
|
+
Object.defineProperty(exports, "kHandlerMachine", { enumerable: true, get: function () { return types_2.kHandlerMachine; } });
|
|
1905
|
+
Object.defineProperty(exports, "kParentLink", { enumerable: true, get: function () { return types_2.kParentLink; } });
|
|
1906
|
+
//# sourceMappingURL=runtime.js.map
|