@camcima/finita 3.0.1 → 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 +3 -3
package/dist/index.js
CHANGED
|
@@ -3,23 +3,22 @@ var Event = class {
|
|
|
3
3
|
name;
|
|
4
4
|
observers = /* @__PURE__ */ new Set();
|
|
5
5
|
metadata = /* @__PURE__ */ new Map();
|
|
6
|
-
invokeArgs = [];
|
|
7
6
|
constructor(name) {
|
|
8
7
|
this.name = name;
|
|
9
8
|
}
|
|
10
9
|
getName() {
|
|
11
10
|
return this.name;
|
|
12
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated Always returns []. Invoke args are now passed directly to
|
|
14
|
+
* Observer.update — reading them from the event was racy when one
|
|
15
|
+
* Process served multiple Statemachines.
|
|
16
|
+
*/
|
|
13
17
|
getInvokeArgs() {
|
|
14
|
-
return
|
|
18
|
+
return [];
|
|
15
19
|
}
|
|
16
20
|
async invoke(...args) {
|
|
17
|
-
this.
|
|
18
|
-
try {
|
|
19
|
-
await this.notify();
|
|
20
|
-
} finally {
|
|
21
|
-
this.invokeArgs = [];
|
|
22
|
-
}
|
|
21
|
+
await this.notify(args);
|
|
23
22
|
}
|
|
24
23
|
attach(observer) {
|
|
25
24
|
this.observers.add(observer);
|
|
@@ -27,9 +26,9 @@ var Event = class {
|
|
|
27
26
|
detach(observer) {
|
|
28
27
|
this.observers.delete(observer);
|
|
29
28
|
}
|
|
30
|
-
async notify() {
|
|
31
|
-
for (const observer of this.observers) {
|
|
32
|
-
await observer.update(this);
|
|
29
|
+
async notify(args) {
|
|
30
|
+
for (const observer of [...this.observers]) {
|
|
31
|
+
await observer.update(this, args);
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
34
|
getObservers() {
|
|
@@ -311,8 +310,9 @@ var DuplicateTransitionError = class extends FinitaError {
|
|
|
311
310
|
const eventLabel = conflict.eventName ?? "<automatic>";
|
|
312
311
|
const existing = conflict.existingConditionName ?? "<no condition>";
|
|
313
312
|
const incoming = conflict.newConditionName ?? "<no condition>";
|
|
313
|
+
const weightInfo = conflict.existingWeight !== void 0 && conflict.newWeight !== void 0 && conflict.existingWeight !== conflict.newWeight ? `, existing weight ${conflict.existingWeight} vs new weight ${conflict.newWeight}` : "";
|
|
314
314
|
super(
|
|
315
|
-
`Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"`
|
|
315
|
+
`Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"${weightInfo}`
|
|
316
316
|
);
|
|
317
317
|
this.name = "DuplicateTransitionError";
|
|
318
318
|
this.conflict = Object.freeze({ ...conflict });
|
|
@@ -320,7 +320,7 @@ var DuplicateTransitionError = class extends FinitaError {
|
|
|
320
320
|
};
|
|
321
321
|
|
|
322
322
|
// src/ProcessBuilder.ts
|
|
323
|
-
var ProcessBuilder = class {
|
|
323
|
+
var ProcessBuilder = class _ProcessBuilder {
|
|
324
324
|
processName;
|
|
325
325
|
stateSpecs = /* @__PURE__ */ new Map();
|
|
326
326
|
transitionSpecs = [];
|
|
@@ -335,6 +335,9 @@ var ProcessBuilder = class {
|
|
|
335
335
|
if (this.stateSpecs.has(name)) {
|
|
336
336
|
throw new DuplicateStateError(name);
|
|
337
337
|
}
|
|
338
|
+
this.validateName("invalidStateName", name, `addState("${name}")`, {
|
|
339
|
+
stateName: name
|
|
340
|
+
});
|
|
338
341
|
this.stateSpecs.set(name, {
|
|
339
342
|
name,
|
|
340
343
|
initial: options.initial === true,
|
|
@@ -348,25 +351,22 @@ var ProcessBuilder = class {
|
|
|
348
351
|
}
|
|
349
352
|
let eventName = null;
|
|
350
353
|
if (options.event !== void 0) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
eventName = raw;
|
|
354
|
+
this.validateName(
|
|
355
|
+
"invalidEventName",
|
|
356
|
+
options.event,
|
|
357
|
+
`addTransition called with an invalid event name from "${fromState}" to "${toState}"`,
|
|
358
|
+
{ fromState, toState, eventName: options.event }
|
|
359
|
+
);
|
|
360
|
+
eventName = options.event;
|
|
360
361
|
}
|
|
361
362
|
if (options.condition) {
|
|
362
363
|
const conditionName = options.condition.getName();
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
364
|
+
this.validateName(
|
|
365
|
+
"invalidConditionName",
|
|
366
|
+
conditionName,
|
|
367
|
+
`addTransition called with an invalid condition name from "${fromState}" to "${toState}"`,
|
|
368
|
+
{ fromState, toState, conditionName }
|
|
369
|
+
);
|
|
370
370
|
}
|
|
371
371
|
this.transitionSpecs.push({
|
|
372
372
|
fromState,
|
|
@@ -400,6 +400,16 @@ var ProcessBuilder = class {
|
|
|
400
400
|
);
|
|
401
401
|
}
|
|
402
402
|
// --- private helpers ---
|
|
403
|
+
/** One name rule for every named entity: non-empty, no leading/trailing whitespace. */
|
|
404
|
+
validateName(code, raw, description, details) {
|
|
405
|
+
if (raw.trim() === "" || raw !== raw.trim()) {
|
|
406
|
+
throw new GraphValidationError(
|
|
407
|
+
code,
|
|
408
|
+
`${description}: name ${JSON.stringify(raw)} is empty or whitespace-padded`,
|
|
409
|
+
details
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
403
413
|
validateInitialState() {
|
|
404
414
|
const initials = Array.from(this.stateSpecs.values()).filter(
|
|
405
415
|
(s) => s.initial
|
|
@@ -451,22 +461,29 @@ var ProcessBuilder = class {
|
|
|
451
461
|
}
|
|
452
462
|
}
|
|
453
463
|
}
|
|
464
|
+
/** Transition identity: (fromState, eventName, toState). Used by both the
|
|
465
|
+
* conflict check and the build-time dedup — keep them in lockstep. */
|
|
466
|
+
static transitionKey(t) {
|
|
467
|
+
return `${t.fromState}\0${t.eventName ?? ""}\0${t.toState}`;
|
|
468
|
+
}
|
|
454
469
|
validateNoConflictingDuplicates() {
|
|
455
470
|
const seen = /* @__PURE__ */ new Map();
|
|
456
471
|
for (const t of this.transitionSpecs) {
|
|
457
|
-
const key =
|
|
472
|
+
const key = _ProcessBuilder.transitionKey(t);
|
|
458
473
|
const existing = seen.get(key);
|
|
459
474
|
if (!existing) {
|
|
460
475
|
seen.set(key, t);
|
|
461
476
|
continue;
|
|
462
477
|
}
|
|
463
|
-
if (existing.condition !== t.condition) {
|
|
478
|
+
if (existing.condition !== t.condition || existing.weight !== t.weight) {
|
|
464
479
|
throw new DuplicateTransitionError({
|
|
465
480
|
fromState: t.fromState,
|
|
466
481
|
toState: t.toState,
|
|
467
482
|
eventName: t.eventName,
|
|
468
483
|
existingConditionName: existing.condition ? existing.condition.getName() : null,
|
|
469
|
-
newConditionName: t.condition ? t.condition.getName() : null
|
|
484
|
+
newConditionName: t.condition ? t.condition.getName() : null,
|
|
485
|
+
existingWeight: existing.weight,
|
|
486
|
+
newWeight: t.weight
|
|
470
487
|
});
|
|
471
488
|
}
|
|
472
489
|
}
|
|
@@ -510,23 +527,12 @@ var ProcessBuilder = class {
|
|
|
510
527
|
);
|
|
511
528
|
}
|
|
512
529
|
const dedupSeen = /* @__PURE__ */ new Set();
|
|
513
|
-
const conditionId = /* @__PURE__ */ new Map();
|
|
514
|
-
let nextConditionId = 0;
|
|
515
|
-
const idForCondition = (cond) => {
|
|
516
|
-
if (cond === null) return "";
|
|
517
|
-
let id = conditionId.get(cond);
|
|
518
|
-
if (id === void 0) {
|
|
519
|
-
id = ++nextConditionId;
|
|
520
|
-
conditionId.set(cond, id);
|
|
521
|
-
}
|
|
522
|
-
return String(id);
|
|
523
|
-
};
|
|
524
530
|
const transitionsByState = /* @__PURE__ */ new Map();
|
|
525
531
|
for (const spec of this.stateSpecs.values()) {
|
|
526
532
|
transitionsByState.set(spec.name, []);
|
|
527
533
|
}
|
|
528
534
|
for (const tSpec of this.transitionSpecs) {
|
|
529
|
-
const dedupKey =
|
|
535
|
+
const dedupKey = _ProcessBuilder.transitionKey(tSpec);
|
|
530
536
|
if (dedupSeen.has(dedupKey)) continue;
|
|
531
537
|
dedupSeen.add(dedupKey);
|
|
532
538
|
const targetState = built.get(tSpec.toState);
|
|
@@ -618,27 +624,6 @@ var NullMutex = class {
|
|
|
618
624
|
}
|
|
619
625
|
};
|
|
620
626
|
|
|
621
|
-
// src/internal/Dispatcher.ts
|
|
622
|
-
var Dispatcher = class {
|
|
623
|
-
commands = [];
|
|
624
|
-
ready = false;
|
|
625
|
-
dispatch(event, args = []) {
|
|
626
|
-
if (this.ready) {
|
|
627
|
-
throw new Error("Was already invoked!");
|
|
628
|
-
}
|
|
629
|
-
this.commands.push({ event, args });
|
|
630
|
-
}
|
|
631
|
-
async invoke() {
|
|
632
|
-
if (this.ready) {
|
|
633
|
-
throw new Error("Was already invoked!");
|
|
634
|
-
}
|
|
635
|
-
for (const { event, args } of this.commands) {
|
|
636
|
-
await event.invoke(...args);
|
|
637
|
-
}
|
|
638
|
-
this.ready = true;
|
|
639
|
-
}
|
|
640
|
-
};
|
|
641
|
-
|
|
642
627
|
// src/internal/OperationQueue.ts
|
|
643
628
|
var OperationQueue = class {
|
|
644
629
|
items = [];
|
|
@@ -655,10 +640,11 @@ var OperationQueue = class {
|
|
|
655
640
|
|
|
656
641
|
// src/filter/ActiveTransitionFilter.ts
|
|
657
642
|
var ActiveTransitionFilter = class {
|
|
658
|
-
static async filter(transitions, subject, context, event) {
|
|
643
|
+
static async filter(transitions, subject, context, event, wrap) {
|
|
644
|
+
const run = wrap ?? ((fn) => fn());
|
|
659
645
|
const active = [];
|
|
660
646
|
for (const transition of transitions) {
|
|
661
|
-
if (await transition.isActive(subject, context, event)) {
|
|
647
|
+
if (await run(() => transition.isActive(subject, context, event))) {
|
|
662
648
|
active.push(transition);
|
|
663
649
|
}
|
|
664
650
|
}
|
|
@@ -691,16 +677,26 @@ var LockCanNotBeAcquiredError = class extends FinitaError {
|
|
|
691
677
|
// src/error/AutomaticTransitionCycleError.ts
|
|
692
678
|
var AutomaticTransitionCycleError = class extends FinitaError {
|
|
693
679
|
code = "automaticTransitionCycle";
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
constructor(
|
|
697
|
-
const visited = Array.from(visitedStateNames);
|
|
680
|
+
stateName;
|
|
681
|
+
hopLimit;
|
|
682
|
+
constructor(stateName, hopLimit) {
|
|
698
683
|
super(
|
|
699
|
-
`Automatic
|
|
684
|
+
`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.`
|
|
700
685
|
);
|
|
701
686
|
this.name = "AutomaticTransitionCycleError";
|
|
702
|
-
this.
|
|
703
|
-
this.
|
|
687
|
+
this.stateName = stateName;
|
|
688
|
+
this.hopLimit = hopLimit;
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// src/error/ReentrancyError.ts
|
|
693
|
+
var ReentrancyError = class extends FinitaError {
|
|
694
|
+
code = "reentrancy";
|
|
695
|
+
constructor(operation) {
|
|
696
|
+
super(
|
|
697
|
+
`${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(...)).`
|
|
698
|
+
);
|
|
699
|
+
this.name = "ReentrancyError";
|
|
704
700
|
}
|
|
705
701
|
};
|
|
706
702
|
|
|
@@ -713,17 +709,26 @@ var Statemachine = class {
|
|
|
713
709
|
currentState;
|
|
714
710
|
lastState = null;
|
|
715
711
|
autoreleaseLock;
|
|
712
|
+
maxAutomaticHops;
|
|
716
713
|
queue = new OperationQueue();
|
|
717
714
|
running = false;
|
|
715
|
+
inSyncCallback = false;
|
|
718
716
|
beforeObservers = [];
|
|
719
717
|
afterObservers = [];
|
|
720
718
|
constructor(subject, process, options = {}) {
|
|
721
719
|
this.subject = subject;
|
|
722
720
|
this.process = process;
|
|
723
|
-
this.currentState = options.initialStateName ? process.getState(options.initialStateName) : process.getInitialState();
|
|
721
|
+
this.currentState = options.initialStateName !== void 0 ? process.getState(options.initialStateName) : process.getInitialState();
|
|
724
722
|
this.transitionSelector = options.transitionSelector ?? new OneOrNoneActiveTransition();
|
|
725
723
|
this.mutex = options.mutex ?? new NullMutex();
|
|
726
724
|
this.autoreleaseLock = options.autoreleaseLock ?? true;
|
|
725
|
+
const hops = options.maxAutomaticHops ?? 100;
|
|
726
|
+
if (!Number.isInteger(hops) || hops < 1) {
|
|
727
|
+
throw new RangeError(
|
|
728
|
+
`maxAutomaticHops must be a positive integer; got ${String(options.maxAutomaticHops)}`
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
this.maxAutomaticHops = hops;
|
|
727
732
|
}
|
|
728
733
|
// --- public getters ---
|
|
729
734
|
getCurrentState() {
|
|
@@ -778,27 +783,45 @@ var Statemachine = class {
|
|
|
778
783
|
// --- public top-level operations ---
|
|
779
784
|
triggerEvent(name, context) {
|
|
780
785
|
return new Promise((resolve, reject) => {
|
|
781
|
-
this.
|
|
782
|
-
|
|
783
|
-
eventName: name,
|
|
784
|
-
context: context ?? /* @__PURE__ */ new Map(),
|
|
785
|
-
resolve,
|
|
786
|
-
reject
|
|
787
|
-
});
|
|
788
|
-
void this.runIfIdle();
|
|
786
|
+
this.assertNotReentrant(`triggerEvent("${name}")`);
|
|
787
|
+
this.enqueueOperation(name, context, resolve, reject);
|
|
789
788
|
});
|
|
790
789
|
}
|
|
791
790
|
checkTransitions(context) {
|
|
792
791
|
return new Promise((resolve, reject) => {
|
|
793
|
-
this.
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
792
|
+
this.assertNotReentrant("checkTransitions()");
|
|
793
|
+
this.enqueueOperation(null, context, resolve, reject);
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
/** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
|
|
797
|
+
* the flag is cleared as soon as fn returns (before any promise it returned
|
|
798
|
+
* is awaited), so concurrent external callers are never affected. This
|
|
799
|
+
* catches triggerEvent/checkTransitions calls made before a callback's first
|
|
800
|
+
* await; calls made after a prior await are not detectable without
|
|
801
|
+
* AsyncLocalStorage (Node-only) and will still deadlock — a documented gap. */
|
|
802
|
+
guardSync(fn) {
|
|
803
|
+
this.inSyncCallback = true;
|
|
804
|
+
try {
|
|
805
|
+
return fn();
|
|
806
|
+
} finally {
|
|
807
|
+
this.inSyncCallback = false;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
assertNotReentrant(operation) {
|
|
811
|
+
if (this.inSyncCallback) {
|
|
812
|
+
throw new ReentrancyError(operation);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
/** Single entry point to the operation queue — every enqueue kicks the runner. */
|
|
816
|
+
enqueueOperation(eventName, context, resolve, reject, ifStateName) {
|
|
817
|
+
this.queue.enqueue({
|
|
818
|
+
eventName,
|
|
819
|
+
context: context ?? /* @__PURE__ */ new Map(),
|
|
820
|
+
ifStateName,
|
|
821
|
+
resolve,
|
|
822
|
+
reject
|
|
801
823
|
});
|
|
824
|
+
void this.runIfIdle();
|
|
802
825
|
}
|
|
803
826
|
// --- internal runner ---
|
|
804
827
|
async runIfIdle() {
|
|
@@ -814,7 +837,12 @@ var Statemachine = class {
|
|
|
814
837
|
}
|
|
815
838
|
}
|
|
816
839
|
async runOperation(op) {
|
|
840
|
+
if (op.ifStateName !== void 0 && this.currentState.getName() !== op.ifStateName) {
|
|
841
|
+
op.resolve();
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
817
844
|
let acquiredHere = false;
|
|
845
|
+
let failure = null;
|
|
818
846
|
try {
|
|
819
847
|
if (!this.mutex.isAcquired()) {
|
|
820
848
|
if (!await this.mutex.acquireLock()) {
|
|
@@ -822,19 +850,24 @@ var Statemachine = class {
|
|
|
822
850
|
}
|
|
823
851
|
acquiredHere = true;
|
|
824
852
|
}
|
|
825
|
-
const event = op.
|
|
853
|
+
const event = op.eventName !== null ? this.resolveEvent(op.eventName) : null;
|
|
826
854
|
await this.processOperation(event, op.context);
|
|
827
|
-
op.resolve();
|
|
828
855
|
} catch (err) {
|
|
829
|
-
|
|
856
|
+
failure = { err };
|
|
830
857
|
} finally {
|
|
831
858
|
if (acquiredHere && this.autoreleaseLock) {
|
|
832
859
|
try {
|
|
833
860
|
await this.mutex.releaseLock();
|
|
834
|
-
} catch {
|
|
861
|
+
} catch (err) {
|
|
862
|
+
if (!failure) failure = { err };
|
|
835
863
|
}
|
|
836
864
|
}
|
|
837
865
|
}
|
|
866
|
+
if (failure) {
|
|
867
|
+
op.reject(failure.err);
|
|
868
|
+
} else {
|
|
869
|
+
op.resolve();
|
|
870
|
+
}
|
|
838
871
|
}
|
|
839
872
|
resolveEvent(name) {
|
|
840
873
|
if (!this.currentState.hasEvent(name)) {
|
|
@@ -849,11 +882,13 @@ var Statemachine = class {
|
|
|
849
882
|
*/
|
|
850
883
|
async processOperation(initialEvent, context) {
|
|
851
884
|
let event = initialEvent;
|
|
852
|
-
|
|
885
|
+
let automaticHops = 0;
|
|
853
886
|
if (event) {
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
887
|
+
const userEvent = event;
|
|
888
|
+
const invokeArgs = [this.subject, context];
|
|
889
|
+
for (const observer of [...userEvent.getObservers()]) {
|
|
890
|
+
await this.guardSync(() => observer.update(userEvent, invokeArgs));
|
|
891
|
+
}
|
|
857
892
|
}
|
|
858
893
|
while (true) {
|
|
859
894
|
const transitions = this.currentState.getTransitions();
|
|
@@ -861,26 +896,28 @@ var Statemachine = class {
|
|
|
861
896
|
transitions,
|
|
862
897
|
this.subject,
|
|
863
898
|
context,
|
|
864
|
-
event ?? void 0
|
|
899
|
+
event ?? void 0,
|
|
900
|
+
(fn) => this.guardSync(fn)
|
|
865
901
|
);
|
|
866
|
-
const selected = this.
|
|
867
|
-
active
|
|
902
|
+
const selected = this.guardSync(
|
|
903
|
+
() => this.transitionSelector.selectTransition(active)
|
|
868
904
|
);
|
|
869
905
|
if (!selected) {
|
|
870
906
|
return;
|
|
871
907
|
}
|
|
872
908
|
const target = selected.getTargetState();
|
|
873
909
|
if (selected.getEventName() === null) {
|
|
874
|
-
|
|
875
|
-
if (
|
|
910
|
+
automaticHops += 1;
|
|
911
|
+
if (automaticHops > this.maxAutomaticHops) {
|
|
876
912
|
throw new AutomaticTransitionCycleError(
|
|
877
913
|
target.getName(),
|
|
878
|
-
|
|
914
|
+
this.maxAutomaticHops
|
|
879
915
|
);
|
|
880
916
|
}
|
|
881
917
|
}
|
|
882
918
|
if (this.currentState !== target) {
|
|
883
|
-
const
|
|
919
|
+
const frame = Object.freeze({
|
|
920
|
+
subject: this.subject,
|
|
884
921
|
fromState: this.currentState,
|
|
885
922
|
toState: target,
|
|
886
923
|
transition: selected,
|
|
@@ -890,39 +927,28 @@ var Statemachine = class {
|
|
|
890
927
|
timestamp: Date.now(),
|
|
891
928
|
machineName: this.process.getName()
|
|
892
929
|
});
|
|
893
|
-
for (const observer of this.beforeObservers) {
|
|
894
|
-
await observer.notify(
|
|
930
|
+
for (const observer of [...this.beforeObservers]) {
|
|
931
|
+
await this.guardSync(() => observer.notify(frame));
|
|
895
932
|
}
|
|
896
|
-
|
|
897
|
-
this.lastState = fromState;
|
|
933
|
+
this.lastState = this.currentState;
|
|
898
934
|
this.currentState = target;
|
|
899
|
-
const committedFrame = Object.freeze({
|
|
900
|
-
fromState,
|
|
901
|
-
toState: target,
|
|
902
|
-
transition: selected,
|
|
903
|
-
event,
|
|
904
|
-
condition: selected.getCondition(),
|
|
905
|
-
context: this.readonlyContext(context),
|
|
906
|
-
timestamp: proposedFrame.timestamp,
|
|
907
|
-
machineName: this.process.getName()
|
|
908
|
-
});
|
|
909
935
|
const enqueueCtx = {
|
|
910
|
-
enqueue: (chainedEventName, chainedCtx) => {
|
|
911
|
-
this.
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
resolve: () => {
|
|
936
|
+
enqueue: (chainedEventName, chainedCtx, ifStateName) => {
|
|
937
|
+
this.enqueueOperation(
|
|
938
|
+
chainedEventName,
|
|
939
|
+
chainedCtx,
|
|
940
|
+
() => {
|
|
916
941
|
},
|
|
917
|
-
|
|
918
|
-
}
|
|
919
|
-
|
|
942
|
+
() => {
|
|
943
|
+
},
|
|
944
|
+
ifStateName
|
|
945
|
+
);
|
|
920
946
|
}
|
|
921
947
|
};
|
|
922
948
|
const errors = [];
|
|
923
|
-
for (const observer of this.afterObservers) {
|
|
949
|
+
for (const observer of [...this.afterObservers]) {
|
|
924
950
|
try {
|
|
925
|
-
await observer.notify(
|
|
951
|
+
await this.guardSync(() => observer.notify(frame, enqueueCtx));
|
|
926
952
|
} catch (err) {
|
|
927
953
|
errors.push(err);
|
|
928
954
|
}
|
|
@@ -989,6 +1015,23 @@ var CallbackCondition = class {
|
|
|
989
1015
|
}
|
|
990
1016
|
};
|
|
991
1017
|
|
|
1018
|
+
// src/error/InvalidSubjectError.ts
|
|
1019
|
+
var InvalidSubjectError = class extends FinitaError {
|
|
1020
|
+
code = "invalidSubject";
|
|
1021
|
+
expectedInterface;
|
|
1022
|
+
missingMembers;
|
|
1023
|
+
constructor(expectedInterface, missingMembers) {
|
|
1024
|
+
const members = Array.from(missingMembers);
|
|
1025
|
+
const memberList = members.map((m) => `"${m}"`).join(", ");
|
|
1026
|
+
super(
|
|
1027
|
+
`Subject does not satisfy ${expectedInterface}; missing member(s): ${memberList || "(unknown)"}`
|
|
1028
|
+
);
|
|
1029
|
+
this.name = "InvalidSubjectError";
|
|
1030
|
+
this.expectedInterface = expectedInterface;
|
|
1031
|
+
this.missingMembers = Object.freeze([...members]);
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
|
|
992
1035
|
// src/condition/Timeout.ts
|
|
993
1036
|
function isLastStateHasChangedDate(obj) {
|
|
994
1037
|
return typeof obj === "object" && obj !== null && "getLastStateHasChangedDate" in obj && typeof obj.getLastStateHasChangedDate === "function";
|
|
@@ -1007,30 +1050,40 @@ var Timeout = class {
|
|
|
1007
1050
|
if (isLastStateHasChangedDate(subject)) {
|
|
1008
1051
|
return subject.getLastStateHasChangedDate();
|
|
1009
1052
|
}
|
|
1010
|
-
throw new
|
|
1053
|
+
throw new InvalidSubjectError("LastStateHasChangedDateInterface", [
|
|
1054
|
+
"getLastStateHasChangedDate"
|
|
1055
|
+
]);
|
|
1011
1056
|
}
|
|
1012
1057
|
checkCondition(subject, context) {
|
|
1013
|
-
|
|
1014
|
-
this.getLastStateHasChangedDate(subject, context).getTime()
|
|
1015
|
-
);
|
|
1016
|
-
date.setTime(date.getTime() + this.timeoutMs);
|
|
1017
|
-
return date <= /* @__PURE__ */ new Date();
|
|
1058
|
+
return this.getLastStateHasChangedDate(subject, context).getTime() + this.timeoutMs <= Date.now();
|
|
1018
1059
|
}
|
|
1019
1060
|
};
|
|
1020
1061
|
|
|
1021
|
-
// src/condition/
|
|
1022
|
-
var
|
|
1062
|
+
// src/condition/CompositeCondition.ts
|
|
1063
|
+
var CompositeCondition = class {
|
|
1023
1064
|
conditions = [];
|
|
1024
|
-
|
|
1065
|
+
joinWord;
|
|
1066
|
+
constructor(joinWord, condition) {
|
|
1067
|
+
this.joinWord = joinWord;
|
|
1025
1068
|
this.conditions.push(condition);
|
|
1026
1069
|
}
|
|
1027
|
-
|
|
1070
|
+
addCondition(condition) {
|
|
1028
1071
|
this.conditions.push(condition);
|
|
1029
1072
|
return this;
|
|
1030
1073
|
}
|
|
1031
1074
|
getName() {
|
|
1032
1075
|
const names = this.conditions.map((c) => c.getName());
|
|
1033
|
-
return `(${names.join(
|
|
1076
|
+
return `(${names.join(` ${this.joinWord} `)})`;
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
// src/condition/AndComposite.ts
|
|
1081
|
+
var AndComposite = class extends CompositeCondition {
|
|
1082
|
+
constructor(condition) {
|
|
1083
|
+
super("and", condition);
|
|
1084
|
+
}
|
|
1085
|
+
addAnd(condition) {
|
|
1086
|
+
return this.addCondition(condition);
|
|
1034
1087
|
}
|
|
1035
1088
|
async checkCondition(subject, context) {
|
|
1036
1089
|
for (const condition of this.conditions) {
|
|
@@ -1043,18 +1096,12 @@ var AndComposite = class {
|
|
|
1043
1096
|
};
|
|
1044
1097
|
|
|
1045
1098
|
// src/condition/OrComposite.ts
|
|
1046
|
-
var OrComposite = class {
|
|
1047
|
-
conditions = [];
|
|
1099
|
+
var OrComposite = class extends CompositeCondition {
|
|
1048
1100
|
constructor(condition) {
|
|
1049
|
-
|
|
1101
|
+
super("or", condition);
|
|
1050
1102
|
}
|
|
1051
1103
|
addOr(condition) {
|
|
1052
|
-
this.
|
|
1053
|
-
return this;
|
|
1054
|
-
}
|
|
1055
|
-
getName() {
|
|
1056
|
-
const names = this.conditions.map((c) => c.getName());
|
|
1057
|
-
return `(${names.join(" or ")})`;
|
|
1104
|
+
return this.addCondition(condition);
|
|
1058
1105
|
}
|
|
1059
1106
|
async checkCondition(subject, context) {
|
|
1060
1107
|
for (const condition of this.conditions) {
|
|
@@ -1086,10 +1133,9 @@ var CallbackObserver = class {
|
|
|
1086
1133
|
constructor(callback) {
|
|
1087
1134
|
this.callback = callback;
|
|
1088
1135
|
}
|
|
1089
|
-
update(subject) {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
return this.callback(...event.getInvokeArgs());
|
|
1136
|
+
update(subject, args) {
|
|
1137
|
+
if (args !== void 0) {
|
|
1138
|
+
return this.callback(...args);
|
|
1093
1139
|
}
|
|
1094
1140
|
return this.callback(subject);
|
|
1095
1141
|
}
|
|
@@ -1098,11 +1144,19 @@ var CallbackObserver = class {
|
|
|
1098
1144
|
// src/observer/StatefulStatusChanger.ts
|
|
1099
1145
|
var StatefulStatusChanger = class {
|
|
1100
1146
|
subject;
|
|
1147
|
+
/**
|
|
1148
|
+
* @param subject Optional explicit subject to write to. When omitted
|
|
1149
|
+
* (recommended), the observer writes to frame.subject — the subject of
|
|
1150
|
+
* whichever machine fired the transition — so a single instance can be
|
|
1151
|
+
* shared safely across every machine a Factory creates.
|
|
1152
|
+
*/
|
|
1101
1153
|
constructor(subject) {
|
|
1102
|
-
this.subject = subject;
|
|
1154
|
+
this.subject = subject ?? null;
|
|
1103
1155
|
}
|
|
1104
1156
|
notify(frame) {
|
|
1105
|
-
this.subject
|
|
1157
|
+
(this.subject ?? frame.subject).setCurrentStateName(
|
|
1158
|
+
frame.toState.getName()
|
|
1159
|
+
);
|
|
1106
1160
|
}
|
|
1107
1161
|
};
|
|
1108
1162
|
|
|
@@ -1115,19 +1169,25 @@ var OnEnterObserver = class _OnEnterObserver {
|
|
|
1115
1169
|
}
|
|
1116
1170
|
notify(frame, ctx) {
|
|
1117
1171
|
if (frame.toState.hasEvent(this.eventName)) {
|
|
1118
|
-
ctx.enqueue(
|
|
1172
|
+
ctx.enqueue(
|
|
1173
|
+
this.eventName,
|
|
1174
|
+
new Map(frame.context),
|
|
1175
|
+
frame.toState.getName()
|
|
1176
|
+
);
|
|
1119
1177
|
}
|
|
1120
1178
|
}
|
|
1121
1179
|
};
|
|
1122
1180
|
|
|
1123
|
-
// src/
|
|
1181
|
+
// src/util/index.ts
|
|
1124
1182
|
function isNamed(obj) {
|
|
1125
1183
|
return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
|
|
1126
1184
|
}
|
|
1127
|
-
function
|
|
1185
|
+
function nameOrString(obj) {
|
|
1128
1186
|
if (isNamed(obj)) return obj.getName();
|
|
1129
1187
|
return String(obj);
|
|
1130
1188
|
}
|
|
1189
|
+
|
|
1190
|
+
// src/observer/TransitionLogger.ts
|
|
1131
1191
|
var TransitionLogger = class {
|
|
1132
1192
|
logger;
|
|
1133
1193
|
loggerLevel;
|
|
@@ -1137,7 +1197,7 @@ var TransitionLogger = class {
|
|
|
1137
1197
|
}
|
|
1138
1198
|
notify(frame) {
|
|
1139
1199
|
let message = "Transition";
|
|
1140
|
-
message += ` from "${
|
|
1200
|
+
message += ` from "${nameOrString(frame.fromState)}" to "${nameOrString(frame.toState)}"`;
|
|
1141
1201
|
const eventName = frame.event ? frame.event.getName() : null;
|
|
1142
1202
|
const conditionName = frame.condition ? frame.condition.getName() : null;
|
|
1143
1203
|
if (eventName || conditionName) {
|
|
@@ -1248,19 +1308,16 @@ var WeightTransition = class {
|
|
|
1248
1308
|
this.epsilon = epsilon;
|
|
1249
1309
|
}
|
|
1250
1310
|
selectTransition(transitions) {
|
|
1251
|
-
|
|
1252
|
-
let
|
|
1253
|
-
for (const transition of
|
|
1311
|
+
const all = Array.from(transitions);
|
|
1312
|
+
let maxWeight = Number.NEGATIVE_INFINITY;
|
|
1313
|
+
for (const transition of all) {
|
|
1254
1314
|
const weight = transition.getWeight();
|
|
1255
|
-
|
|
1256
|
-
if (bestWeight === null || diff >= this.epsilon) {
|
|
1257
|
-
bestWeight = weight;
|
|
1258
|
-
bestTransitions = [transition];
|
|
1259
|
-
} else if (Math.abs(diff) < this.epsilon) {
|
|
1260
|
-
bestTransitions.push(transition);
|
|
1261
|
-
}
|
|
1315
|
+
if (weight > maxWeight) maxWeight = weight;
|
|
1262
1316
|
}
|
|
1263
|
-
|
|
1317
|
+
const best = all.filter(
|
|
1318
|
+
(transition) => maxWeight - transition.getWeight() < this.epsilon
|
|
1319
|
+
);
|
|
1320
|
+
return this.innerSelector.selectTransition(best);
|
|
1264
1321
|
}
|
|
1265
1322
|
};
|
|
1266
1323
|
|
|
@@ -1403,23 +1460,6 @@ var AbstractNamedProcessDetector = class {
|
|
|
1403
1460
|
}
|
|
1404
1461
|
};
|
|
1405
1462
|
|
|
1406
|
-
// src/error/InvalidSubjectError.ts
|
|
1407
|
-
var InvalidSubjectError = class extends FinitaError {
|
|
1408
|
-
code = "invalidSubject";
|
|
1409
|
-
expectedInterface;
|
|
1410
|
-
missingMembers;
|
|
1411
|
-
constructor(expectedInterface, missingMembers) {
|
|
1412
|
-
const members = Array.from(missingMembers);
|
|
1413
|
-
const memberList = members.map((m) => `"${m}"`).join(", ");
|
|
1414
|
-
super(
|
|
1415
|
-
`Subject does not satisfy ${expectedInterface}; missing member(s): ${memberList || "(unknown)"}`
|
|
1416
|
-
);
|
|
1417
|
-
this.name = "InvalidSubjectError";
|
|
1418
|
-
this.expectedInterface = expectedInterface;
|
|
1419
|
-
this.missingMembers = Object.freeze([...members]);
|
|
1420
|
-
}
|
|
1421
|
-
};
|
|
1422
|
-
|
|
1423
1463
|
// src/factory/StatefulStateNameDetector.ts
|
|
1424
1464
|
function isStateful(obj) {
|
|
1425
1465
|
return typeof obj === "object" && obj !== null && "getCurrentStateName" in obj && typeof obj.getCurrentStateName === "function";
|
|
@@ -1434,11 +1474,8 @@ var StatefulStateNameDetector = class {
|
|
|
1434
1474
|
};
|
|
1435
1475
|
|
|
1436
1476
|
// src/graph/GraphBuilder.ts
|
|
1437
|
-
function
|
|
1438
|
-
return
|
|
1439
|
-
}
|
|
1440
|
-
function escapeDoubleQuotes(str) {
|
|
1441
|
-
return str.replace(/"/g, '\\"');
|
|
1477
|
+
function escapeDotString(str) {
|
|
1478
|
+
return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1442
1479
|
}
|
|
1443
1480
|
function toMermaidId(name) {
|
|
1444
1481
|
return "s_" + name.replace(
|
|
@@ -1447,13 +1484,7 @@ function toMermaidId(name) {
|
|
|
1447
1484
|
);
|
|
1448
1485
|
}
|
|
1449
1486
|
function escapeMermaidLabel(str) {
|
|
1450
|
-
return str.replace(/"/g, "#quot;");
|
|
1451
|
-
}
|
|
1452
|
-
function convertToString(obj) {
|
|
1453
|
-
if (isNamed2(obj)) {
|
|
1454
|
-
return obj.getName();
|
|
1455
|
-
}
|
|
1456
|
-
return String(obj);
|
|
1487
|
+
return str.replace(/\\/g, "#92;").replace(/"/g, "#quot;");
|
|
1457
1488
|
}
|
|
1458
1489
|
var GraphBuilder = class {
|
|
1459
1490
|
nodes = /* @__PURE__ */ new Map();
|
|
@@ -1473,13 +1504,15 @@ var GraphBuilder = class {
|
|
|
1473
1504
|
const eventName = transition.getEventName();
|
|
1474
1505
|
if (eventName) {
|
|
1475
1506
|
parts.push(`E: ${eventName}`);
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1507
|
+
if (state.hasEvent(eventName)) {
|
|
1508
|
+
const event = state.getEvent(eventName);
|
|
1509
|
+
const observerNames = [];
|
|
1510
|
+
for (const observer of event.getObservers()) {
|
|
1511
|
+
observerNames.push(nameOrString(observer));
|
|
1512
|
+
}
|
|
1513
|
+
if (observerNames.length > 0) {
|
|
1514
|
+
parts.push(`C: ${observerNames.join(", ")}`);
|
|
1515
|
+
}
|
|
1483
1516
|
}
|
|
1484
1517
|
}
|
|
1485
1518
|
const conditionName = transition.getConditionName();
|
|
@@ -1532,13 +1565,13 @@ var GraphBuilder = class {
|
|
|
1532
1565
|
lines.push("digraph {");
|
|
1533
1566
|
lines.push(` rankdir=${rankdir};`);
|
|
1534
1567
|
for (const node of graph.nodes) {
|
|
1535
|
-
const label =
|
|
1568
|
+
const label = escapeDotString(node.label);
|
|
1536
1569
|
lines.push(` "${label}" [label="${label}"];`);
|
|
1537
1570
|
}
|
|
1538
1571
|
for (const edge of graph.edges) {
|
|
1539
|
-
const source =
|
|
1540
|
-
const target =
|
|
1541
|
-
const label =
|
|
1572
|
+
const source = escapeDotString(edge.source);
|
|
1573
|
+
const target = escapeDotString(edge.target);
|
|
1574
|
+
const label = escapeDotString(edge.label);
|
|
1542
1575
|
lines.push(` "${source}" -> "${target}" [label="${label}"];`);
|
|
1543
1576
|
}
|
|
1544
1577
|
lines.push("}");
|
|
@@ -1601,6 +1634,7 @@ export {
|
|
|
1601
1634
|
ProcessBuilder,
|
|
1602
1635
|
ProcessFinalizedError,
|
|
1603
1636
|
ProcessNotFoundError,
|
|
1637
|
+
ReentrancyError,
|
|
1604
1638
|
ScoreTransition,
|
|
1605
1639
|
SingleProcessDetector,
|
|
1606
1640
|
State,
|