@camcima/finita 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +247 -212
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -31
- package/dist/index.d.ts +113 -31
- package/dist/index.js +246 -212
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/dist/index.cjs
CHANGED
|
@@ -52,6 +52,7 @@ __export(index_exports, {
|
|
|
52
52
|
ProcessBuilder: () => ProcessBuilder,
|
|
53
53
|
ProcessFinalizedError: () => ProcessFinalizedError,
|
|
54
54
|
ProcessNotFoundError: () => ProcessNotFoundError,
|
|
55
|
+
ReentrancyError: () => ReentrancyError,
|
|
55
56
|
ScoreTransition: () => ScoreTransition,
|
|
56
57
|
SingleProcessDetector: () => SingleProcessDetector,
|
|
57
58
|
State: () => State,
|
|
@@ -74,23 +75,22 @@ var Event = class {
|
|
|
74
75
|
name;
|
|
75
76
|
observers = /* @__PURE__ */ new Set();
|
|
76
77
|
metadata = /* @__PURE__ */ new Map();
|
|
77
|
-
invokeArgs = [];
|
|
78
78
|
constructor(name) {
|
|
79
79
|
this.name = name;
|
|
80
80
|
}
|
|
81
81
|
getName() {
|
|
82
82
|
return this.name;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* @deprecated Always returns []. Invoke args are now passed directly to
|
|
86
|
+
* Observer.update — reading them from the event was racy when one
|
|
87
|
+
* Process served multiple Statemachines.
|
|
88
|
+
*/
|
|
84
89
|
getInvokeArgs() {
|
|
85
|
-
return
|
|
90
|
+
return [];
|
|
86
91
|
}
|
|
87
92
|
async invoke(...args) {
|
|
88
|
-
this.
|
|
89
|
-
try {
|
|
90
|
-
await this.notify();
|
|
91
|
-
} finally {
|
|
92
|
-
this.invokeArgs = [];
|
|
93
|
-
}
|
|
93
|
+
await this.notify(args);
|
|
94
94
|
}
|
|
95
95
|
attach(observer) {
|
|
96
96
|
this.observers.add(observer);
|
|
@@ -98,9 +98,9 @@ var Event = class {
|
|
|
98
98
|
detach(observer) {
|
|
99
99
|
this.observers.delete(observer);
|
|
100
100
|
}
|
|
101
|
-
async notify() {
|
|
102
|
-
for (const observer of this.observers) {
|
|
103
|
-
await observer.update(this);
|
|
101
|
+
async notify(args) {
|
|
102
|
+
for (const observer of [...this.observers]) {
|
|
103
|
+
await observer.update(this, args);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
getObservers() {
|
|
@@ -382,8 +382,9 @@ var DuplicateTransitionError = class extends FinitaError {
|
|
|
382
382
|
const eventLabel = conflict.eventName ?? "<automatic>";
|
|
383
383
|
const existing = conflict.existingConditionName ?? "<no condition>";
|
|
384
384
|
const incoming = conflict.newConditionName ?? "<no condition>";
|
|
385
|
+
const weightInfo = conflict.existingWeight !== void 0 && conflict.newWeight !== void 0 && conflict.existingWeight !== conflict.newWeight ? `, existing weight ${conflict.existingWeight} vs new weight ${conflict.newWeight}` : "";
|
|
385
386
|
super(
|
|
386
|
-
`Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"`
|
|
387
|
+
`Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"${weightInfo}`
|
|
387
388
|
);
|
|
388
389
|
this.name = "DuplicateTransitionError";
|
|
389
390
|
this.conflict = Object.freeze({ ...conflict });
|
|
@@ -391,7 +392,7 @@ var DuplicateTransitionError = class extends FinitaError {
|
|
|
391
392
|
};
|
|
392
393
|
|
|
393
394
|
// src/ProcessBuilder.ts
|
|
394
|
-
var ProcessBuilder = class {
|
|
395
|
+
var ProcessBuilder = class _ProcessBuilder {
|
|
395
396
|
processName;
|
|
396
397
|
stateSpecs = /* @__PURE__ */ new Map();
|
|
397
398
|
transitionSpecs = [];
|
|
@@ -406,6 +407,9 @@ var ProcessBuilder = class {
|
|
|
406
407
|
if (this.stateSpecs.has(name)) {
|
|
407
408
|
throw new DuplicateStateError(name);
|
|
408
409
|
}
|
|
410
|
+
this.validateName("invalidStateName", name, `addState("${name}")`, {
|
|
411
|
+
stateName: name
|
|
412
|
+
});
|
|
409
413
|
this.stateSpecs.set(name, {
|
|
410
414
|
name,
|
|
411
415
|
initial: options.initial === true,
|
|
@@ -419,25 +423,22 @@ var ProcessBuilder = class {
|
|
|
419
423
|
}
|
|
420
424
|
let eventName = null;
|
|
421
425
|
if (options.event !== void 0) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
eventName = raw;
|
|
426
|
+
this.validateName(
|
|
427
|
+
"invalidEventName",
|
|
428
|
+
options.event,
|
|
429
|
+
`addTransition called with an invalid event name from "${fromState}" to "${toState}"`,
|
|
430
|
+
{ fromState, toState, eventName: options.event }
|
|
431
|
+
);
|
|
432
|
+
eventName = options.event;
|
|
431
433
|
}
|
|
432
434
|
if (options.condition) {
|
|
433
435
|
const conditionName = options.condition.getName();
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
436
|
+
this.validateName(
|
|
437
|
+
"invalidConditionName",
|
|
438
|
+
conditionName,
|
|
439
|
+
`addTransition called with an invalid condition name from "${fromState}" to "${toState}"`,
|
|
440
|
+
{ fromState, toState, conditionName }
|
|
441
|
+
);
|
|
441
442
|
}
|
|
442
443
|
this.transitionSpecs.push({
|
|
443
444
|
fromState,
|
|
@@ -471,6 +472,16 @@ var ProcessBuilder = class {
|
|
|
471
472
|
);
|
|
472
473
|
}
|
|
473
474
|
// --- private helpers ---
|
|
475
|
+
/** One name rule for every named entity: non-empty, no leading/trailing whitespace. */
|
|
476
|
+
validateName(code, raw, description, details) {
|
|
477
|
+
if (raw.trim() === "" || raw !== raw.trim()) {
|
|
478
|
+
throw new GraphValidationError(
|
|
479
|
+
code,
|
|
480
|
+
`${description}: name ${JSON.stringify(raw)} is empty or whitespace-padded`,
|
|
481
|
+
details
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
474
485
|
validateInitialState() {
|
|
475
486
|
const initials = Array.from(this.stateSpecs.values()).filter(
|
|
476
487
|
(s) => s.initial
|
|
@@ -522,22 +533,29 @@ var ProcessBuilder = class {
|
|
|
522
533
|
}
|
|
523
534
|
}
|
|
524
535
|
}
|
|
536
|
+
/** Transition identity: (fromState, eventName, toState). Used by both the
|
|
537
|
+
* conflict check and the build-time dedup — keep them in lockstep. */
|
|
538
|
+
static transitionKey(t) {
|
|
539
|
+
return `${t.fromState}\0${t.eventName ?? ""}\0${t.toState}`;
|
|
540
|
+
}
|
|
525
541
|
validateNoConflictingDuplicates() {
|
|
526
542
|
const seen = /* @__PURE__ */ new Map();
|
|
527
543
|
for (const t of this.transitionSpecs) {
|
|
528
|
-
const key =
|
|
544
|
+
const key = _ProcessBuilder.transitionKey(t);
|
|
529
545
|
const existing = seen.get(key);
|
|
530
546
|
if (!existing) {
|
|
531
547
|
seen.set(key, t);
|
|
532
548
|
continue;
|
|
533
549
|
}
|
|
534
|
-
if (existing.condition !== t.condition) {
|
|
550
|
+
if (existing.condition !== t.condition || existing.weight !== t.weight) {
|
|
535
551
|
throw new DuplicateTransitionError({
|
|
536
552
|
fromState: t.fromState,
|
|
537
553
|
toState: t.toState,
|
|
538
554
|
eventName: t.eventName,
|
|
539
555
|
existingConditionName: existing.condition ? existing.condition.getName() : null,
|
|
540
|
-
newConditionName: t.condition ? t.condition.getName() : null
|
|
556
|
+
newConditionName: t.condition ? t.condition.getName() : null,
|
|
557
|
+
existingWeight: existing.weight,
|
|
558
|
+
newWeight: t.weight
|
|
541
559
|
});
|
|
542
560
|
}
|
|
543
561
|
}
|
|
@@ -581,23 +599,12 @@ var ProcessBuilder = class {
|
|
|
581
599
|
);
|
|
582
600
|
}
|
|
583
601
|
const dedupSeen = /* @__PURE__ */ new Set();
|
|
584
|
-
const conditionId = /* @__PURE__ */ new Map();
|
|
585
|
-
let nextConditionId = 0;
|
|
586
|
-
const idForCondition = (cond) => {
|
|
587
|
-
if (cond === null) return "";
|
|
588
|
-
let id = conditionId.get(cond);
|
|
589
|
-
if (id === void 0) {
|
|
590
|
-
id = ++nextConditionId;
|
|
591
|
-
conditionId.set(cond, id);
|
|
592
|
-
}
|
|
593
|
-
return String(id);
|
|
594
|
-
};
|
|
595
602
|
const transitionsByState = /* @__PURE__ */ new Map();
|
|
596
603
|
for (const spec of this.stateSpecs.values()) {
|
|
597
604
|
transitionsByState.set(spec.name, []);
|
|
598
605
|
}
|
|
599
606
|
for (const tSpec of this.transitionSpecs) {
|
|
600
|
-
const dedupKey =
|
|
607
|
+
const dedupKey = _ProcessBuilder.transitionKey(tSpec);
|
|
601
608
|
if (dedupSeen.has(dedupKey)) continue;
|
|
602
609
|
dedupSeen.add(dedupKey);
|
|
603
610
|
const targetState = built.get(tSpec.toState);
|
|
@@ -689,27 +696,6 @@ var NullMutex = class {
|
|
|
689
696
|
}
|
|
690
697
|
};
|
|
691
698
|
|
|
692
|
-
// src/internal/Dispatcher.ts
|
|
693
|
-
var Dispatcher = class {
|
|
694
|
-
commands = [];
|
|
695
|
-
ready = false;
|
|
696
|
-
dispatch(event, args = []) {
|
|
697
|
-
if (this.ready) {
|
|
698
|
-
throw new Error("Was already invoked!");
|
|
699
|
-
}
|
|
700
|
-
this.commands.push({ event, args });
|
|
701
|
-
}
|
|
702
|
-
async invoke() {
|
|
703
|
-
if (this.ready) {
|
|
704
|
-
throw new Error("Was already invoked!");
|
|
705
|
-
}
|
|
706
|
-
for (const { event, args } of this.commands) {
|
|
707
|
-
await event.invoke(...args);
|
|
708
|
-
}
|
|
709
|
-
this.ready = true;
|
|
710
|
-
}
|
|
711
|
-
};
|
|
712
|
-
|
|
713
699
|
// src/internal/OperationQueue.ts
|
|
714
700
|
var OperationQueue = class {
|
|
715
701
|
items = [];
|
|
@@ -726,10 +712,11 @@ var OperationQueue = class {
|
|
|
726
712
|
|
|
727
713
|
// src/filter/ActiveTransitionFilter.ts
|
|
728
714
|
var ActiveTransitionFilter = class {
|
|
729
|
-
static async filter(transitions, subject, context, event) {
|
|
715
|
+
static async filter(transitions, subject, context, event, wrap) {
|
|
716
|
+
const run = wrap ?? ((fn) => fn());
|
|
730
717
|
const active = [];
|
|
731
718
|
for (const transition of transitions) {
|
|
732
|
-
if (await transition.isActive(subject, context, event)) {
|
|
719
|
+
if (await run(() => transition.isActive(subject, context, event))) {
|
|
733
720
|
active.push(transition);
|
|
734
721
|
}
|
|
735
722
|
}
|
|
@@ -762,16 +749,26 @@ var LockCanNotBeAcquiredError = class extends FinitaError {
|
|
|
762
749
|
// src/error/AutomaticTransitionCycleError.ts
|
|
763
750
|
var AutomaticTransitionCycleError = class extends FinitaError {
|
|
764
751
|
code = "automaticTransitionCycle";
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
constructor(
|
|
768
|
-
const visited = Array.from(visitedStateNames);
|
|
752
|
+
stateName;
|
|
753
|
+
hopLimit;
|
|
754
|
+
constructor(stateName, hopLimit) {
|
|
769
755
|
super(
|
|
770
|
-
`Automatic
|
|
756
|
+
`Automatic transitions exceeded ${hopLimit} hops without reaching a quiescent state (last target: "${stateName}") \u2014 the graph is likely looping forever. Raise maxAutomaticHops if the loop is legitimate and bounded. Transitions committed before this error are NOT rolled back.`
|
|
771
757
|
);
|
|
772
758
|
this.name = "AutomaticTransitionCycleError";
|
|
773
|
-
this.
|
|
774
|
-
this.
|
|
759
|
+
this.stateName = stateName;
|
|
760
|
+
this.hopLimit = hopLimit;
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
// src/error/ReentrancyError.ts
|
|
765
|
+
var ReentrancyError = class extends FinitaError {
|
|
766
|
+
code = "reentrancy";
|
|
767
|
+
constructor(operation) {
|
|
768
|
+
super(
|
|
769
|
+
`${operation} was called from inside an observer or condition of the same Statemachine. Awaiting it would deadlock: the machine runs one operation at a time and the runner is blocked on your callback. Where applicable, use the EnqueueContext passed to after-observers to chain events instead; from other callbacks, defer the call out of the synchronous path, e.g. queueMicrotask(() => sm.triggerEvent(...)).`
|
|
770
|
+
);
|
|
771
|
+
this.name = "ReentrancyError";
|
|
775
772
|
}
|
|
776
773
|
};
|
|
777
774
|
|
|
@@ -784,17 +781,26 @@ var Statemachine = class {
|
|
|
784
781
|
currentState;
|
|
785
782
|
lastState = null;
|
|
786
783
|
autoreleaseLock;
|
|
784
|
+
maxAutomaticHops;
|
|
787
785
|
queue = new OperationQueue();
|
|
788
786
|
running = false;
|
|
787
|
+
inSyncCallback = false;
|
|
789
788
|
beforeObservers = [];
|
|
790
789
|
afterObservers = [];
|
|
791
790
|
constructor(subject, process, options = {}) {
|
|
792
791
|
this.subject = subject;
|
|
793
792
|
this.process = process;
|
|
794
|
-
this.currentState = options.initialStateName ? process.getState(options.initialStateName) : process.getInitialState();
|
|
793
|
+
this.currentState = options.initialStateName !== void 0 ? process.getState(options.initialStateName) : process.getInitialState();
|
|
795
794
|
this.transitionSelector = options.transitionSelector ?? new OneOrNoneActiveTransition();
|
|
796
795
|
this.mutex = options.mutex ?? new NullMutex();
|
|
797
796
|
this.autoreleaseLock = options.autoreleaseLock ?? true;
|
|
797
|
+
const hops = options.maxAutomaticHops ?? 100;
|
|
798
|
+
if (!Number.isInteger(hops) || hops < 1) {
|
|
799
|
+
throw new RangeError(
|
|
800
|
+
`maxAutomaticHops must be a positive integer; got ${String(options.maxAutomaticHops)}`
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
this.maxAutomaticHops = hops;
|
|
798
804
|
}
|
|
799
805
|
// --- public getters ---
|
|
800
806
|
getCurrentState() {
|
|
@@ -849,27 +855,45 @@ var Statemachine = class {
|
|
|
849
855
|
// --- public top-level operations ---
|
|
850
856
|
triggerEvent(name, context) {
|
|
851
857
|
return new Promise((resolve, reject) => {
|
|
852
|
-
this.
|
|
853
|
-
|
|
854
|
-
eventName: name,
|
|
855
|
-
context: context ?? /* @__PURE__ */ new Map(),
|
|
856
|
-
resolve,
|
|
857
|
-
reject
|
|
858
|
-
});
|
|
859
|
-
void this.runIfIdle();
|
|
858
|
+
this.assertNotReentrant(`triggerEvent("${name}")`);
|
|
859
|
+
this.enqueueOperation(name, context, resolve, reject);
|
|
860
860
|
});
|
|
861
861
|
}
|
|
862
862
|
checkTransitions(context) {
|
|
863
863
|
return new Promise((resolve, reject) => {
|
|
864
|
-
this.
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
864
|
+
this.assertNotReentrant("checkTransitions()");
|
|
865
|
+
this.enqueueOperation(null, context, resolve, reject);
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
/** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
|
|
869
|
+
* the flag is cleared as soon as fn returns (before any promise it returned
|
|
870
|
+
* is awaited), so concurrent external callers are never affected. This
|
|
871
|
+
* catches triggerEvent/checkTransitions calls made before a callback's first
|
|
872
|
+
* await; calls made after a prior await are not detectable without
|
|
873
|
+
* AsyncLocalStorage (Node-only) and will still deadlock — a documented gap. */
|
|
874
|
+
guardSync(fn) {
|
|
875
|
+
this.inSyncCallback = true;
|
|
876
|
+
try {
|
|
877
|
+
return fn();
|
|
878
|
+
} finally {
|
|
879
|
+
this.inSyncCallback = false;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
assertNotReentrant(operation) {
|
|
883
|
+
if (this.inSyncCallback) {
|
|
884
|
+
throw new ReentrancyError(operation);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
/** Single entry point to the operation queue — every enqueue kicks the runner. */
|
|
888
|
+
enqueueOperation(eventName, context, resolve, reject, ifStateName) {
|
|
889
|
+
this.queue.enqueue({
|
|
890
|
+
eventName,
|
|
891
|
+
context: context ?? /* @__PURE__ */ new Map(),
|
|
892
|
+
ifStateName,
|
|
893
|
+
resolve,
|
|
894
|
+
reject
|
|
872
895
|
});
|
|
896
|
+
void this.runIfIdle();
|
|
873
897
|
}
|
|
874
898
|
// --- internal runner ---
|
|
875
899
|
async runIfIdle() {
|
|
@@ -885,7 +909,12 @@ var Statemachine = class {
|
|
|
885
909
|
}
|
|
886
910
|
}
|
|
887
911
|
async runOperation(op) {
|
|
912
|
+
if (op.ifStateName !== void 0 && this.currentState.getName() !== op.ifStateName) {
|
|
913
|
+
op.resolve();
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
888
916
|
let acquiredHere = false;
|
|
917
|
+
let failure = null;
|
|
889
918
|
try {
|
|
890
919
|
if (!this.mutex.isAcquired()) {
|
|
891
920
|
if (!await this.mutex.acquireLock()) {
|
|
@@ -893,19 +922,24 @@ var Statemachine = class {
|
|
|
893
922
|
}
|
|
894
923
|
acquiredHere = true;
|
|
895
924
|
}
|
|
896
|
-
const event = op.
|
|
925
|
+
const event = op.eventName !== null ? this.resolveEvent(op.eventName) : null;
|
|
897
926
|
await this.processOperation(event, op.context);
|
|
898
|
-
op.resolve();
|
|
899
927
|
} catch (err) {
|
|
900
|
-
|
|
928
|
+
failure = { err };
|
|
901
929
|
} finally {
|
|
902
930
|
if (acquiredHere && this.autoreleaseLock) {
|
|
903
931
|
try {
|
|
904
932
|
await this.mutex.releaseLock();
|
|
905
|
-
} catch {
|
|
933
|
+
} catch (err) {
|
|
934
|
+
if (!failure) failure = { err };
|
|
906
935
|
}
|
|
907
936
|
}
|
|
908
937
|
}
|
|
938
|
+
if (failure) {
|
|
939
|
+
op.reject(failure.err);
|
|
940
|
+
} else {
|
|
941
|
+
op.resolve();
|
|
942
|
+
}
|
|
909
943
|
}
|
|
910
944
|
resolveEvent(name) {
|
|
911
945
|
if (!this.currentState.hasEvent(name)) {
|
|
@@ -920,11 +954,13 @@ var Statemachine = class {
|
|
|
920
954
|
*/
|
|
921
955
|
async processOperation(initialEvent, context) {
|
|
922
956
|
let event = initialEvent;
|
|
923
|
-
|
|
957
|
+
let automaticHops = 0;
|
|
924
958
|
if (event) {
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
959
|
+
const userEvent = event;
|
|
960
|
+
const invokeArgs = [this.subject, context];
|
|
961
|
+
for (const observer of [...userEvent.getObservers()]) {
|
|
962
|
+
await this.guardSync(() => observer.update(userEvent, invokeArgs));
|
|
963
|
+
}
|
|
928
964
|
}
|
|
929
965
|
while (true) {
|
|
930
966
|
const transitions = this.currentState.getTransitions();
|
|
@@ -932,26 +968,28 @@ var Statemachine = class {
|
|
|
932
968
|
transitions,
|
|
933
969
|
this.subject,
|
|
934
970
|
context,
|
|
935
|
-
event ?? void 0
|
|
971
|
+
event ?? void 0,
|
|
972
|
+
(fn) => this.guardSync(fn)
|
|
936
973
|
);
|
|
937
|
-
const selected = this.
|
|
938
|
-
active
|
|
974
|
+
const selected = this.guardSync(
|
|
975
|
+
() => this.transitionSelector.selectTransition(active)
|
|
939
976
|
);
|
|
940
977
|
if (!selected) {
|
|
941
978
|
return;
|
|
942
979
|
}
|
|
943
980
|
const target = selected.getTargetState();
|
|
944
981
|
if (selected.getEventName() === null) {
|
|
945
|
-
|
|
946
|
-
if (
|
|
982
|
+
automaticHops += 1;
|
|
983
|
+
if (automaticHops > this.maxAutomaticHops) {
|
|
947
984
|
throw new AutomaticTransitionCycleError(
|
|
948
985
|
target.getName(),
|
|
949
|
-
|
|
986
|
+
this.maxAutomaticHops
|
|
950
987
|
);
|
|
951
988
|
}
|
|
952
989
|
}
|
|
953
990
|
if (this.currentState !== target) {
|
|
954
|
-
const
|
|
991
|
+
const frame = Object.freeze({
|
|
992
|
+
subject: this.subject,
|
|
955
993
|
fromState: this.currentState,
|
|
956
994
|
toState: target,
|
|
957
995
|
transition: selected,
|
|
@@ -961,39 +999,28 @@ var Statemachine = class {
|
|
|
961
999
|
timestamp: Date.now(),
|
|
962
1000
|
machineName: this.process.getName()
|
|
963
1001
|
});
|
|
964
|
-
for (const observer of this.beforeObservers) {
|
|
965
|
-
await observer.notify(
|
|
1002
|
+
for (const observer of [...this.beforeObservers]) {
|
|
1003
|
+
await this.guardSync(() => observer.notify(frame));
|
|
966
1004
|
}
|
|
967
|
-
|
|
968
|
-
this.lastState = fromState;
|
|
1005
|
+
this.lastState = this.currentState;
|
|
969
1006
|
this.currentState = target;
|
|
970
|
-
const committedFrame = Object.freeze({
|
|
971
|
-
fromState,
|
|
972
|
-
toState: target,
|
|
973
|
-
transition: selected,
|
|
974
|
-
event,
|
|
975
|
-
condition: selected.getCondition(),
|
|
976
|
-
context: this.readonlyContext(context),
|
|
977
|
-
timestamp: proposedFrame.timestamp,
|
|
978
|
-
machineName: this.process.getName()
|
|
979
|
-
});
|
|
980
1007
|
const enqueueCtx = {
|
|
981
|
-
enqueue: (chainedEventName, chainedCtx) => {
|
|
982
|
-
this.
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
resolve: () => {
|
|
1008
|
+
enqueue: (chainedEventName, chainedCtx, ifStateName) => {
|
|
1009
|
+
this.enqueueOperation(
|
|
1010
|
+
chainedEventName,
|
|
1011
|
+
chainedCtx,
|
|
1012
|
+
() => {
|
|
987
1013
|
},
|
|
988
|
-
|
|
989
|
-
}
|
|
990
|
-
|
|
1014
|
+
() => {
|
|
1015
|
+
},
|
|
1016
|
+
ifStateName
|
|
1017
|
+
);
|
|
991
1018
|
}
|
|
992
1019
|
};
|
|
993
1020
|
const errors = [];
|
|
994
|
-
for (const observer of this.afterObservers) {
|
|
1021
|
+
for (const observer of [...this.afterObservers]) {
|
|
995
1022
|
try {
|
|
996
|
-
await observer.notify(
|
|
1023
|
+
await this.guardSync(() => observer.notify(frame, enqueueCtx));
|
|
997
1024
|
} catch (err) {
|
|
998
1025
|
errors.push(err);
|
|
999
1026
|
}
|
|
@@ -1060,6 +1087,23 @@ var CallbackCondition = class {
|
|
|
1060
1087
|
}
|
|
1061
1088
|
};
|
|
1062
1089
|
|
|
1090
|
+
// src/error/InvalidSubjectError.ts
|
|
1091
|
+
var InvalidSubjectError = class extends FinitaError {
|
|
1092
|
+
code = "invalidSubject";
|
|
1093
|
+
expectedInterface;
|
|
1094
|
+
missingMembers;
|
|
1095
|
+
constructor(expectedInterface, missingMembers) {
|
|
1096
|
+
const members = Array.from(missingMembers);
|
|
1097
|
+
const memberList = members.map((m) => `"${m}"`).join(", ");
|
|
1098
|
+
super(
|
|
1099
|
+
`Subject does not satisfy ${expectedInterface}; missing member(s): ${memberList || "(unknown)"}`
|
|
1100
|
+
);
|
|
1101
|
+
this.name = "InvalidSubjectError";
|
|
1102
|
+
this.expectedInterface = expectedInterface;
|
|
1103
|
+
this.missingMembers = Object.freeze([...members]);
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1063
1107
|
// src/condition/Timeout.ts
|
|
1064
1108
|
function isLastStateHasChangedDate(obj) {
|
|
1065
1109
|
return typeof obj === "object" && obj !== null && "getLastStateHasChangedDate" in obj && typeof obj.getLastStateHasChangedDate === "function";
|
|
@@ -1078,30 +1122,40 @@ var Timeout = class {
|
|
|
1078
1122
|
if (isLastStateHasChangedDate(subject)) {
|
|
1079
1123
|
return subject.getLastStateHasChangedDate();
|
|
1080
1124
|
}
|
|
1081
|
-
throw new
|
|
1125
|
+
throw new InvalidSubjectError("LastStateHasChangedDateInterface", [
|
|
1126
|
+
"getLastStateHasChangedDate"
|
|
1127
|
+
]);
|
|
1082
1128
|
}
|
|
1083
1129
|
checkCondition(subject, context) {
|
|
1084
|
-
|
|
1085
|
-
this.getLastStateHasChangedDate(subject, context).getTime()
|
|
1086
|
-
);
|
|
1087
|
-
date.setTime(date.getTime() + this.timeoutMs);
|
|
1088
|
-
return date <= /* @__PURE__ */ new Date();
|
|
1130
|
+
return this.getLastStateHasChangedDate(subject, context).getTime() + this.timeoutMs <= Date.now();
|
|
1089
1131
|
}
|
|
1090
1132
|
};
|
|
1091
1133
|
|
|
1092
|
-
// src/condition/
|
|
1093
|
-
var
|
|
1134
|
+
// src/condition/CompositeCondition.ts
|
|
1135
|
+
var CompositeCondition = class {
|
|
1094
1136
|
conditions = [];
|
|
1095
|
-
|
|
1137
|
+
joinWord;
|
|
1138
|
+
constructor(joinWord, condition) {
|
|
1139
|
+
this.joinWord = joinWord;
|
|
1096
1140
|
this.conditions.push(condition);
|
|
1097
1141
|
}
|
|
1098
|
-
|
|
1142
|
+
addCondition(condition) {
|
|
1099
1143
|
this.conditions.push(condition);
|
|
1100
1144
|
return this;
|
|
1101
1145
|
}
|
|
1102
1146
|
getName() {
|
|
1103
1147
|
const names = this.conditions.map((c) => c.getName());
|
|
1104
|
-
return `(${names.join(
|
|
1148
|
+
return `(${names.join(` ${this.joinWord} `)})`;
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// src/condition/AndComposite.ts
|
|
1153
|
+
var AndComposite = class extends CompositeCondition {
|
|
1154
|
+
constructor(condition) {
|
|
1155
|
+
super("and", condition);
|
|
1156
|
+
}
|
|
1157
|
+
addAnd(condition) {
|
|
1158
|
+
return this.addCondition(condition);
|
|
1105
1159
|
}
|
|
1106
1160
|
async checkCondition(subject, context) {
|
|
1107
1161
|
for (const condition of this.conditions) {
|
|
@@ -1114,18 +1168,12 @@ var AndComposite = class {
|
|
|
1114
1168
|
};
|
|
1115
1169
|
|
|
1116
1170
|
// src/condition/OrComposite.ts
|
|
1117
|
-
var OrComposite = class {
|
|
1118
|
-
conditions = [];
|
|
1171
|
+
var OrComposite = class extends CompositeCondition {
|
|
1119
1172
|
constructor(condition) {
|
|
1120
|
-
|
|
1173
|
+
super("or", condition);
|
|
1121
1174
|
}
|
|
1122
1175
|
addOr(condition) {
|
|
1123
|
-
this.
|
|
1124
|
-
return this;
|
|
1125
|
-
}
|
|
1126
|
-
getName() {
|
|
1127
|
-
const names = this.conditions.map((c) => c.getName());
|
|
1128
|
-
return `(${names.join(" or ")})`;
|
|
1176
|
+
return this.addCondition(condition);
|
|
1129
1177
|
}
|
|
1130
1178
|
async checkCondition(subject, context) {
|
|
1131
1179
|
for (const condition of this.conditions) {
|
|
@@ -1157,10 +1205,9 @@ var CallbackObserver = class {
|
|
|
1157
1205
|
constructor(callback) {
|
|
1158
1206
|
this.callback = callback;
|
|
1159
1207
|
}
|
|
1160
|
-
update(subject) {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
return this.callback(...event.getInvokeArgs());
|
|
1208
|
+
update(subject, args) {
|
|
1209
|
+
if (args !== void 0) {
|
|
1210
|
+
return this.callback(...args);
|
|
1164
1211
|
}
|
|
1165
1212
|
return this.callback(subject);
|
|
1166
1213
|
}
|
|
@@ -1169,11 +1216,19 @@ var CallbackObserver = class {
|
|
|
1169
1216
|
// src/observer/StatefulStatusChanger.ts
|
|
1170
1217
|
var StatefulStatusChanger = class {
|
|
1171
1218
|
subject;
|
|
1219
|
+
/**
|
|
1220
|
+
* @param subject Optional explicit subject to write to. When omitted
|
|
1221
|
+
* (recommended), the observer writes to frame.subject — the subject of
|
|
1222
|
+
* whichever machine fired the transition — so a single instance can be
|
|
1223
|
+
* shared safely across every machine a Factory creates.
|
|
1224
|
+
*/
|
|
1172
1225
|
constructor(subject) {
|
|
1173
|
-
this.subject = subject;
|
|
1226
|
+
this.subject = subject ?? null;
|
|
1174
1227
|
}
|
|
1175
1228
|
notify(frame) {
|
|
1176
|
-
this.subject
|
|
1229
|
+
(this.subject ?? frame.subject).setCurrentStateName(
|
|
1230
|
+
frame.toState.getName()
|
|
1231
|
+
);
|
|
1177
1232
|
}
|
|
1178
1233
|
};
|
|
1179
1234
|
|
|
@@ -1186,19 +1241,25 @@ var OnEnterObserver = class _OnEnterObserver {
|
|
|
1186
1241
|
}
|
|
1187
1242
|
notify(frame, ctx) {
|
|
1188
1243
|
if (frame.toState.hasEvent(this.eventName)) {
|
|
1189
|
-
ctx.enqueue(
|
|
1244
|
+
ctx.enqueue(
|
|
1245
|
+
this.eventName,
|
|
1246
|
+
new Map(frame.context),
|
|
1247
|
+
frame.toState.getName()
|
|
1248
|
+
);
|
|
1190
1249
|
}
|
|
1191
1250
|
}
|
|
1192
1251
|
};
|
|
1193
1252
|
|
|
1194
|
-
// src/
|
|
1253
|
+
// src/util/index.ts
|
|
1195
1254
|
function isNamed(obj) {
|
|
1196
1255
|
return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
|
|
1197
1256
|
}
|
|
1198
|
-
function
|
|
1257
|
+
function nameOrString(obj) {
|
|
1199
1258
|
if (isNamed(obj)) return obj.getName();
|
|
1200
1259
|
return String(obj);
|
|
1201
1260
|
}
|
|
1261
|
+
|
|
1262
|
+
// src/observer/TransitionLogger.ts
|
|
1202
1263
|
var TransitionLogger = class {
|
|
1203
1264
|
logger;
|
|
1204
1265
|
loggerLevel;
|
|
@@ -1208,7 +1269,7 @@ var TransitionLogger = class {
|
|
|
1208
1269
|
}
|
|
1209
1270
|
notify(frame) {
|
|
1210
1271
|
let message = "Transition";
|
|
1211
|
-
message += ` from "${
|
|
1272
|
+
message += ` from "${nameOrString(frame.fromState)}" to "${nameOrString(frame.toState)}"`;
|
|
1212
1273
|
const eventName = frame.event ? frame.event.getName() : null;
|
|
1213
1274
|
const conditionName = frame.condition ? frame.condition.getName() : null;
|
|
1214
1275
|
if (eventName || conditionName) {
|
|
@@ -1319,19 +1380,16 @@ var WeightTransition = class {
|
|
|
1319
1380
|
this.epsilon = epsilon;
|
|
1320
1381
|
}
|
|
1321
1382
|
selectTransition(transitions) {
|
|
1322
|
-
|
|
1323
|
-
let
|
|
1324
|
-
for (const transition of
|
|
1383
|
+
const all = Array.from(transitions);
|
|
1384
|
+
let maxWeight = Number.NEGATIVE_INFINITY;
|
|
1385
|
+
for (const transition of all) {
|
|
1325
1386
|
const weight = transition.getWeight();
|
|
1326
|
-
|
|
1327
|
-
if (bestWeight === null || diff >= this.epsilon) {
|
|
1328
|
-
bestWeight = weight;
|
|
1329
|
-
bestTransitions = [transition];
|
|
1330
|
-
} else if (Math.abs(diff) < this.epsilon) {
|
|
1331
|
-
bestTransitions.push(transition);
|
|
1332
|
-
}
|
|
1387
|
+
if (weight > maxWeight) maxWeight = weight;
|
|
1333
1388
|
}
|
|
1334
|
-
|
|
1389
|
+
const best = all.filter(
|
|
1390
|
+
(transition) => maxWeight - transition.getWeight() < this.epsilon
|
|
1391
|
+
);
|
|
1392
|
+
return this.innerSelector.selectTransition(best);
|
|
1335
1393
|
}
|
|
1336
1394
|
};
|
|
1337
1395
|
|
|
@@ -1474,23 +1532,6 @@ var AbstractNamedProcessDetector = class {
|
|
|
1474
1532
|
}
|
|
1475
1533
|
};
|
|
1476
1534
|
|
|
1477
|
-
// src/error/InvalidSubjectError.ts
|
|
1478
|
-
var InvalidSubjectError = class extends FinitaError {
|
|
1479
|
-
code = "invalidSubject";
|
|
1480
|
-
expectedInterface;
|
|
1481
|
-
missingMembers;
|
|
1482
|
-
constructor(expectedInterface, missingMembers) {
|
|
1483
|
-
const members = Array.from(missingMembers);
|
|
1484
|
-
const memberList = members.map((m) => `"${m}"`).join(", ");
|
|
1485
|
-
super(
|
|
1486
|
-
`Subject does not satisfy ${expectedInterface}; missing member(s): ${memberList || "(unknown)"}`
|
|
1487
|
-
);
|
|
1488
|
-
this.name = "InvalidSubjectError";
|
|
1489
|
-
this.expectedInterface = expectedInterface;
|
|
1490
|
-
this.missingMembers = Object.freeze([...members]);
|
|
1491
|
-
}
|
|
1492
|
-
};
|
|
1493
|
-
|
|
1494
1535
|
// src/factory/StatefulStateNameDetector.ts
|
|
1495
1536
|
function isStateful(obj) {
|
|
1496
1537
|
return typeof obj === "object" && obj !== null && "getCurrentStateName" in obj && typeof obj.getCurrentStateName === "function";
|
|
@@ -1505,11 +1546,8 @@ var StatefulStateNameDetector = class {
|
|
|
1505
1546
|
};
|
|
1506
1547
|
|
|
1507
1548
|
// src/graph/GraphBuilder.ts
|
|
1508
|
-
function
|
|
1509
|
-
return
|
|
1510
|
-
}
|
|
1511
|
-
function escapeDoubleQuotes(str) {
|
|
1512
|
-
return str.replace(/"/g, '\\"');
|
|
1549
|
+
function escapeDotString(str) {
|
|
1550
|
+
return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1513
1551
|
}
|
|
1514
1552
|
function toMermaidId(name) {
|
|
1515
1553
|
return "s_" + name.replace(
|
|
@@ -1518,13 +1556,7 @@ function toMermaidId(name) {
|
|
|
1518
1556
|
);
|
|
1519
1557
|
}
|
|
1520
1558
|
function escapeMermaidLabel(str) {
|
|
1521
|
-
return str.replace(/"/g, "#quot;");
|
|
1522
|
-
}
|
|
1523
|
-
function convertToString(obj) {
|
|
1524
|
-
if (isNamed2(obj)) {
|
|
1525
|
-
return obj.getName();
|
|
1526
|
-
}
|
|
1527
|
-
return String(obj);
|
|
1559
|
+
return str.replace(/\\/g, "#92;").replace(/"/g, "#quot;");
|
|
1528
1560
|
}
|
|
1529
1561
|
var GraphBuilder = class {
|
|
1530
1562
|
nodes = /* @__PURE__ */ new Map();
|
|
@@ -1544,13 +1576,15 @@ var GraphBuilder = class {
|
|
|
1544
1576
|
const eventName = transition.getEventName();
|
|
1545
1577
|
if (eventName) {
|
|
1546
1578
|
parts.push(`E: ${eventName}`);
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1579
|
+
if (state.hasEvent(eventName)) {
|
|
1580
|
+
const event = state.getEvent(eventName);
|
|
1581
|
+
const observerNames = [];
|
|
1582
|
+
for (const observer of event.getObservers()) {
|
|
1583
|
+
observerNames.push(nameOrString(observer));
|
|
1584
|
+
}
|
|
1585
|
+
if (observerNames.length > 0) {
|
|
1586
|
+
parts.push(`C: ${observerNames.join(", ")}`);
|
|
1587
|
+
}
|
|
1554
1588
|
}
|
|
1555
1589
|
}
|
|
1556
1590
|
const conditionName = transition.getConditionName();
|
|
@@ -1603,13 +1637,13 @@ var GraphBuilder = class {
|
|
|
1603
1637
|
lines.push("digraph {");
|
|
1604
1638
|
lines.push(` rankdir=${rankdir};`);
|
|
1605
1639
|
for (const node of graph.nodes) {
|
|
1606
|
-
const label =
|
|
1640
|
+
const label = escapeDotString(node.label);
|
|
1607
1641
|
lines.push(` "${label}" [label="${label}"];`);
|
|
1608
1642
|
}
|
|
1609
1643
|
for (const edge of graph.edges) {
|
|
1610
|
-
const source =
|
|
1611
|
-
const target =
|
|
1612
|
-
const label =
|
|
1644
|
+
const source = escapeDotString(edge.source);
|
|
1645
|
+
const target = escapeDotString(edge.target);
|
|
1646
|
+
const label = escapeDotString(edge.label);
|
|
1613
1647
|
lines.push(` "${source}" -> "${target}" [label="${label}"];`);
|
|
1614
1648
|
}
|
|
1615
1649
|
lines.push("}");
|
|
@@ -1673,6 +1707,7 @@ var GraphBuilder = class {
|
|
|
1673
1707
|
ProcessBuilder,
|
|
1674
1708
|
ProcessFinalizedError,
|
|
1675
1709
|
ProcessNotFoundError,
|
|
1710
|
+
ReentrancyError,
|
|
1676
1711
|
ScoreTransition,
|
|
1677
1712
|
SingleProcessDetector,
|
|
1678
1713
|
State,
|