ihsm 0.1.1 → 0.1.21
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/lib/cjs/index.d.ts +4 -1
- package/lib/cjs/index.js +13 -1
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/internal/console-instrumentation.d.ts +34 -0
- package/lib/cjs/internal/console-instrumentation.js +71 -0
- package/lib/cjs/internal/console-instrumentation.js.map +1 -0
- package/lib/cjs/internal/identity.d.ts +16 -0
- package/lib/cjs/internal/identity.js +170 -0
- package/lib/cjs/internal/identity.js.map +1 -0
- package/lib/cjs/internal/instrumentation.d.ts +47 -0
- package/lib/cjs/internal/instrumentation.js +201 -0
- package/lib/cjs/internal/instrumentation.js.map +1 -0
- package/lib/cjs/internal/runtime.d.ts +89 -6
- package/lib/cjs/internal/runtime.js +676 -52
- package/lib/cjs/internal/runtime.js.map +1 -1
- package/lib/cjs/internal/types.d.ts +174 -7
- package/lib/cjs/internal/types.js.map +1 -1
- package/lib/cjs/testing.d.ts +86 -1
- package/lib/cjs/testing.js +54 -0
- package/lib/cjs/testing.js.map +1 -1
- package/lib/cjs/types.d.ts +1 -1
- package/lib/esm/index.d.ts +4 -1
- package/lib/esm/index.js +3 -1
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/internal/console-instrumentation.d.ts +34 -0
- package/lib/esm/internal/console-instrumentation.js +68 -0
- package/lib/esm/internal/console-instrumentation.js.map +1 -0
- package/lib/esm/internal/identity.d.ts +16 -0
- package/lib/esm/internal/identity.js +159 -0
- package/lib/esm/internal/identity.js.map +1 -0
- package/lib/esm/internal/instrumentation.d.ts +47 -0
- package/lib/esm/internal/instrumentation.js +178 -0
- package/lib/esm/internal/instrumentation.js.map +1 -0
- package/lib/esm/internal/runtime.d.ts +89 -6
- package/lib/esm/internal/runtime.js +666 -51
- package/lib/esm/internal/runtime.js.map +1 -1
- package/lib/esm/internal/types.d.ts +174 -7
- package/lib/esm/internal/types.js.map +1 -1
- package/lib/esm/testing.d.ts +86 -1
- package/lib/esm/testing.js +53 -0
- package/lib/esm/testing.js.map +1 -1
- package/lib/esm/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { kHandlerMachine, kParentLink } from './types.js';
|
|
2
|
+
import { actorNameFromTopState, childActorPath, mintActorIdentity, rootActorPath } from './identity.js';
|
|
3
|
+
import { getActiveInstrumentation, getTaskMeta, notifyActorCreated, notifyActorSpawned, notifyEnqueue, notifyError, notifyLog, notifyMacrostepBegin, notifyMacrostepEnd, notifyMicrostepBegin, notifyMicrostepEnd, notifyOutboundCallBegin, notifyOutboundCallEnd, notifyPortCallBegin, notifyPortCallEnd, setTaskMeta } from './instrumentation.js';
|
|
2
4
|
//#region TraceLevel
|
|
3
5
|
export var TraceLevel;
|
|
4
6
|
(function (TraceLevel) {
|
|
@@ -220,6 +222,20 @@ export class FatalErrorState extends TopState {
|
|
|
220
222
|
defineStateName(TopState, 'TopState');
|
|
221
223
|
defineStateName(FatalErrorState, 'FatalErrorState');
|
|
222
224
|
/** @internal */
|
|
225
|
+
export function lookupHandlerState(hsm, eventName) {
|
|
226
|
+
let state = hsm.currentState;
|
|
227
|
+
while (true) {
|
|
228
|
+
const prototype = state.prototype;
|
|
229
|
+
if (Object.prototype.hasOwnProperty.call(prototype, eventName)) {
|
|
230
|
+
return getStateName(state);
|
|
231
|
+
}
|
|
232
|
+
if (state === TopState) {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
state = Object.getPrototypeOf(state);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** @internal */
|
|
223
239
|
export function lookupEventHandler(hsm, eventName) {
|
|
224
240
|
let state = hsm.currentState;
|
|
225
241
|
while (true) {
|
|
@@ -444,19 +460,29 @@ export function splitServiceArgs(args) {
|
|
|
444
460
|
}
|
|
445
461
|
return { callArgs: args.slice(0, -1), timeoutMs };
|
|
446
462
|
}
|
|
447
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Race a service-call promise against a `timeoutMs` deadline.
|
|
465
|
+
*
|
|
466
|
+
* The deadline is armed through `timer` — the actor's port timer service — so that under a
|
|
467
|
+
* {@link TestPort} virtual clock a call timeout is driven by `port.advance(...)` and stays
|
|
468
|
+
* fully deterministic. When no port timer is available the host timer is used (production
|
|
469
|
+
* `Port` already delegates to the host timer, so behaviour there is unchanged).
|
|
470
|
+
*/
|
|
471
|
+
export function serviceCallWithTimeout(promise, method, timeoutMs, timer) {
|
|
448
472
|
if (timeoutMs === 0) {
|
|
449
473
|
return Promise.reject(new CallTimeoutError(method));
|
|
450
474
|
}
|
|
475
|
+
const arm = (callback) => (timer !== undefined ? timer.setTimeout(callback, timeoutMs) : globalThis.setTimeout(callback, timeoutMs));
|
|
476
|
+
const disarm = (handle) => (timer !== undefined ? timer.clearTimeout(handle) : globalThis.clearTimeout(handle));
|
|
451
477
|
return new Promise((resolve, reject) => {
|
|
452
|
-
const
|
|
478
|
+
const handle = arm(() => {
|
|
453
479
|
reject(new CallTimeoutError(method));
|
|
454
|
-
}
|
|
480
|
+
});
|
|
455
481
|
promise.then(value => {
|
|
456
|
-
|
|
482
|
+
disarm(handle);
|
|
457
483
|
resolve(value);
|
|
458
484
|
}, err => {
|
|
459
|
-
|
|
485
|
+
disarm(handle);
|
|
460
486
|
reject(err);
|
|
461
487
|
});
|
|
462
488
|
});
|
|
@@ -488,8 +514,17 @@ function getFacetProto(topState, index, kind, facet) {
|
|
|
488
514
|
if (facet === 'call') {
|
|
489
515
|
built[name] = function (...args) {
|
|
490
516
|
const { callArgs, timeoutMs } = splitServiceArgs(args);
|
|
491
|
-
const
|
|
492
|
-
|
|
517
|
+
const machine = this[kMachine];
|
|
518
|
+
const begin = machine.beginOutboundCall?.(name, machine.actorUuid);
|
|
519
|
+
const promise = machine.dispatchService(name, callArgs).then(value => {
|
|
520
|
+
machine.endOutboundCall?.(begin, 'ok');
|
|
521
|
+
return value;
|
|
522
|
+
}, cause => {
|
|
523
|
+
const err = asError(cause);
|
|
524
|
+
machine.endOutboundCall?.(begin, 'error', err);
|
|
525
|
+
throw err;
|
|
526
|
+
});
|
|
527
|
+
return timeoutMs === undefined ? promise : serviceCallWithTimeout(promise, name, timeoutMs, machine.callTimer);
|
|
493
528
|
};
|
|
494
529
|
}
|
|
495
530
|
else {
|
|
@@ -526,6 +561,12 @@ export function createActorHandle(machine, topState, index, kind) {
|
|
|
526
561
|
Object.defineProperty(handle, 'notify', { value: createFacet(machine, topState, index, kind, 'notify'), enumerable: true });
|
|
527
562
|
Object.defineProperty(handle, 'notifyNow', { value: createFacet(machine, topState, index, kind, 'notifyNow'), enumerable: true });
|
|
528
563
|
Object.defineProperty(handle, 'call', { value: createFacet(machine, topState, index, kind, 'call'), enumerable: true });
|
|
564
|
+
Object.defineProperty(handle, 'id', {
|
|
565
|
+
enumerable: true,
|
|
566
|
+
get() {
|
|
567
|
+
return machine.actorUuid;
|
|
568
|
+
},
|
|
569
|
+
});
|
|
529
570
|
handle.hsm = machine.actorHsmFor(kind);
|
|
530
571
|
return handle;
|
|
531
572
|
}
|
|
@@ -567,6 +608,18 @@ export class SelfCallDeadlockError extends Error {
|
|
|
567
608
|
this.name = 'SelfCallDeadlockError';
|
|
568
609
|
}
|
|
569
610
|
}
|
|
611
|
+
function errorPhaseFromError(err) {
|
|
612
|
+
if (err instanceof TransitionError) {
|
|
613
|
+
return err.failedCallback === 'onEntry' ? 'onEntry' : 'onExit';
|
|
614
|
+
}
|
|
615
|
+
if (err instanceof UnhandledEventError)
|
|
616
|
+
return 'unhandled';
|
|
617
|
+
if (err instanceof InitializationError)
|
|
618
|
+
return 'initialize';
|
|
619
|
+
if (err instanceof EventHandlerError)
|
|
620
|
+
return 'handler';
|
|
621
|
+
return 'handler';
|
|
622
|
+
}
|
|
570
623
|
/**
|
|
571
624
|
* Lazy Node AsyncLocalStorage for non-production deadlock detection.
|
|
572
625
|
* State lives in a closure — no exported mutable slot.
|
|
@@ -601,6 +654,45 @@ export const dispatchContext = (() => {
|
|
|
601
654
|
}
|
|
602
655
|
return { get, resetInit, markUnavailable };
|
|
603
656
|
})();
|
|
657
|
+
/**
|
|
658
|
+
* Best-effort current runtime trace anchor (`actorUuid`, `macrostepId`, `stepSeq`) for user code.
|
|
659
|
+
* Returns `undefined` when called outside an active handler dispatch turn.
|
|
660
|
+
*/
|
|
661
|
+
export function currentTraceAnchor() {
|
|
662
|
+
const token = dispatchContext.get()?.getStore();
|
|
663
|
+
if (token?.actorUuid === undefined)
|
|
664
|
+
return undefined;
|
|
665
|
+
return {
|
|
666
|
+
actorUuid: token.actorUuid,
|
|
667
|
+
macrostepId: token.macrostepId,
|
|
668
|
+
stepSeq: token.stepSeq,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/** Lazy ALS carrying the currently-executing proxied port call (when instrumentation is active). */
|
|
672
|
+
const portCallContext = (() => {
|
|
673
|
+
let storage = undefined;
|
|
674
|
+
function get() {
|
|
675
|
+
if (storage !== undefined) {
|
|
676
|
+
return storage ?? undefined;
|
|
677
|
+
}
|
|
678
|
+
if (typeof process === 'undefined' || process.versions?.node === undefined) {
|
|
679
|
+
storage = null;
|
|
680
|
+
return undefined;
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
// Dynamic require — keeps browser bundles free of `node:async_hooks`.
|
|
684
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
685
|
+
const hooks = require('node:async_hooks');
|
|
686
|
+
storage = new hooks.AsyncLocalStorage();
|
|
687
|
+
return storage;
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
storage = null;
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return { get };
|
|
695
|
+
})();
|
|
604
696
|
//#region transition-routines
|
|
605
697
|
/** Thrown when a generated transition table's graph hash does not match the scanned hierarchy. */
|
|
606
698
|
export class TransitionTableError extends Error {
|
|
@@ -651,10 +743,14 @@ export function planTransitionClasses(srcState, destState) {
|
|
|
651
743
|
}
|
|
652
744
|
return { exit: srcPath, entry: dstPath, finalState };
|
|
653
745
|
}
|
|
654
|
-
async function invokeLifecycleHook(hsm, instance, state, hook, fromStateName, toStateName, style, tracer) {
|
|
746
|
+
async function invokeLifecycleHook(hsm, instance, state, hook, fromStateName, toStateName, style, tracer, hookEvents) {
|
|
655
747
|
const statePrototype = state.prototype;
|
|
656
748
|
const stateName = getStateName(state);
|
|
657
749
|
const hasHook = Object.prototype.hasOwnProperty.call(statePrototype, hook);
|
|
750
|
+
// Emit hook tracer callbacks for real (own) hooks at verbose, or always for a structural seam
|
|
751
|
+
// (instrumentation) whose entry/exit spans are not TraceLevel-gated (spec §4.8). Skipped default
|
|
752
|
+
// hooks are never eventized.
|
|
753
|
+
const emitHookEvents = (style === 'verbose' || hookEvents) && hasHook;
|
|
658
754
|
if ((style === 'verbose' || style === 'debug') && !hasHook) {
|
|
659
755
|
if (style === 'verbose') {
|
|
660
756
|
tracer?.traceHookSkipped(stateName, hook);
|
|
@@ -662,16 +758,19 @@ async function invokeLifecycleHook(hsm, instance, state, hook, fromStateName, to
|
|
|
662
758
|
return;
|
|
663
759
|
}
|
|
664
760
|
try {
|
|
761
|
+
if (emitHookEvents) {
|
|
762
|
+
tracer?.traceHookStart?.(stateName, hook);
|
|
763
|
+
}
|
|
665
764
|
const res = statePrototype[hook].call(instance);
|
|
666
765
|
if (res) {
|
|
667
766
|
await res;
|
|
668
767
|
}
|
|
669
|
-
if (
|
|
768
|
+
if (emitHookEvents) {
|
|
670
769
|
tracer?.traceHookDone(stateName, hook);
|
|
671
770
|
}
|
|
672
771
|
}
|
|
673
772
|
catch (cause) {
|
|
674
|
-
if (
|
|
773
|
+
if (emitHookEvents) {
|
|
675
774
|
tracer?.traceHookError(stateName, hook, cause);
|
|
676
775
|
}
|
|
677
776
|
throw new TransitionError(hsm, asError(cause), stateName, hook, fromStateName, toStateName);
|
|
@@ -685,16 +784,27 @@ async function invokeLifecycleHook(hsm, instance, state, hook, fromStateName, to
|
|
|
685
784
|
export async function executeTransitionRoutine(hsm, instance, plan, srcState, dstState, options = {}) {
|
|
686
785
|
const style = options.style ?? 'production';
|
|
687
786
|
const tracer = options.tracer;
|
|
787
|
+
const hookEvents = options.hookEvents ?? false;
|
|
688
788
|
const fromStateName = getStateName(srcState);
|
|
689
789
|
const toStateName = getStateName(dstState);
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
790
|
+
// The structural transition span fires whenever a tracer is attached (the instrumentation seam is
|
|
791
|
+
// not TraceLevel-gated); the console tracer is only ever supplied off-PRODUCTION, so this is a
|
|
792
|
+
// no-op there unless an explicit instrumentation tracer is present.
|
|
793
|
+
tracer?.traceTransitionStart(fromStateName, toStateName);
|
|
693
794
|
for (const state of plan.exit) {
|
|
694
|
-
await invokeLifecycleHook(hsm, instance, state, 'onExit', fromStateName, toStateName, style, tracer);
|
|
795
|
+
await invokeLifecycleHook(hsm, instance, state, 'onExit', fromStateName, toStateName, style, tracer, hookEvents);
|
|
695
796
|
}
|
|
797
|
+
let initializeOpened = false;
|
|
696
798
|
for (const state of plan.entry) {
|
|
697
|
-
|
|
799
|
+
if (!initializeOpened && state !== dstState) {
|
|
800
|
+
tracer?.traceInitializeStart?.(toStateName);
|
|
801
|
+
initializeOpened = true;
|
|
802
|
+
}
|
|
803
|
+
await invokeLifecycleHook(hsm, instance, state, 'onEntry', fromStateName, toStateName, style, tracer, hookEvents);
|
|
804
|
+
}
|
|
805
|
+
if (initializeOpened) {
|
|
806
|
+
const finalName = plan.finalState !== undefined ? getStateName(plan.finalState) : toStateName;
|
|
807
|
+
tracer?.traceInitializeDone?.(finalName);
|
|
698
808
|
}
|
|
699
809
|
const applyState = (next) => {
|
|
700
810
|
if (options.setCurrentState) {
|
|
@@ -717,6 +827,8 @@ export async function executeTransitionRoutine(hsm, instance, plan, srcState, ds
|
|
|
717
827
|
}
|
|
718
828
|
if (plan.finalState) {
|
|
719
829
|
applyState(plan.finalState);
|
|
830
|
+
// Close the structural transition span on the PRODUCTION path (verbose/debug returned above).
|
|
831
|
+
tracer?.traceTransitionDone(getStateName(plan.finalState));
|
|
720
832
|
}
|
|
721
833
|
}
|
|
722
834
|
export function createTransitionTracer(hsm) {
|
|
@@ -724,6 +836,12 @@ export function createTransitionTracer(hsm) {
|
|
|
724
836
|
traceTransitionStart(fromStateName, toStateName) {
|
|
725
837
|
hsm._tracePush(`transition from ${fromStateName} to ${toStateName}`, `started transition from ${fromStateName} to ${toStateName} `);
|
|
726
838
|
},
|
|
839
|
+
traceInitializeStart(stateName) {
|
|
840
|
+
hsm._tracePush(`initialize ${stateName}`, `started initialize drill-down from ${stateName}`);
|
|
841
|
+
},
|
|
842
|
+
traceInitializeDone(finalStateName) {
|
|
843
|
+
hsm._tracePopDone(`done initialize drill-down at ${finalStateName}`);
|
|
844
|
+
},
|
|
727
845
|
traceHookDone(stateName, hook) {
|
|
728
846
|
hsm._traceWrite(`${stateName}.${hook}() done`);
|
|
729
847
|
},
|
|
@@ -755,9 +873,15 @@ class RuntimeTransitionRoutine {
|
|
|
755
873
|
}
|
|
756
874
|
async execute(hsm, srcState, dstState) {
|
|
757
875
|
const style = hsm.traceLevel === TraceLevel.PRODUCTION ? 'production' : hsm.traceLevel === TraceLevel.DEBUG ? 'debug' : 'verbose';
|
|
876
|
+
const machine = hsm;
|
|
877
|
+
const instTracer = machine.instrumentation?.transition;
|
|
878
|
+
const tracer = instTracer ?? (style !== 'production' ? createTransitionTracer(hsm) : undefined);
|
|
758
879
|
await executeTransitionRoutine(hsm, hsm._instance, this.plan, srcState, dstState, {
|
|
759
880
|
style,
|
|
760
|
-
...(
|
|
881
|
+
...(tracer !== undefined ? { tracer } : {}),
|
|
882
|
+
// The instrumentation seam's entry/exit spans are structural (not TraceLevel-gated); the
|
|
883
|
+
// console tracer keeps its verbose-only gating.
|
|
884
|
+
hookEvents: instTracer !== undefined,
|
|
761
885
|
setCurrentState: state => {
|
|
762
886
|
hsm.currentState = state;
|
|
763
887
|
},
|
|
@@ -1350,42 +1474,60 @@ export function createInitTask(host, resolver) {
|
|
|
1350
1474
|
.then(() => executePendingTransition(host, resolver))
|
|
1351
1475
|
.then(() => done())
|
|
1352
1476
|
.catch((err) => {
|
|
1353
|
-
host.
|
|
1477
|
+
host.reportDispatchError(asError(err));
|
|
1354
1478
|
done();
|
|
1355
1479
|
});
|
|
1356
1480
|
};
|
|
1357
1481
|
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Run a dispatch body inside the ambient `dispatchContext` token so that any `notify`/`call`/timer
|
|
1484
|
+
* the handler issues can be attributed to the running `(macrostepId, stepSeq)` (CORE-B, §5.6.3).
|
|
1485
|
+
* Falls back to a plain run when no ALS is available (browser) or the host predates the seam.
|
|
1486
|
+
*/
|
|
1487
|
+
function runWithinDispatch(host, run) {
|
|
1488
|
+
const machine = host;
|
|
1489
|
+
const wants = machine.needsDispatchContext?.() ?? host.traceLevel !== TraceLevel.PRODUCTION;
|
|
1490
|
+
if (!wants) {
|
|
1491
|
+
run();
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const storage = dispatchContext.get();
|
|
1495
|
+
if (storage === undefined) {
|
|
1496
|
+
run();
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const token = machine.buildDispatchToken?.() ?? { machine: host };
|
|
1500
|
+
storage.run(token, run);
|
|
1501
|
+
}
|
|
1502
|
+
function currentPortCallToken() {
|
|
1503
|
+
const storage = portCallContext.get();
|
|
1504
|
+
return storage?.getStore();
|
|
1505
|
+
}
|
|
1358
1506
|
/** @internal */
|
|
1359
1507
|
export function createNotificationTask(host, resolver, name, args) {
|
|
1360
1508
|
const strategy = dispatchStrategyFor(host.traceLevel);
|
|
1361
1509
|
return (done) => {
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1510
|
+
runWithinDispatch(host, () => {
|
|
1511
|
+
void strategy
|
|
1512
|
+
.dispatchEvent(host, resolver, name, ...args)
|
|
1513
|
+
.catch((err) => host.reportDispatchError(asError(err)))
|
|
1514
|
+
.finally(() => done());
|
|
1515
|
+
});
|
|
1366
1516
|
};
|
|
1367
1517
|
}
|
|
1368
1518
|
/** @internal */
|
|
1369
1519
|
export function createServiceTask(host, resolver, name, args, resolve, reject) {
|
|
1370
|
-
const machine = host;
|
|
1371
1520
|
return (done) => {
|
|
1372
1521
|
const run = () => invokeHandler(host, resolver, name, args)
|
|
1373
1522
|
.then(resolve)
|
|
1374
1523
|
.catch((err) => {
|
|
1375
1524
|
reject(asError(err));
|
|
1376
1525
|
})
|
|
1377
|
-
.catch((err) => host.
|
|
1526
|
+
.catch((err) => host.reportDispatchError(asError(err)))
|
|
1378
1527
|
.finally(() => done());
|
|
1379
|
-
|
|
1528
|
+
runWithinDispatch(host, () => {
|
|
1380
1529
|
void run();
|
|
1381
|
-
|
|
1382
|
-
}
|
|
1383
|
-
const storage = dispatchContext.get();
|
|
1384
|
-
if (storage === undefined) {
|
|
1385
|
-
void run();
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
void storage.run({ machine }, run);
|
|
1530
|
+
});
|
|
1389
1531
|
};
|
|
1390
1532
|
}
|
|
1391
1533
|
//#endregion
|
|
@@ -1396,6 +1538,9 @@ export class HsmObject {
|
|
|
1396
1538
|
topStateName;
|
|
1397
1539
|
ctxTypeName;
|
|
1398
1540
|
traceWriter;
|
|
1541
|
+
actorUuid;
|
|
1542
|
+
actorName;
|
|
1543
|
+
actorPath;
|
|
1399
1544
|
/** @internal */
|
|
1400
1545
|
_instance;
|
|
1401
1546
|
/** @internal */
|
|
@@ -1410,7 +1555,9 @@ export class HsmObject {
|
|
|
1410
1555
|
dispatchErrorCallback;
|
|
1411
1556
|
_traceLevel;
|
|
1412
1557
|
_traceDomainStack;
|
|
1413
|
-
|
|
1558
|
+
_instrumentationHost;
|
|
1559
|
+
_drainWaiters = [];
|
|
1560
|
+
constructor(TopState, instance, traceWriter, traceLevel, dispatchErrorCallback, identity) {
|
|
1414
1561
|
this._instance = instance;
|
|
1415
1562
|
this._transitionState = undefined;
|
|
1416
1563
|
this._traceLevel = traceLevel;
|
|
@@ -1420,6 +1567,9 @@ export class HsmObject {
|
|
|
1420
1567
|
this._jobs = [];
|
|
1421
1568
|
this._hiPriorityJobs = [];
|
|
1422
1569
|
this._isRunning = false;
|
|
1570
|
+
this.actorUuid = identity.uuid;
|
|
1571
|
+
this.actorName = identity.name;
|
|
1572
|
+
this.actorPath = identity.path;
|
|
1423
1573
|
this.topState = TopState;
|
|
1424
1574
|
this.topStateName = getStateName(TopState);
|
|
1425
1575
|
this.ctxTypeName = Object.getPrototypeOf(instance.ctx).constructor.name;
|
|
@@ -1436,6 +1586,14 @@ export class HsmObject {
|
|
|
1436
1586
|
get port() {
|
|
1437
1587
|
return this._instance.portRef;
|
|
1438
1588
|
}
|
|
1589
|
+
/** The bound port when it provides a timer service, so service-call timeouts honour a virtual clock. */
|
|
1590
|
+
get callTimer() {
|
|
1591
|
+
const port = this._instance.portRef;
|
|
1592
|
+
if (port !== undefined && typeof port.setTimeout === 'function' && typeof port.clearTimeout === 'function') {
|
|
1593
|
+
return port;
|
|
1594
|
+
}
|
|
1595
|
+
return undefined;
|
|
1596
|
+
}
|
|
1439
1597
|
get eventName() {
|
|
1440
1598
|
return this._currentEventName ?? '';
|
|
1441
1599
|
}
|
|
@@ -1483,14 +1641,37 @@ export class HsmObject {
|
|
|
1483
1641
|
set traceLevel(traceLevel) {
|
|
1484
1642
|
this._traceLevel = traceLevel;
|
|
1485
1643
|
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Resolve when the actor next reaches stability (mailbox fully drained).
|
|
1646
|
+
*
|
|
1647
|
+
* The resolver fires at the queue-drain point — *after* {@link InstrumentationHost.onQueuesDrained}
|
|
1648
|
+
* — so the closing `macrostep.end` and the macrostep-boundary reset are observable before `sync()`
|
|
1649
|
+
* resolves, and a subsequent external stimulus deterministically starts its own macrostep. The
|
|
1650
|
+
* pushed task is a no-op (internal) whose only purpose is to guarantee a drain cycle occurs.
|
|
1651
|
+
*/
|
|
1486
1652
|
sync() {
|
|
1487
1653
|
return new Promise(resolve => {
|
|
1488
|
-
this.
|
|
1489
|
-
|
|
1654
|
+
this._drainWaiters.push(resolve);
|
|
1655
|
+
const task = (doneCallback) => {
|
|
1490
1656
|
doneCallback();
|
|
1491
|
-
}
|
|
1657
|
+
};
|
|
1658
|
+
setTaskMeta(task, { internal: true });
|
|
1659
|
+
this.pushTask(task);
|
|
1492
1660
|
});
|
|
1493
1661
|
}
|
|
1662
|
+
/** Invoke the user dispatch-error callback, first notifying instrumentation (pure observer). */
|
|
1663
|
+
reportDispatchError(err) {
|
|
1664
|
+
this._instrumentationHost?.onDispatchError(err);
|
|
1665
|
+
this.dispatchErrorCallback(this, err);
|
|
1666
|
+
}
|
|
1667
|
+
flushDrainWaiters() {
|
|
1668
|
+
if (this._drainWaiters.length === 0)
|
|
1669
|
+
return;
|
|
1670
|
+
const waiters = this._drainWaiters;
|
|
1671
|
+
this._drainWaiters = [];
|
|
1672
|
+
for (const resolve of waiters)
|
|
1673
|
+
resolve();
|
|
1674
|
+
}
|
|
1494
1675
|
pushTask(t) {
|
|
1495
1676
|
this.enqueueTask(t, this._jobs);
|
|
1496
1677
|
}
|
|
@@ -1518,6 +1699,8 @@ export class HsmObject {
|
|
|
1518
1699
|
dequeue() {
|
|
1519
1700
|
if (this._hiPriorityJobs.length == 0 && this._jobs.length == 0) {
|
|
1520
1701
|
this._isRunning = false;
|
|
1702
|
+
this._instrumentationHost?.onQueuesDrained();
|
|
1703
|
+
this.flushDrainWaiters();
|
|
1521
1704
|
return;
|
|
1522
1705
|
}
|
|
1523
1706
|
const task = this._hiPriorityJobs.length > 0 ? this._hiPriorityJobs.shift() : this._jobs.shift();
|
|
@@ -1527,10 +1710,32 @@ export class HsmObject {
|
|
|
1527
1710
|
setTimeout(() => this.runTask(task).then(() => this.dequeue()), 0);
|
|
1528
1711
|
}
|
|
1529
1712
|
runTask(task) {
|
|
1713
|
+
this._instrumentationHost?.onTaskBegin(task);
|
|
1714
|
+
let outcome = 'ok';
|
|
1530
1715
|
return new Promise(resolve => {
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1716
|
+
const runBody = () => {
|
|
1717
|
+
task(() => {
|
|
1718
|
+
this.drainHiPriority()
|
|
1719
|
+
.then(() => {
|
|
1720
|
+
this._instrumentationHost?.onTaskEnd(task, outcome);
|
|
1721
|
+
resolve();
|
|
1722
|
+
})
|
|
1723
|
+
.catch((_err) => {
|
|
1724
|
+
outcome = 'error';
|
|
1725
|
+
this._instrumentationHost?.onTaskEnd(task, outcome);
|
|
1726
|
+
resolve();
|
|
1727
|
+
});
|
|
1728
|
+
});
|
|
1729
|
+
};
|
|
1730
|
+
try {
|
|
1731
|
+
runBody();
|
|
1732
|
+
}
|
|
1733
|
+
catch (err) {
|
|
1734
|
+
outcome = 'error';
|
|
1735
|
+
this._instrumentationHost?.onTaskEnd(task, outcome);
|
|
1736
|
+
resolve();
|
|
1737
|
+
throw asError(err);
|
|
1738
|
+
}
|
|
1534
1739
|
});
|
|
1535
1740
|
}
|
|
1536
1741
|
drainHiPriority() {
|
|
@@ -1558,18 +1763,51 @@ export class HsmObject {
|
|
|
1558
1763
|
get traceHeader() {
|
|
1559
1764
|
return `${this._traceDomainStack.length === 0 ? '' : this._traceDomainStack.join('|') + '|'}`;
|
|
1560
1765
|
}
|
|
1766
|
+
get traceFrames() {
|
|
1767
|
+
return this._traceDomainStack.map(name => ({ name, kind: classifyFrameKind(name) }));
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
/** Best-effort classification of a live trace-domain name into a structured {@link TraceFrame.kind}. */
|
|
1771
|
+
function classifyFrameKind(name) {
|
|
1772
|
+
if (name.startsWith('#'))
|
|
1773
|
+
return 'event';
|
|
1774
|
+
if (name === 'execute')
|
|
1775
|
+
return 'handler';
|
|
1776
|
+
if (name === 'initialize' || name.startsWith('initialize'))
|
|
1777
|
+
return 'initialize';
|
|
1778
|
+
if (name.startsWith('transition'))
|
|
1779
|
+
return 'transition';
|
|
1780
|
+
if (name.includes('onEntry'))
|
|
1781
|
+
return 'onEntry';
|
|
1782
|
+
if (name.includes('onExit'))
|
|
1783
|
+
return 'onExit';
|
|
1784
|
+
return 'handler';
|
|
1561
1785
|
}
|
|
1562
1786
|
//#region machine
|
|
1563
1787
|
export class Machine extends HsmObject {
|
|
1564
1788
|
transitionResolver;
|
|
1789
|
+
identity;
|
|
1790
|
+
instrumentation;
|
|
1565
1791
|
_dispatchStrategy;
|
|
1566
1792
|
protocolIndex;
|
|
1567
1793
|
handlerFacade;
|
|
1568
1794
|
selfActor;
|
|
1569
1795
|
selfImmediate;
|
|
1570
1796
|
actorFacades = new Map();
|
|
1571
|
-
|
|
1572
|
-
|
|
1797
|
+
_macrostepCounter = 0;
|
|
1798
|
+
_currentMacrostep;
|
|
1799
|
+
_microstepFromState;
|
|
1800
|
+
_childSpawnCounters = new Map();
|
|
1801
|
+
_nextTraceCallId = 0;
|
|
1802
|
+
_proxiedPort;
|
|
1803
|
+
constructor(topState, instance, protocolIndex, traceWriter, traceLevel, dispatchErrorCallback, initialize, identity, instrumentation, transitionResolver) {
|
|
1804
|
+
super(topState, instance, traceWriter, traceLevel, dispatchErrorCallback, identity);
|
|
1805
|
+
this.identity = identity;
|
|
1806
|
+
this.instrumentation = instrumentation;
|
|
1807
|
+
if (instrumentation !== undefined) {
|
|
1808
|
+
this._instrumentationHost = this;
|
|
1809
|
+
notifyActorCreated(instrumentation, identity);
|
|
1810
|
+
}
|
|
1573
1811
|
Object.defineProperty(instance, kHandlerMachine, { value: this, enumerable: false, writable: false, configurable: false });
|
|
1574
1812
|
this.protocolIndex = protocolIndex;
|
|
1575
1813
|
cacheProtocolIndex(topState, protocolIndex);
|
|
@@ -1583,7 +1821,227 @@ export class Machine extends HsmObject {
|
|
|
1583
1821
|
Object.defineProperty(instance, 'notifyNow', { value: this.selfImmediate, enumerable: true, configurable: true });
|
|
1584
1822
|
this.bindPort(instance.portRef);
|
|
1585
1823
|
if (initialize) {
|
|
1586
|
-
|
|
1824
|
+
const initTask = createInitTask(this, this.transitionResolver);
|
|
1825
|
+
setTaskMeta(initTask, { event: 'initialize', queue: 'default', triggerKind: 'init', internal: false });
|
|
1826
|
+
this.pushTask(initTask);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
allocateChildSpawnIndex(childTopName) {
|
|
1830
|
+
const childName = actorNameFromTopState(childTopName);
|
|
1831
|
+
const index = this._childSpawnCounters.get(childName) ?? 0;
|
|
1832
|
+
this._childSpawnCounters.set(childName, index + 1);
|
|
1833
|
+
return index;
|
|
1834
|
+
}
|
|
1835
|
+
needsDispatchContext() {
|
|
1836
|
+
return this.instrumentation !== undefined || this.traceLevel !== TraceLevel.PRODUCTION;
|
|
1837
|
+
}
|
|
1838
|
+
buildDispatchToken() {
|
|
1839
|
+
return {
|
|
1840
|
+
machine: this,
|
|
1841
|
+
actorUuid: this.actorUuid,
|
|
1842
|
+
macrostepId: this._currentMacrostep?.id,
|
|
1843
|
+
stepSeq: this._currentMacrostep?.stepSeq,
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
readDispatchCause(kind) {
|
|
1847
|
+
const storage = dispatchContext.get();
|
|
1848
|
+
const token = storage?.getStore();
|
|
1849
|
+
if (token?.actorUuid === undefined)
|
|
1850
|
+
return undefined;
|
|
1851
|
+
return {
|
|
1852
|
+
actorUuid: token.actorUuid,
|
|
1853
|
+
macrostepId: token.macrostepId,
|
|
1854
|
+
stepSeq: token.stepSeq,
|
|
1855
|
+
kind,
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
nextTraceCallId() {
|
|
1859
|
+
this._nextTraceCallId += 1;
|
|
1860
|
+
return this._nextTraceCallId;
|
|
1861
|
+
}
|
|
1862
|
+
beginPortCall(method) {
|
|
1863
|
+
if (this.instrumentation === undefined)
|
|
1864
|
+
return undefined;
|
|
1865
|
+
const cause = this.readDispatchCause('wire');
|
|
1866
|
+
const info = {
|
|
1867
|
+
callId: this.nextTraceCallId(),
|
|
1868
|
+
method,
|
|
1869
|
+
cause,
|
|
1870
|
+
};
|
|
1871
|
+
notifyPortCallBegin(this.instrumentation, info);
|
|
1872
|
+
return info;
|
|
1873
|
+
}
|
|
1874
|
+
endPortCall(begin, outcome, error) {
|
|
1875
|
+
if (this.instrumentation === undefined || begin === undefined)
|
|
1876
|
+
return;
|
|
1877
|
+
const info = {
|
|
1878
|
+
callId: begin.callId,
|
|
1879
|
+
method: begin.method,
|
|
1880
|
+
outcome,
|
|
1881
|
+
error,
|
|
1882
|
+
};
|
|
1883
|
+
notifyPortCallEnd(this.instrumentation, info);
|
|
1884
|
+
}
|
|
1885
|
+
beginOutboundCall(service, targetUuid) {
|
|
1886
|
+
if (this.instrumentation === undefined)
|
|
1887
|
+
return undefined;
|
|
1888
|
+
const portToken = currentPortCallToken();
|
|
1889
|
+
const cause = this.readDispatchCause('wire') ?? portToken?.cause;
|
|
1890
|
+
const info = {
|
|
1891
|
+
callId: this.nextTraceCallId(),
|
|
1892
|
+
service,
|
|
1893
|
+
targetUuid,
|
|
1894
|
+
cause,
|
|
1895
|
+
};
|
|
1896
|
+
notifyOutboundCallBegin(this.instrumentation, info);
|
|
1897
|
+
return info;
|
|
1898
|
+
}
|
|
1899
|
+
endOutboundCall(begin, outcome, error) {
|
|
1900
|
+
if (this.instrumentation === undefined || begin === undefined)
|
|
1901
|
+
return;
|
|
1902
|
+
const info = {
|
|
1903
|
+
callId: begin.callId,
|
|
1904
|
+
service: begin.service,
|
|
1905
|
+
outcome,
|
|
1906
|
+
error,
|
|
1907
|
+
};
|
|
1908
|
+
notifyOutboundCallEnd(this.instrumentation, info);
|
|
1909
|
+
}
|
|
1910
|
+
slotBucket(eventName) {
|
|
1911
|
+
return this.protocolIndex.get(eventName)?.bucket ?? 'notifications';
|
|
1912
|
+
}
|
|
1913
|
+
onTaskBegin(task) {
|
|
1914
|
+
const meta = getTaskMeta(task);
|
|
1915
|
+
if (meta?.internal === true || this.instrumentation === undefined)
|
|
1916
|
+
return;
|
|
1917
|
+
if (this._currentMacrostep === undefined) {
|
|
1918
|
+
const id = `${this.actorUuid}:${++this._macrostepCounter}`;
|
|
1919
|
+
const trigger = meta?.event ?? 'unknown';
|
|
1920
|
+
const triggerKind = meta?.triggerKind ?? (this._jobs.length + this._hiPriorityJobs.length > 0 ? 'self' : 'external');
|
|
1921
|
+
this._currentMacrostep = {
|
|
1922
|
+
id,
|
|
1923
|
+
trigger,
|
|
1924
|
+
triggerKind,
|
|
1925
|
+
startState: this.currentStateName,
|
|
1926
|
+
stepSeq: -1,
|
|
1927
|
+
transitioned: false,
|
|
1928
|
+
outcome: 'ok',
|
|
1929
|
+
cause: meta?.cause,
|
|
1930
|
+
};
|
|
1931
|
+
notifyMacrostepBegin(this.instrumentation, {
|
|
1932
|
+
id,
|
|
1933
|
+
actor: this.identity,
|
|
1934
|
+
trigger,
|
|
1935
|
+
triggerKind,
|
|
1936
|
+
startState: this.currentStateName,
|
|
1937
|
+
cause: meta?.cause,
|
|
1938
|
+
delayMs: meta?.delayMs,
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
const macrostep = this._currentMacrostep;
|
|
1942
|
+
macrostep.stepSeq += 1;
|
|
1943
|
+
const seq = macrostep.stepSeq;
|
|
1944
|
+
const fromState = this.currentStateName;
|
|
1945
|
+
this._microstepFromState = fromState;
|
|
1946
|
+
// Stamp seq + fromState on the task so onTaskEnd pairs correctly even when nested priority
|
|
1947
|
+
// drains mutate the shared macrostep counter between this task's begin and end.
|
|
1948
|
+
if (meta !== undefined) {
|
|
1949
|
+
meta.seq = seq;
|
|
1950
|
+
meta.fromState = fromState;
|
|
1951
|
+
}
|
|
1952
|
+
else {
|
|
1953
|
+
setTaskMeta(task, { seq, fromState });
|
|
1954
|
+
}
|
|
1955
|
+
const eventName = meta?.event ?? this.eventName ?? 'unknown';
|
|
1956
|
+
const handlerState = lookupHandlerState(this, eventName);
|
|
1957
|
+
const storage = dispatchContext.get();
|
|
1958
|
+
const runMicrostep = () => {
|
|
1959
|
+
notifyMicrostepBegin(this.instrumentation, {
|
|
1960
|
+
macrostepId: macrostep.id,
|
|
1961
|
+
seq,
|
|
1962
|
+
event: eventName,
|
|
1963
|
+
bucket: this.slotBucket(eventName),
|
|
1964
|
+
queue: meta?.queue ?? 'default',
|
|
1965
|
+
fromState: this.currentStateName,
|
|
1966
|
+
handlerState,
|
|
1967
|
+
cause: meta?.cause,
|
|
1968
|
+
});
|
|
1969
|
+
};
|
|
1970
|
+
if (storage !== undefined && this.needsDispatchContext()) {
|
|
1971
|
+
storage.run(this.buildDispatchToken(), runMicrostep);
|
|
1972
|
+
}
|
|
1973
|
+
else {
|
|
1974
|
+
runMicrostep();
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
onTaskEnd(task, outcome) {
|
|
1978
|
+
const meta = getTaskMeta(task);
|
|
1979
|
+
if (meta?.internal === true || this.instrumentation === undefined || this._currentMacrostep === undefined)
|
|
1980
|
+
return;
|
|
1981
|
+
const macrostep = this._currentMacrostep;
|
|
1982
|
+
const fromState = meta?.fromState ?? this._microstepFromState ?? macrostep.startState;
|
|
1983
|
+
const seq = meta?.seq ?? macrostep.stepSeq;
|
|
1984
|
+
const transitioned = this.currentStateName !== fromState;
|
|
1985
|
+
if (transitioned)
|
|
1986
|
+
macrostep.transitioned = true;
|
|
1987
|
+
if (outcome === 'error')
|
|
1988
|
+
macrostep.outcome = 'error';
|
|
1989
|
+
notifyMicrostepEnd(this.instrumentation, {
|
|
1990
|
+
macrostepId: macrostep.id,
|
|
1991
|
+
seq,
|
|
1992
|
+
toState: this.currentStateName,
|
|
1993
|
+
transitioned,
|
|
1994
|
+
async: false,
|
|
1995
|
+
outcome,
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
onDispatchError(err) {
|
|
1999
|
+
if (this.instrumentation === undefined)
|
|
2000
|
+
return;
|
|
2001
|
+
notifyError(this.instrumentation, {
|
|
2002
|
+
phase: errorPhaseFromError(err),
|
|
2003
|
+
errorClass: err.name,
|
|
2004
|
+
error: err,
|
|
2005
|
+
recovered: false,
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
onQueuesDrained() {
|
|
2009
|
+
if (this.instrumentation === undefined || this._currentMacrostep === undefined)
|
|
2010
|
+
return;
|
|
2011
|
+
const macrostep = this._currentMacrostep;
|
|
2012
|
+
notifyMacrostepEnd(this.instrumentation, {
|
|
2013
|
+
id: macrostep.id,
|
|
2014
|
+
endState: this.currentStateName,
|
|
2015
|
+
steps: macrostep.stepSeq + 1,
|
|
2016
|
+
transitioned: macrostep.transitioned,
|
|
2017
|
+
outcome: macrostep.outcome,
|
|
2018
|
+
});
|
|
2019
|
+
this._currentMacrostep = undefined;
|
|
2020
|
+
}
|
|
2021
|
+
resolveEnqueueCause() {
|
|
2022
|
+
const inherited = this.readDispatchCause('message');
|
|
2023
|
+
if (inherited === undefined) {
|
|
2024
|
+
return { actorUuid: this.actorUuid, kind: 'cause' };
|
|
2025
|
+
}
|
|
2026
|
+
if (inherited.actorUuid !== this.actorUuid) {
|
|
2027
|
+
return inherited;
|
|
2028
|
+
}
|
|
2029
|
+
return { ...inherited, kind: 'cause' };
|
|
2030
|
+
}
|
|
2031
|
+
enqueueWithInstrumentation(task, event, queue, triggerKind) {
|
|
2032
|
+
const cause = this.resolveEnqueueCause();
|
|
2033
|
+
setTaskMeta(task, { event, queue, cause, triggerKind });
|
|
2034
|
+
notifyEnqueue(this.instrumentation, {
|
|
2035
|
+
event,
|
|
2036
|
+
queue,
|
|
2037
|
+
cause,
|
|
2038
|
+
targetUuid: this.actorUuid,
|
|
2039
|
+
});
|
|
2040
|
+
if (queue === 'priority') {
|
|
2041
|
+
this.pushHiPriorityTask(task);
|
|
2042
|
+
}
|
|
2043
|
+
else {
|
|
2044
|
+
this.pushTask(task);
|
|
1587
2045
|
}
|
|
1588
2046
|
}
|
|
1589
2047
|
get traceLevel() {
|
|
@@ -1603,12 +2061,22 @@ export class Machine extends HsmObject {
|
|
|
1603
2061
|
}
|
|
1604
2062
|
this.recordObserverEvent(name, args);
|
|
1605
2063
|
return new Promise((resolve, reject) => {
|
|
1606
|
-
|
|
2064
|
+
const task = createServiceTask(this, this.transitionResolver, name, args, resolve, reject);
|
|
2065
|
+
if (this.instrumentation !== undefined) {
|
|
2066
|
+
this.enqueueWithInstrumentation(task, name, 'default', 'call');
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
this.pushTask(task);
|
|
1607
2070
|
});
|
|
1608
2071
|
}
|
|
1609
2072
|
dispatchNotification(name, args, queue) {
|
|
1610
2073
|
this.recordObserverEvent(name, args);
|
|
1611
2074
|
const task = createNotificationTask(this, this.transitionResolver, name, args);
|
|
2075
|
+
if (this.instrumentation !== undefined) {
|
|
2076
|
+
const triggerKind = this._currentMacrostep === undefined ? 'external' : 'self';
|
|
2077
|
+
this.enqueueWithInstrumentation(task, name, queue, triggerKind);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
1612
2080
|
if (queue === 'priority') {
|
|
1613
2081
|
this.pushHiPriorityTask(task);
|
|
1614
2082
|
}
|
|
@@ -1625,14 +2093,60 @@ export class Machine extends HsmObject {
|
|
|
1625
2093
|
return facade;
|
|
1626
2094
|
}
|
|
1627
2095
|
scheduleNotification(ms, name, args) {
|
|
1628
|
-
const enqueue = () => {
|
|
1629
|
-
this.dispatchNotification(name, args, 'default');
|
|
1630
|
-
};
|
|
1631
2096
|
const port = this._instance.portRef;
|
|
1632
2097
|
if (port === undefined) {
|
|
1633
2098
|
throw new Error('ihsm: deferred notification requires a port');
|
|
1634
2099
|
}
|
|
1635
|
-
|
|
2100
|
+
if (this.instrumentation === undefined) {
|
|
2101
|
+
port.setTimeout(() => this.dispatchNotification(name, args, 'default'), ms);
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
// Capture the arming step's dispatch token now (the timer fires later while the actor is idle,
|
|
2105
|
+
// so the ambient token is gone by then). The fired macrostep links back to it as `timer` (§5.3.1).
|
|
2106
|
+
const armed = this.readDispatchCause('timer');
|
|
2107
|
+
const cause = armed ?? { actorUuid: this.actorUuid, kind: 'timer' };
|
|
2108
|
+
port.setTimeout(() => this.enqueueTimerNotification(name, args, cause, ms), ms);
|
|
2109
|
+
}
|
|
2110
|
+
enqueueTimerNotification(name, args, cause, delayMs) {
|
|
2111
|
+
this.recordObserverEvent(name, args);
|
|
2112
|
+
const task = createNotificationTask(this, this.transitionResolver, name, args);
|
|
2113
|
+
setTaskMeta(task, { event: name, queue: 'default', cause, triggerKind: 'timer', delayMs });
|
|
2114
|
+
notifyEnqueue(this.instrumentation, { event: name, queue: 'default', cause, delayMs, targetUuid: this.actorUuid });
|
|
2115
|
+
this.pushTask(task);
|
|
2116
|
+
}
|
|
2117
|
+
_actorLogger;
|
|
2118
|
+
/** Severity-typed handler logger surfaced as `this.hsm.log.*` (CORE-F, §4.10.1). */
|
|
2119
|
+
get logger() {
|
|
2120
|
+
if (this._actorLogger === undefined) {
|
|
2121
|
+
const emit = (severity, message, attributes) => this.emitUserLog(severity, message, attributes);
|
|
2122
|
+
this._actorLogger = {
|
|
2123
|
+
trace: (m, a) => emit('trace', m, a),
|
|
2124
|
+
debug: (m, a) => emit('debug', m, a),
|
|
2125
|
+
info: (m, a) => emit('info', m, a),
|
|
2126
|
+
warn: (m, a) => emit('warn', m, a),
|
|
2127
|
+
error: (m, a) => emit('error', m, a),
|
|
2128
|
+
fatal: (m, a) => emit('fatal', m, a),
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
return this._actorLogger;
|
|
2132
|
+
}
|
|
2133
|
+
emitUserLog(severity, message, attributes) {
|
|
2134
|
+
const isError = message instanceof Error;
|
|
2135
|
+
const text = isError ? message.message : message;
|
|
2136
|
+
const body = `${this.traceHeader}${this.currentStateName}: ${text}`;
|
|
2137
|
+
// User logs fire on intent and are never TraceLevel-gated; mirror to the TraceWriter for console.
|
|
2138
|
+
this.traceWriter.write(this, body);
|
|
2139
|
+
if (this.instrumentation === undefined)
|
|
2140
|
+
return;
|
|
2141
|
+
const record = {
|
|
2142
|
+
severity,
|
|
2143
|
+
body,
|
|
2144
|
+
attributes,
|
|
2145
|
+
frames: this.traceFrames,
|
|
2146
|
+
error: isError ? message : undefined,
|
|
2147
|
+
source: 'user',
|
|
2148
|
+
};
|
|
2149
|
+
notifyLog(this.instrumentation, record);
|
|
1636
2150
|
}
|
|
1637
2151
|
/** @internal Binds deferred self-notifications to a port instance. */
|
|
1638
2152
|
bindPort(portRef) {
|
|
@@ -1648,7 +2162,50 @@ export class Machine extends HsmObject {
|
|
|
1648
2162
|
},
|
|
1649
2163
|
transition: next => machine.transition(next),
|
|
1650
2164
|
get port() {
|
|
1651
|
-
|
|
2165
|
+
const rawPort = instance.portRef;
|
|
2166
|
+
if (machine.instrumentation === undefined || rawPort === undefined || typeof rawPort !== 'object') {
|
|
2167
|
+
return rawPort;
|
|
2168
|
+
}
|
|
2169
|
+
if (machine._proxiedPort !== undefined)
|
|
2170
|
+
return machine._proxiedPort;
|
|
2171
|
+
const proxy = new Proxy(rawPort, {
|
|
2172
|
+
get(target, prop, receiver) {
|
|
2173
|
+
const value = Reflect.get(target, prop, receiver);
|
|
2174
|
+
if (typeof value !== 'function')
|
|
2175
|
+
return value;
|
|
2176
|
+
const method = String(prop);
|
|
2177
|
+
return (...args) => {
|
|
2178
|
+
const begin = machine.beginPortCall(method);
|
|
2179
|
+
if (begin === undefined) {
|
|
2180
|
+
return Reflect.apply(value, target, args);
|
|
2181
|
+
}
|
|
2182
|
+
const call = () => Reflect.apply(value, target, args);
|
|
2183
|
+
const storage = portCallContext.get();
|
|
2184
|
+
try {
|
|
2185
|
+
const result = storage !== undefined ? storage.run({ machine, callId: begin.callId, method, cause: begin.cause }, call) : call();
|
|
2186
|
+
if (result instanceof Promise) {
|
|
2187
|
+
return result.then((value) => {
|
|
2188
|
+
machine.endPortCall(begin, 'ok');
|
|
2189
|
+
return value;
|
|
2190
|
+
}, (cause) => {
|
|
2191
|
+
const err = asError(cause);
|
|
2192
|
+
machine.endPortCall(begin, 'error', err);
|
|
2193
|
+
throw err;
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
machine.endPortCall(begin, 'ok');
|
|
2197
|
+
return result;
|
|
2198
|
+
}
|
|
2199
|
+
catch (cause) {
|
|
2200
|
+
const err = asError(cause);
|
|
2201
|
+
machine.endPortCall(begin, 'error', err);
|
|
2202
|
+
throw err;
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
},
|
|
2206
|
+
});
|
|
2207
|
+
machine._proxiedPort = proxy;
|
|
2208
|
+
return proxy;
|
|
1652
2209
|
},
|
|
1653
2210
|
unhandled: () => machine.unhandled(),
|
|
1654
2211
|
get eventName() {
|
|
@@ -1672,6 +2229,24 @@ export class Machine extends HsmObject {
|
|
|
1672
2229
|
get traceHeader() {
|
|
1673
2230
|
return machine.traceHeader;
|
|
1674
2231
|
},
|
|
2232
|
+
get traceFrames() {
|
|
2233
|
+
return machine.traceFrames;
|
|
2234
|
+
},
|
|
2235
|
+
get log() {
|
|
2236
|
+
return machine.logger;
|
|
2237
|
+
},
|
|
2238
|
+
get id() {
|
|
2239
|
+
return machine.actorUuid;
|
|
2240
|
+
},
|
|
2241
|
+
get actorUuid() {
|
|
2242
|
+
return machine.actorUuid;
|
|
2243
|
+
},
|
|
2244
|
+
get actorName() {
|
|
2245
|
+
return machine.actorName;
|
|
2246
|
+
},
|
|
2247
|
+
get actorPath() {
|
|
2248
|
+
return machine.actorPath;
|
|
2249
|
+
},
|
|
1675
2250
|
get traceLevel() {
|
|
1676
2251
|
return machine.traceLevel;
|
|
1677
2252
|
},
|
|
@@ -1739,6 +2314,18 @@ export class Machine extends HsmObject {
|
|
|
1739
2314
|
get traceHeader() {
|
|
1740
2315
|
return machine.traceHeader;
|
|
1741
2316
|
},
|
|
2317
|
+
get id() {
|
|
2318
|
+
return machine.actorUuid;
|
|
2319
|
+
},
|
|
2320
|
+
get actorUuid() {
|
|
2321
|
+
return machine.actorUuid;
|
|
2322
|
+
},
|
|
2323
|
+
get actorName() {
|
|
2324
|
+
return machine.actorName;
|
|
2325
|
+
},
|
|
2326
|
+
get actorPath() {
|
|
2327
|
+
return machine.actorPath;
|
|
2328
|
+
},
|
|
1742
2329
|
};
|
|
1743
2330
|
if (includeState) {
|
|
1744
2331
|
Object.defineProperties(facade, {
|
|
@@ -1808,9 +2395,16 @@ export function defaultDispatchErrorCallback(hsm, err) {
|
|
|
1808
2395
|
throw err;
|
|
1809
2396
|
}
|
|
1810
2397
|
/** @internal Spawn with embodiment kind — used by factories and `ihsm/testing`. */
|
|
1811
|
-
export function spawnActor(kind, topState, ctx, port, options) {
|
|
2398
|
+
export function spawnActor(kind, topState, ctx, port, options, spawnContext = {}) {
|
|
1812
2399
|
const { initialize = defaultInitialize, traceLevel = TraceLevel.DEBUG, traceWriter = defaultTraceWriter, dispatchErrorCallback = defaultDispatchErrorCallback, transitions } = options;
|
|
1813
2400
|
const protocolIndex = buildProtocolIndex(topState);
|
|
2401
|
+
const topStateName = getStateName(topState);
|
|
2402
|
+
const parentMachine = spawnContext.parentMachine;
|
|
2403
|
+
// Tracing is a cross-cutting concern: the actor adopts the globally-registered collector(s) at
|
|
2404
|
+
// spawn (no per-actor `instrumentation` option). Snapshotting here keeps "no collector" actors
|
|
2405
|
+
// at zero overhead while still sharing one collector instance across a parent and its children.
|
|
2406
|
+
const resolvedInstrumentation = getActiveInstrumentation();
|
|
2407
|
+
const identity = parentMachine !== undefined ? mintActorIdentity('child', childActorPath(parentMachine.actorPath, topStateName, parentMachine.allocateChildSpawnIndex(topStateName)), parentMachine.actorUuid) : mintActorIdentity(kind, rootActorPath(topStateName));
|
|
1814
2408
|
const boundPort = (port ?? new Port());
|
|
1815
2409
|
const instance = {
|
|
1816
2410
|
ctx,
|
|
@@ -1818,7 +2412,26 @@ export function spawnActor(kind, topState, ctx, port, options) {
|
|
|
1818
2412
|
portRef: boundPort,
|
|
1819
2413
|
};
|
|
1820
2414
|
Object.setPrototypeOf(instance, topState.prototype);
|
|
1821
|
-
const machine = new Machine(topState, instance, protocolIndex, traceWriter, traceLevel, dispatchErrorCallback, initialize, transitions ?? new RuntimeTransitionResolver());
|
|
2415
|
+
const machine = new Machine(topState, instance, protocolIndex, traceWriter, traceLevel, dispatchErrorCallback, initialize, identity, resolvedInstrumentation, transitions ?? new RuntimeTransitionResolver());
|
|
2416
|
+
if (resolvedInstrumentation !== undefined) {
|
|
2417
|
+
const token = dispatchContext.get()?.getStore();
|
|
2418
|
+
const parentCause = token?.actorUuid !== undefined
|
|
2419
|
+
? {
|
|
2420
|
+
actorUuid: token.actorUuid,
|
|
2421
|
+
macrostepId: token.macrostepId,
|
|
2422
|
+
stepSeq: token.stepSeq,
|
|
2423
|
+
kind: 'spawn',
|
|
2424
|
+
}
|
|
2425
|
+
: {
|
|
2426
|
+
actorUuid: identity.parentUuid ?? identity.uuid,
|
|
2427
|
+
kind: 'spawn',
|
|
2428
|
+
};
|
|
2429
|
+
const spawnInfo = {
|
|
2430
|
+
parent: parentCause,
|
|
2431
|
+
child: identity,
|
|
2432
|
+
};
|
|
2433
|
+
notifyActorSpawned(resolvedInstrumentation, spawnInfo);
|
|
2434
|
+
}
|
|
1822
2435
|
const portKind = isRequestingPort(boundPort) ? 'child' : kind === 'root' ? 'inbound' : kind;
|
|
1823
2436
|
boundPort.actor = createActorHandle(machine, topState, protocolIndex, portKind);
|
|
1824
2437
|
return createActorHandle(machine, topState, protocolIndex, kind);
|
|
@@ -1839,9 +2452,11 @@ export function asParentActor(handler) {
|
|
|
1839
2452
|
}
|
|
1840
2453
|
/** Parent composes a child machine — returns full child protocol shell with `parent` set. */
|
|
1841
2454
|
export function makeChildActor(parent, childTop, childCtx, port, options = {}) {
|
|
1842
|
-
const
|
|
2455
|
+
const parentMachine = parent[kParentLink];
|
|
2456
|
+
const child = spawnActor('child', childTop, childCtx, port, options, { parentMachine: parentMachine });
|
|
1843
2457
|
Object.defineProperty(child, 'parent', { value: parent, enumerable: true, writable: false, configurable: true });
|
|
1844
2458
|
return child;
|
|
1845
2459
|
}
|
|
1846
2460
|
export { kHandlerMachine, kParentLink } from './types.js';
|
|
2461
|
+
export { configureRunSeed, getRunSeed, getRunNamespace, actorNameFromTopState, mintActorIdentity, rootActorPath, childActorPath } from './identity.js';
|
|
1847
2462
|
//# sourceMappingURL=runtime.js.map
|