@camcima/finita 3.0.1 → 4.1.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 +340 -211
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +179 -34
- package/dist/index.d.ts +179 -34
- package/dist/index.js +338 -211
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -52,6 +52,8 @@ __export(index_exports, {
|
|
|
52
52
|
ProcessBuilder: () => ProcessBuilder,
|
|
53
53
|
ProcessFinalizedError: () => ProcessFinalizedError,
|
|
54
54
|
ProcessNotFoundError: () => ProcessNotFoundError,
|
|
55
|
+
QueueLimitExceededError: () => QueueLimitExceededError,
|
|
56
|
+
ReentrancyError: () => ReentrancyError,
|
|
55
57
|
ScoreTransition: () => ScoreTransition,
|
|
56
58
|
SingleProcessDetector: () => SingleProcessDetector,
|
|
57
59
|
State: () => State,
|
|
@@ -74,23 +76,22 @@ var Event = class {
|
|
|
74
76
|
name;
|
|
75
77
|
observers = /* @__PURE__ */ new Set();
|
|
76
78
|
metadata = /* @__PURE__ */ new Map();
|
|
77
|
-
invokeArgs = [];
|
|
78
79
|
constructor(name) {
|
|
79
80
|
this.name = name;
|
|
80
81
|
}
|
|
81
82
|
getName() {
|
|
82
83
|
return this.name;
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* @deprecated Always returns []. Invoke args are now passed directly to
|
|
87
|
+
* Observer.update — reading them from the event was racy when one
|
|
88
|
+
* Process served multiple Statemachines.
|
|
89
|
+
*/
|
|
84
90
|
getInvokeArgs() {
|
|
85
|
-
return
|
|
91
|
+
return [];
|
|
86
92
|
}
|
|
87
93
|
async invoke(...args) {
|
|
88
|
-
this.
|
|
89
|
-
try {
|
|
90
|
-
await this.notify();
|
|
91
|
-
} finally {
|
|
92
|
-
this.invokeArgs = [];
|
|
93
|
-
}
|
|
94
|
+
await this.notify(args);
|
|
94
95
|
}
|
|
95
96
|
attach(observer) {
|
|
96
97
|
this.observers.add(observer);
|
|
@@ -98,9 +99,9 @@ var Event = class {
|
|
|
98
99
|
detach(observer) {
|
|
99
100
|
this.observers.delete(observer);
|
|
100
101
|
}
|
|
101
|
-
async notify() {
|
|
102
|
-
for (const observer of this.observers) {
|
|
103
|
-
await observer.update(this);
|
|
102
|
+
async notify(args) {
|
|
103
|
+
for (const observer of [...this.observers]) {
|
|
104
|
+
await observer.update(this, args);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
getObservers() {
|
|
@@ -256,6 +257,7 @@ var State = class {
|
|
|
256
257
|
throw new Error(`State "${this.name}" transitions already set`);
|
|
257
258
|
}
|
|
258
259
|
this._transitions = new Set(transitions);
|
|
260
|
+
Object.freeze(this);
|
|
259
261
|
}
|
|
260
262
|
getName() {
|
|
261
263
|
return this.name;
|
|
@@ -306,6 +308,7 @@ var Transition = class {
|
|
|
306
308
|
this.eventName = eventName;
|
|
307
309
|
this.condition = condition;
|
|
308
310
|
this.weight = weight;
|
|
311
|
+
Object.freeze(this);
|
|
309
312
|
}
|
|
310
313
|
getTargetState() {
|
|
311
314
|
return this.targetState;
|
|
@@ -382,8 +385,9 @@ var DuplicateTransitionError = class extends FinitaError {
|
|
|
382
385
|
const eventLabel = conflict.eventName ?? "<automatic>";
|
|
383
386
|
const existing = conflict.existingConditionName ?? "<no condition>";
|
|
384
387
|
const incoming = conflict.newConditionName ?? "<no condition>";
|
|
388
|
+
const weightInfo = conflict.existingWeight !== void 0 && conflict.newWeight !== void 0 && conflict.existingWeight !== conflict.newWeight ? `, existing weight ${conflict.existingWeight} vs new weight ${conflict.newWeight}` : "";
|
|
385
389
|
super(
|
|
386
|
-
`Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"`
|
|
390
|
+
`Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"${weightInfo}`
|
|
387
391
|
);
|
|
388
392
|
this.name = "DuplicateTransitionError";
|
|
389
393
|
this.conflict = Object.freeze({ ...conflict });
|
|
@@ -391,7 +395,7 @@ var DuplicateTransitionError = class extends FinitaError {
|
|
|
391
395
|
};
|
|
392
396
|
|
|
393
397
|
// src/ProcessBuilder.ts
|
|
394
|
-
var ProcessBuilder = class {
|
|
398
|
+
var ProcessBuilder = class _ProcessBuilder {
|
|
395
399
|
processName;
|
|
396
400
|
stateSpecs = /* @__PURE__ */ new Map();
|
|
397
401
|
transitionSpecs = [];
|
|
@@ -406,6 +410,9 @@ var ProcessBuilder = class {
|
|
|
406
410
|
if (this.stateSpecs.has(name)) {
|
|
407
411
|
throw new DuplicateStateError(name);
|
|
408
412
|
}
|
|
413
|
+
this.validateName("invalidStateName", name, `addState("${name}")`, {
|
|
414
|
+
stateName: name
|
|
415
|
+
});
|
|
409
416
|
this.stateSpecs.set(name, {
|
|
410
417
|
name,
|
|
411
418
|
initial: options.initial === true,
|
|
@@ -419,32 +426,37 @@ var ProcessBuilder = class {
|
|
|
419
426
|
}
|
|
420
427
|
let eventName = null;
|
|
421
428
|
if (options.event !== void 0) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
eventName = raw;
|
|
429
|
+
this.validateName(
|
|
430
|
+
"invalidEventName",
|
|
431
|
+
options.event,
|
|
432
|
+
`addTransition called with an invalid event name from "${fromState}" to "${toState}"`,
|
|
433
|
+
{ fromState, toState, eventName: options.event }
|
|
434
|
+
);
|
|
435
|
+
eventName = options.event;
|
|
431
436
|
}
|
|
432
437
|
if (options.condition) {
|
|
433
438
|
const conditionName = options.condition.getName();
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
439
|
+
this.validateName(
|
|
440
|
+
"invalidConditionName",
|
|
441
|
+
conditionName,
|
|
442
|
+
`addTransition called with an invalid condition name from "${fromState}" to "${toState}"`,
|
|
443
|
+
{ fromState, toState, conditionName }
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
const weight = options.weight ?? 1;
|
|
447
|
+
if (!Number.isFinite(weight)) {
|
|
448
|
+
throw new GraphValidationError(
|
|
449
|
+
"invalidTransitionWeight",
|
|
450
|
+
`addTransition from "${fromState}" to "${toState}": weight must be a finite number; got ${String(weight)}`,
|
|
451
|
+
{ fromState, toState, eventName, weight }
|
|
452
|
+
);
|
|
441
453
|
}
|
|
442
454
|
this.transitionSpecs.push({
|
|
443
455
|
fromState,
|
|
444
456
|
toState,
|
|
445
457
|
eventName,
|
|
446
458
|
condition: options.condition ?? null,
|
|
447
|
-
weight
|
|
459
|
+
weight
|
|
448
460
|
});
|
|
449
461
|
return this;
|
|
450
462
|
}
|
|
@@ -452,7 +464,6 @@ var ProcessBuilder = class {
|
|
|
452
464
|
if (this.built) {
|
|
453
465
|
throw new ProcessFinalizedError(this.processName);
|
|
454
466
|
}
|
|
455
|
-
this.built = true;
|
|
456
467
|
this.validateInitialState();
|
|
457
468
|
this.validateTransitionEndpoints();
|
|
458
469
|
this.validateNoConflictingDuplicates();
|
|
@@ -463,6 +474,7 @@ var ProcessBuilder = class {
|
|
|
463
474
|
this.validateOrphans(finalStates, initialName);
|
|
464
475
|
}
|
|
465
476
|
const initialState = finalStates.get(initialName);
|
|
477
|
+
this.built = true;
|
|
466
478
|
return new Process(
|
|
467
479
|
INTERNAL_CONSTRUCTION_KEY,
|
|
468
480
|
this.processName,
|
|
@@ -471,6 +483,16 @@ var ProcessBuilder = class {
|
|
|
471
483
|
);
|
|
472
484
|
}
|
|
473
485
|
// --- private helpers ---
|
|
486
|
+
/** One name rule for every named entity: non-empty, no leading/trailing whitespace. */
|
|
487
|
+
validateName(code, raw, description, details) {
|
|
488
|
+
if (raw.trim() === "" || raw !== raw.trim()) {
|
|
489
|
+
throw new GraphValidationError(
|
|
490
|
+
code,
|
|
491
|
+
`${description}: name ${JSON.stringify(raw)} is empty or whitespace-padded`,
|
|
492
|
+
details
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
474
496
|
validateInitialState() {
|
|
475
497
|
const initials = Array.from(this.stateSpecs.values()).filter(
|
|
476
498
|
(s) => s.initial
|
|
@@ -522,22 +544,29 @@ var ProcessBuilder = class {
|
|
|
522
544
|
}
|
|
523
545
|
}
|
|
524
546
|
}
|
|
547
|
+
/** Transition identity: (fromState, eventName, toState). Used by both the
|
|
548
|
+
* conflict check and the build-time dedup — keep them in lockstep. */
|
|
549
|
+
static transitionKey(t) {
|
|
550
|
+
return `${t.fromState}\0${t.eventName ?? ""}\0${t.toState}`;
|
|
551
|
+
}
|
|
525
552
|
validateNoConflictingDuplicates() {
|
|
526
553
|
const seen = /* @__PURE__ */ new Map();
|
|
527
554
|
for (const t of this.transitionSpecs) {
|
|
528
|
-
const key =
|
|
555
|
+
const key = _ProcessBuilder.transitionKey(t);
|
|
529
556
|
const existing = seen.get(key);
|
|
530
557
|
if (!existing) {
|
|
531
558
|
seen.set(key, t);
|
|
532
559
|
continue;
|
|
533
560
|
}
|
|
534
|
-
if (existing.condition !== t.condition) {
|
|
561
|
+
if (existing.condition !== t.condition || existing.weight !== t.weight) {
|
|
535
562
|
throw new DuplicateTransitionError({
|
|
536
563
|
fromState: t.fromState,
|
|
537
564
|
toState: t.toState,
|
|
538
565
|
eventName: t.eventName,
|
|
539
566
|
existingConditionName: existing.condition ? existing.condition.getName() : null,
|
|
540
|
-
newConditionName: t.condition ? t.condition.getName() : null
|
|
567
|
+
newConditionName: t.condition ? t.condition.getName() : null,
|
|
568
|
+
existingWeight: existing.weight,
|
|
569
|
+
newWeight: t.weight
|
|
541
570
|
});
|
|
542
571
|
}
|
|
543
572
|
}
|
|
@@ -581,23 +610,12 @@ var ProcessBuilder = class {
|
|
|
581
610
|
);
|
|
582
611
|
}
|
|
583
612
|
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
613
|
const transitionsByState = /* @__PURE__ */ new Map();
|
|
596
614
|
for (const spec of this.stateSpecs.values()) {
|
|
597
615
|
transitionsByState.set(spec.name, []);
|
|
598
616
|
}
|
|
599
617
|
for (const tSpec of this.transitionSpecs) {
|
|
600
|
-
const dedupKey =
|
|
618
|
+
const dedupKey = _ProcessBuilder.transitionKey(tSpec);
|
|
601
619
|
if (dedupSeen.has(dedupKey)) continue;
|
|
602
620
|
dedupSeen.add(dedupKey);
|
|
603
621
|
const targetState = built.get(tSpec.toState);
|
|
@@ -689,27 +707,6 @@ var NullMutex = class {
|
|
|
689
707
|
}
|
|
690
708
|
};
|
|
691
709
|
|
|
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
710
|
// src/internal/OperationQueue.ts
|
|
714
711
|
var OperationQueue = class {
|
|
715
712
|
items = [];
|
|
@@ -722,14 +719,18 @@ var OperationQueue = class {
|
|
|
722
719
|
isEmpty() {
|
|
723
720
|
return this.items.length === 0;
|
|
724
721
|
}
|
|
722
|
+
size() {
|
|
723
|
+
return this.items.length;
|
|
724
|
+
}
|
|
725
725
|
};
|
|
726
726
|
|
|
727
727
|
// src/filter/ActiveTransitionFilter.ts
|
|
728
728
|
var ActiveTransitionFilter = class {
|
|
729
|
-
static async filter(transitions, subject, context, event) {
|
|
729
|
+
static async filter(transitions, subject, context, event, wrap) {
|
|
730
|
+
const run = wrap ?? ((fn) => fn());
|
|
730
731
|
const active = [];
|
|
731
732
|
for (const transition of transitions) {
|
|
732
|
-
if (await transition.isActive(subject, context, event)) {
|
|
733
|
+
if (await run(() => transition.isActive(subject, context, event))) {
|
|
733
734
|
active.push(transition);
|
|
734
735
|
}
|
|
735
736
|
}
|
|
@@ -762,16 +763,37 @@ var LockCanNotBeAcquiredError = class extends FinitaError {
|
|
|
762
763
|
// src/error/AutomaticTransitionCycleError.ts
|
|
763
764
|
var AutomaticTransitionCycleError = class extends FinitaError {
|
|
764
765
|
code = "automaticTransitionCycle";
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
constructor(
|
|
768
|
-
const visited = Array.from(visitedStateNames);
|
|
766
|
+
stateName;
|
|
767
|
+
hopLimit;
|
|
768
|
+
constructor(stateName, hopLimit) {
|
|
769
769
|
super(
|
|
770
|
-
`Automatic
|
|
770
|
+
`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
771
|
);
|
|
772
772
|
this.name = "AutomaticTransitionCycleError";
|
|
773
|
-
this.
|
|
774
|
-
this.
|
|
773
|
+
this.stateName = stateName;
|
|
774
|
+
this.hopLimit = hopLimit;
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
// src/error/ReentrancyError.ts
|
|
779
|
+
var ReentrancyError = class extends FinitaError {
|
|
780
|
+
code = "reentrancy";
|
|
781
|
+
constructor(operation) {
|
|
782
|
+
super(
|
|
783
|
+
`${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(...)).`
|
|
784
|
+
);
|
|
785
|
+
this.name = "ReentrancyError";
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// src/error/QueueLimitExceededError.ts
|
|
790
|
+
var QueueLimitExceededError = class extends FinitaError {
|
|
791
|
+
code = "queueLimitExceeded";
|
|
792
|
+
constructor(limit, eventName) {
|
|
793
|
+
super(
|
|
794
|
+
`${eventName === null ? "checkTransitions()" : `triggerEvent("${eventName}")`} rejected: the operation queue already holds ${limit} pending operation(s) (maxQueueLength = ${limit}).`
|
|
795
|
+
);
|
|
796
|
+
this.name = "QueueLimitExceededError";
|
|
775
797
|
}
|
|
776
798
|
};
|
|
777
799
|
|
|
@@ -784,17 +806,39 @@ var Statemachine = class {
|
|
|
784
806
|
currentState;
|
|
785
807
|
lastState = null;
|
|
786
808
|
autoreleaseLock;
|
|
809
|
+
maxAutomaticHops;
|
|
810
|
+
maxQueueLength;
|
|
787
811
|
queue = new OperationQueue();
|
|
788
812
|
running = false;
|
|
813
|
+
idleWaiters = [];
|
|
814
|
+
inSyncCallback = false;
|
|
789
815
|
beforeObservers = [];
|
|
790
816
|
afterObservers = [];
|
|
817
|
+
onChainedOperationError;
|
|
818
|
+
onReleaseError;
|
|
791
819
|
constructor(subject, process, options = {}) {
|
|
792
820
|
this.subject = subject;
|
|
793
821
|
this.process = process;
|
|
794
|
-
this.currentState = options.initialStateName ? process.getState(options.initialStateName) : process.getInitialState();
|
|
822
|
+
this.currentState = options.initialStateName !== void 0 ? process.getState(options.initialStateName) : process.getInitialState();
|
|
795
823
|
this.transitionSelector = options.transitionSelector ?? new OneOrNoneActiveTransition();
|
|
796
824
|
this.mutex = options.mutex ?? new NullMutex();
|
|
797
825
|
this.autoreleaseLock = options.autoreleaseLock ?? true;
|
|
826
|
+
const hops = options.maxAutomaticHops ?? 100;
|
|
827
|
+
if (!Number.isInteger(hops) || hops < 1) {
|
|
828
|
+
throw new RangeError(
|
|
829
|
+
`maxAutomaticHops must be a positive integer; got ${String(options.maxAutomaticHops)}`
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
this.maxAutomaticHops = hops;
|
|
833
|
+
const maxQueue = options.maxQueueLength ?? Infinity;
|
|
834
|
+
if (maxQueue !== Infinity && (!Number.isInteger(maxQueue) || maxQueue < 1)) {
|
|
835
|
+
throw new RangeError(
|
|
836
|
+
`maxQueueLength must be a positive integer; got ${String(options.maxQueueLength)}`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
this.maxQueueLength = maxQueue;
|
|
840
|
+
this.onChainedOperationError = options.onChainedOperationError;
|
|
841
|
+
this.onReleaseError = options.onReleaseError;
|
|
798
842
|
}
|
|
799
843
|
// --- public getters ---
|
|
800
844
|
getCurrentState() {
|
|
@@ -811,6 +855,7 @@ var Statemachine = class {
|
|
|
811
855
|
}
|
|
812
856
|
// --- public observer attach/detach ---
|
|
813
857
|
attachBefore(observer) {
|
|
858
|
+
if (this.beforeObservers.includes(observer)) return;
|
|
814
859
|
this.beforeObservers.push(observer);
|
|
815
860
|
}
|
|
816
861
|
detachBefore(observer) {
|
|
@@ -821,6 +866,7 @@ var Statemachine = class {
|
|
|
821
866
|
return this.beforeObservers;
|
|
822
867
|
}
|
|
823
868
|
attachAfter(observer) {
|
|
869
|
+
if (this.afterObservers.includes(observer)) return;
|
|
824
870
|
this.afterObservers.push(observer);
|
|
825
871
|
}
|
|
826
872
|
detachAfter(observer) {
|
|
@@ -849,28 +895,64 @@ var Statemachine = class {
|
|
|
849
895
|
// --- public top-level operations ---
|
|
850
896
|
triggerEvent(name, context) {
|
|
851
897
|
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();
|
|
898
|
+
this.assertNotReentrant(`triggerEvent("${name}")`);
|
|
899
|
+
this.enqueueOperation(name, context, resolve, reject);
|
|
860
900
|
});
|
|
861
901
|
}
|
|
862
902
|
checkTransitions(context) {
|
|
863
903
|
return new Promise((resolve, reject) => {
|
|
864
|
-
this.
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
904
|
+
this.assertNotReentrant("checkTransitions()");
|
|
905
|
+
this.enqueueOperation(null, context, resolve, reject);
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Resolves once the operation queue is empty and the runner is idle —
|
|
910
|
+
* i.e. every operation enqueued so far, including operations chained via
|
|
911
|
+
* EnqueueContext.enqueue(), has completed. Resolves immediately if the
|
|
912
|
+
* machine is already idle. Note this is a quiescence point, not a
|
|
913
|
+
* receipt: work scheduled later (e.g. from a timer) starts a new drain.
|
|
914
|
+
*/
|
|
915
|
+
whenIdle() {
|
|
916
|
+
if (!this.running && this.queue.isEmpty()) {
|
|
917
|
+
return Promise.resolve();
|
|
918
|
+
}
|
|
919
|
+
return new Promise((resolve) => {
|
|
920
|
+
this.idleWaiters.push(resolve);
|
|
872
921
|
});
|
|
873
922
|
}
|
|
923
|
+
/** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
|
|
924
|
+
* the flag is cleared as soon as fn returns (before any promise it returned
|
|
925
|
+
* is awaited), so concurrent external callers are never affected. This
|
|
926
|
+
* catches triggerEvent/checkTransitions calls made before a callback's first
|
|
927
|
+
* await; calls made after a prior await are not detectable without
|
|
928
|
+
* AsyncLocalStorage (Node-only) and will still deadlock — a documented gap. */
|
|
929
|
+
guardSync(fn) {
|
|
930
|
+
this.inSyncCallback = true;
|
|
931
|
+
try {
|
|
932
|
+
return fn();
|
|
933
|
+
} finally {
|
|
934
|
+
this.inSyncCallback = false;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
assertNotReentrant(operation) {
|
|
938
|
+
if (this.inSyncCallback) {
|
|
939
|
+
throw new ReentrancyError(operation);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
/** Single entry point to the operation queue — every enqueue kicks the runner. */
|
|
943
|
+
enqueueOperation(eventName, context, resolve, reject, ifStateName) {
|
|
944
|
+
if (this.queue.size() >= this.maxQueueLength) {
|
|
945
|
+
throw new QueueLimitExceededError(this.maxQueueLength, eventName);
|
|
946
|
+
}
|
|
947
|
+
this.queue.enqueue({
|
|
948
|
+
eventName,
|
|
949
|
+
context: context ?? /* @__PURE__ */ new Map(),
|
|
950
|
+
ifStateName,
|
|
951
|
+
resolve,
|
|
952
|
+
reject
|
|
953
|
+
});
|
|
954
|
+
void this.runIfIdle();
|
|
955
|
+
}
|
|
874
956
|
// --- internal runner ---
|
|
875
957
|
async runIfIdle() {
|
|
876
958
|
if (this.running) return;
|
|
@@ -882,10 +964,20 @@ var Statemachine = class {
|
|
|
882
964
|
}
|
|
883
965
|
} finally {
|
|
884
966
|
this.running = false;
|
|
967
|
+
if (this.queue.isEmpty() && this.idleWaiters.length > 0) {
|
|
968
|
+
const waiters = this.idleWaiters;
|
|
969
|
+
this.idleWaiters = [];
|
|
970
|
+
for (const waiter of waiters) waiter();
|
|
971
|
+
}
|
|
885
972
|
}
|
|
886
973
|
}
|
|
887
974
|
async runOperation(op) {
|
|
975
|
+
if (op.ifStateName !== void 0 && this.currentState.getName() !== op.ifStateName) {
|
|
976
|
+
op.resolve();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
888
979
|
let acquiredHere = false;
|
|
980
|
+
let failure = null;
|
|
889
981
|
try {
|
|
890
982
|
if (!this.mutex.isAcquired()) {
|
|
891
983
|
if (!await this.mutex.acquireLock()) {
|
|
@@ -893,19 +985,28 @@ var Statemachine = class {
|
|
|
893
985
|
}
|
|
894
986
|
acquiredHere = true;
|
|
895
987
|
}
|
|
896
|
-
const event = op.
|
|
988
|
+
const event = op.eventName !== null ? this.resolveEvent(op.eventName) : null;
|
|
897
989
|
await this.processOperation(event, op.context);
|
|
898
|
-
op.resolve();
|
|
899
990
|
} catch (err) {
|
|
900
|
-
|
|
991
|
+
failure = { err };
|
|
901
992
|
} finally {
|
|
902
993
|
if (acquiredHere && this.autoreleaseLock) {
|
|
903
994
|
try {
|
|
904
995
|
await this.mutex.releaseLock();
|
|
905
|
-
} catch {
|
|
996
|
+
} catch (err) {
|
|
997
|
+
try {
|
|
998
|
+
this.onReleaseError?.(err);
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
1001
|
+
if (!failure) failure = { err };
|
|
906
1002
|
}
|
|
907
1003
|
}
|
|
908
1004
|
}
|
|
1005
|
+
if (failure) {
|
|
1006
|
+
op.reject(failure.err);
|
|
1007
|
+
} else {
|
|
1008
|
+
op.resolve();
|
|
1009
|
+
}
|
|
909
1010
|
}
|
|
910
1011
|
resolveEvent(name) {
|
|
911
1012
|
if (!this.currentState.hasEvent(name)) {
|
|
@@ -920,11 +1021,13 @@ var Statemachine = class {
|
|
|
920
1021
|
*/
|
|
921
1022
|
async processOperation(initialEvent, context) {
|
|
922
1023
|
let event = initialEvent;
|
|
923
|
-
|
|
1024
|
+
let automaticHops = 0;
|
|
924
1025
|
if (event) {
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
1026
|
+
const userEvent = event;
|
|
1027
|
+
const invokeArgs = [this.subject, context];
|
|
1028
|
+
for (const observer of [...userEvent.getObservers()]) {
|
|
1029
|
+
await this.guardSync(() => observer.update(userEvent, invokeArgs));
|
|
1030
|
+
}
|
|
928
1031
|
}
|
|
929
1032
|
while (true) {
|
|
930
1033
|
const transitions = this.currentState.getTransitions();
|
|
@@ -932,26 +1035,28 @@ var Statemachine = class {
|
|
|
932
1035
|
transitions,
|
|
933
1036
|
this.subject,
|
|
934
1037
|
context,
|
|
935
|
-
event ?? void 0
|
|
1038
|
+
event ?? void 0,
|
|
1039
|
+
(fn) => this.guardSync(fn)
|
|
936
1040
|
);
|
|
937
|
-
const selected = this.
|
|
938
|
-
active
|
|
1041
|
+
const selected = this.guardSync(
|
|
1042
|
+
() => this.transitionSelector.selectTransition(active)
|
|
939
1043
|
);
|
|
940
1044
|
if (!selected) {
|
|
941
1045
|
return;
|
|
942
1046
|
}
|
|
943
1047
|
const target = selected.getTargetState();
|
|
944
1048
|
if (selected.getEventName() === null) {
|
|
945
|
-
|
|
946
|
-
if (
|
|
1049
|
+
automaticHops += 1;
|
|
1050
|
+
if (automaticHops > this.maxAutomaticHops) {
|
|
947
1051
|
throw new AutomaticTransitionCycleError(
|
|
948
1052
|
target.getName(),
|
|
949
|
-
|
|
1053
|
+
this.maxAutomaticHops
|
|
950
1054
|
);
|
|
951
1055
|
}
|
|
952
1056
|
}
|
|
953
1057
|
if (this.currentState !== target) {
|
|
954
|
-
const
|
|
1058
|
+
const frame = Object.freeze({
|
|
1059
|
+
subject: this.subject,
|
|
955
1060
|
fromState: this.currentState,
|
|
956
1061
|
toState: target,
|
|
957
1062
|
transition: selected,
|
|
@@ -961,39 +1066,34 @@ var Statemachine = class {
|
|
|
961
1066
|
timestamp: Date.now(),
|
|
962
1067
|
machineName: this.process.getName()
|
|
963
1068
|
});
|
|
964
|
-
for (const observer of this.beforeObservers) {
|
|
965
|
-
await observer.notify(
|
|
1069
|
+
for (const observer of [...this.beforeObservers]) {
|
|
1070
|
+
await this.guardSync(() => observer.notify(frame));
|
|
966
1071
|
}
|
|
967
|
-
|
|
968
|
-
this.lastState = fromState;
|
|
1072
|
+
this.lastState = this.currentState;
|
|
969
1073
|
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
1074
|
const enqueueCtx = {
|
|
981
|
-
enqueue: (chainedEventName, chainedCtx) => {
|
|
982
|
-
this.
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1075
|
+
enqueue: (chainedEventName, chainedCtx, ifStateName) => {
|
|
1076
|
+
this.enqueueOperation(
|
|
1077
|
+
chainedEventName,
|
|
1078
|
+
chainedCtx,
|
|
1079
|
+
() => {
|
|
1080
|
+
},
|
|
1081
|
+
(err) => {
|
|
1082
|
+
try {
|
|
1083
|
+
this.onChainedOperationError?.(err, {
|
|
1084
|
+
eventName: chainedEventName
|
|
1085
|
+
});
|
|
1086
|
+
} catch {
|
|
1087
|
+
}
|
|
987
1088
|
},
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
});
|
|
1089
|
+
ifStateName
|
|
1090
|
+
);
|
|
991
1091
|
}
|
|
992
1092
|
};
|
|
993
1093
|
const errors = [];
|
|
994
|
-
for (const observer of this.afterObservers) {
|
|
1094
|
+
for (const observer of [...this.afterObservers]) {
|
|
995
1095
|
try {
|
|
996
|
-
await observer.notify(
|
|
1096
|
+
await this.guardSync(() => observer.notify(frame, enqueueCtx));
|
|
997
1097
|
} catch (err) {
|
|
998
1098
|
errors.push(err);
|
|
999
1099
|
}
|
|
@@ -1060,6 +1160,23 @@ var CallbackCondition = class {
|
|
|
1060
1160
|
}
|
|
1061
1161
|
};
|
|
1062
1162
|
|
|
1163
|
+
// src/error/InvalidSubjectError.ts
|
|
1164
|
+
var InvalidSubjectError = class extends FinitaError {
|
|
1165
|
+
code = "invalidSubject";
|
|
1166
|
+
expectedInterface;
|
|
1167
|
+
missingMembers;
|
|
1168
|
+
constructor(expectedInterface, missingMembers) {
|
|
1169
|
+
const members = Array.from(missingMembers);
|
|
1170
|
+
const memberList = members.map((m) => `"${m}"`).join(", ");
|
|
1171
|
+
super(
|
|
1172
|
+
`Subject does not satisfy ${expectedInterface}; missing member(s): ${memberList || "(unknown)"}`
|
|
1173
|
+
);
|
|
1174
|
+
this.name = "InvalidSubjectError";
|
|
1175
|
+
this.expectedInterface = expectedInterface;
|
|
1176
|
+
this.missingMembers = Object.freeze([...members]);
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1063
1180
|
// src/condition/Timeout.ts
|
|
1064
1181
|
function isLastStateHasChangedDate(obj) {
|
|
1065
1182
|
return typeof obj === "object" && obj !== null && "getLastStateHasChangedDate" in obj && typeof obj.getLastStateHasChangedDate === "function";
|
|
@@ -1078,30 +1195,40 @@ var Timeout = class {
|
|
|
1078
1195
|
if (isLastStateHasChangedDate(subject)) {
|
|
1079
1196
|
return subject.getLastStateHasChangedDate();
|
|
1080
1197
|
}
|
|
1081
|
-
throw new
|
|
1198
|
+
throw new InvalidSubjectError("LastStateHasChangedDateInterface", [
|
|
1199
|
+
"getLastStateHasChangedDate"
|
|
1200
|
+
]);
|
|
1082
1201
|
}
|
|
1083
1202
|
checkCondition(subject, context) {
|
|
1084
|
-
|
|
1085
|
-
this.getLastStateHasChangedDate(subject, context).getTime()
|
|
1086
|
-
);
|
|
1087
|
-
date.setTime(date.getTime() + this.timeoutMs);
|
|
1088
|
-
return date <= /* @__PURE__ */ new Date();
|
|
1203
|
+
return this.getLastStateHasChangedDate(subject, context).getTime() + this.timeoutMs <= Date.now();
|
|
1089
1204
|
}
|
|
1090
1205
|
};
|
|
1091
1206
|
|
|
1092
|
-
// src/condition/
|
|
1093
|
-
var
|
|
1207
|
+
// src/condition/CompositeCondition.ts
|
|
1208
|
+
var CompositeCondition = class {
|
|
1094
1209
|
conditions = [];
|
|
1095
|
-
|
|
1210
|
+
joinWord;
|
|
1211
|
+
constructor(joinWord, condition) {
|
|
1212
|
+
this.joinWord = joinWord;
|
|
1096
1213
|
this.conditions.push(condition);
|
|
1097
1214
|
}
|
|
1098
|
-
|
|
1215
|
+
addCondition(condition) {
|
|
1099
1216
|
this.conditions.push(condition);
|
|
1100
1217
|
return this;
|
|
1101
1218
|
}
|
|
1102
1219
|
getName() {
|
|
1103
1220
|
const names = this.conditions.map((c) => c.getName());
|
|
1104
|
-
return `(${names.join(
|
|
1221
|
+
return `(${names.join(` ${this.joinWord} `)})`;
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
// src/condition/AndComposite.ts
|
|
1226
|
+
var AndComposite = class extends CompositeCondition {
|
|
1227
|
+
constructor(condition) {
|
|
1228
|
+
super("and", condition);
|
|
1229
|
+
}
|
|
1230
|
+
addAnd(condition) {
|
|
1231
|
+
return this.addCondition(condition);
|
|
1105
1232
|
}
|
|
1106
1233
|
async checkCondition(subject, context) {
|
|
1107
1234
|
for (const condition of this.conditions) {
|
|
@@ -1114,18 +1241,12 @@ var AndComposite = class {
|
|
|
1114
1241
|
};
|
|
1115
1242
|
|
|
1116
1243
|
// src/condition/OrComposite.ts
|
|
1117
|
-
var OrComposite = class {
|
|
1118
|
-
conditions = [];
|
|
1244
|
+
var OrComposite = class extends CompositeCondition {
|
|
1119
1245
|
constructor(condition) {
|
|
1120
|
-
|
|
1246
|
+
super("or", condition);
|
|
1121
1247
|
}
|
|
1122
1248
|
addOr(condition) {
|
|
1123
|
-
this.
|
|
1124
|
-
return this;
|
|
1125
|
-
}
|
|
1126
|
-
getName() {
|
|
1127
|
-
const names = this.conditions.map((c) => c.getName());
|
|
1128
|
-
return `(${names.join(" or ")})`;
|
|
1249
|
+
return this.addCondition(condition);
|
|
1129
1250
|
}
|
|
1130
1251
|
async checkCondition(subject, context) {
|
|
1131
1252
|
for (const condition of this.conditions) {
|
|
@@ -1157,10 +1278,9 @@ var CallbackObserver = class {
|
|
|
1157
1278
|
constructor(callback) {
|
|
1158
1279
|
this.callback = callback;
|
|
1159
1280
|
}
|
|
1160
|
-
update(subject) {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
return this.callback(...event.getInvokeArgs());
|
|
1281
|
+
update(subject, args) {
|
|
1282
|
+
if (args !== void 0) {
|
|
1283
|
+
return this.callback(...args);
|
|
1164
1284
|
}
|
|
1165
1285
|
return this.callback(subject);
|
|
1166
1286
|
}
|
|
@@ -1169,11 +1289,19 @@ var CallbackObserver = class {
|
|
|
1169
1289
|
// src/observer/StatefulStatusChanger.ts
|
|
1170
1290
|
var StatefulStatusChanger = class {
|
|
1171
1291
|
subject;
|
|
1292
|
+
/**
|
|
1293
|
+
* @param subject Optional explicit subject to write to. When omitted
|
|
1294
|
+
* (recommended), the observer writes to frame.subject — the subject of
|
|
1295
|
+
* whichever machine fired the transition — so a single instance can be
|
|
1296
|
+
* shared safely across every machine a Factory creates.
|
|
1297
|
+
*/
|
|
1172
1298
|
constructor(subject) {
|
|
1173
|
-
this.subject = subject;
|
|
1299
|
+
this.subject = subject ?? null;
|
|
1174
1300
|
}
|
|
1175
1301
|
notify(frame) {
|
|
1176
|
-
this.subject
|
|
1302
|
+
(this.subject ?? frame.subject).setCurrentStateName(
|
|
1303
|
+
frame.toState.getName()
|
|
1304
|
+
);
|
|
1177
1305
|
}
|
|
1178
1306
|
};
|
|
1179
1307
|
|
|
@@ -1186,19 +1314,25 @@ var OnEnterObserver = class _OnEnterObserver {
|
|
|
1186
1314
|
}
|
|
1187
1315
|
notify(frame, ctx) {
|
|
1188
1316
|
if (frame.toState.hasEvent(this.eventName)) {
|
|
1189
|
-
ctx.enqueue(
|
|
1317
|
+
ctx.enqueue(
|
|
1318
|
+
this.eventName,
|
|
1319
|
+
new Map(frame.context),
|
|
1320
|
+
frame.toState.getName()
|
|
1321
|
+
);
|
|
1190
1322
|
}
|
|
1191
1323
|
}
|
|
1192
1324
|
};
|
|
1193
1325
|
|
|
1194
|
-
// src/
|
|
1326
|
+
// src/util/index.ts
|
|
1195
1327
|
function isNamed(obj) {
|
|
1196
1328
|
return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
|
|
1197
1329
|
}
|
|
1198
|
-
function
|
|
1330
|
+
function nameOrString(obj) {
|
|
1199
1331
|
if (isNamed(obj)) return obj.getName();
|
|
1200
1332
|
return String(obj);
|
|
1201
1333
|
}
|
|
1334
|
+
|
|
1335
|
+
// src/observer/TransitionLogger.ts
|
|
1202
1336
|
var TransitionLogger = class {
|
|
1203
1337
|
logger;
|
|
1204
1338
|
loggerLevel;
|
|
@@ -1208,7 +1342,7 @@ var TransitionLogger = class {
|
|
|
1208
1342
|
}
|
|
1209
1343
|
notify(frame) {
|
|
1210
1344
|
let message = "Transition";
|
|
1211
|
-
message += ` from "${
|
|
1345
|
+
message += ` from "${nameOrString(frame.fromState)}" to "${nameOrString(frame.toState)}"`;
|
|
1212
1346
|
const eventName = frame.event ? frame.event.getName() : null;
|
|
1213
1347
|
const conditionName = frame.condition ? frame.condition.getName() : null;
|
|
1214
1348
|
if (eventName || conditionName) {
|
|
@@ -1315,23 +1449,30 @@ var WeightTransition = class {
|
|
|
1315
1449
|
innerSelector;
|
|
1316
1450
|
epsilon;
|
|
1317
1451
|
constructor(innerSelector, epsilon = 1e-3) {
|
|
1452
|
+
if (!Number.isFinite(epsilon) || epsilon <= 0) {
|
|
1453
|
+
throw new RangeError(
|
|
1454
|
+
`WeightTransition epsilon must be a finite number greater than 0; got ${String(epsilon)}`
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1318
1457
|
this.innerSelector = innerSelector ?? new OneOrNoneActiveTransition();
|
|
1319
1458
|
this.epsilon = epsilon;
|
|
1320
1459
|
}
|
|
1321
1460
|
selectTransition(transitions) {
|
|
1322
|
-
|
|
1323
|
-
let
|
|
1324
|
-
for (const transition of
|
|
1461
|
+
const all = Array.from(transitions);
|
|
1462
|
+
let maxWeight = Number.NEGATIVE_INFINITY;
|
|
1463
|
+
for (const transition of all) {
|
|
1325
1464
|
const weight = transition.getWeight();
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
} else if (Math.abs(diff) < this.epsilon) {
|
|
1331
|
-
bestTransitions.push(transition);
|
|
1465
|
+
if (!Number.isFinite(weight)) {
|
|
1466
|
+
throw new RangeError(
|
|
1467
|
+
`WeightTransition: transition weights must be finite numbers; got ${String(weight)}`
|
|
1468
|
+
);
|
|
1332
1469
|
}
|
|
1470
|
+
if (weight > maxWeight) maxWeight = weight;
|
|
1333
1471
|
}
|
|
1334
|
-
|
|
1472
|
+
const best = all.filter(
|
|
1473
|
+
(transition) => maxWeight - transition.getWeight() < this.epsilon
|
|
1474
|
+
);
|
|
1475
|
+
return this.innerSelector.selectTransition(best);
|
|
1335
1476
|
}
|
|
1336
1477
|
};
|
|
1337
1478
|
|
|
@@ -1474,23 +1615,6 @@ var AbstractNamedProcessDetector = class {
|
|
|
1474
1615
|
}
|
|
1475
1616
|
};
|
|
1476
1617
|
|
|
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
1618
|
// src/factory/StatefulStateNameDetector.ts
|
|
1495
1619
|
function isStateful(obj) {
|
|
1496
1620
|
return typeof obj === "object" && obj !== null && "getCurrentStateName" in obj && typeof obj.getCurrentStateName === "function";
|
|
@@ -1505,11 +1629,8 @@ var StatefulStateNameDetector = class {
|
|
|
1505
1629
|
};
|
|
1506
1630
|
|
|
1507
1631
|
// src/graph/GraphBuilder.ts
|
|
1508
|
-
function
|
|
1509
|
-
return
|
|
1510
|
-
}
|
|
1511
|
-
function escapeDoubleQuotes(str) {
|
|
1512
|
-
return str.replace(/"/g, '\\"');
|
|
1632
|
+
function escapeDotString(str) {
|
|
1633
|
+
return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1513
1634
|
}
|
|
1514
1635
|
function toMermaidId(name) {
|
|
1515
1636
|
return "s_" + name.replace(
|
|
@@ -1518,13 +1639,15 @@ function toMermaidId(name) {
|
|
|
1518
1639
|
);
|
|
1519
1640
|
}
|
|
1520
1641
|
function escapeMermaidLabel(str) {
|
|
1521
|
-
return str.replace(/"/g, "#quot;");
|
|
1642
|
+
return str.replace(/\\/g, "#92;").replace(/"/g, "#quot;");
|
|
1522
1643
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1644
|
+
var VALID_DIRECTIONS = /* @__PURE__ */ new Set(["TB", "BT", "LR", "RL"]);
|
|
1645
|
+
function assertDirection(value, optionName) {
|
|
1646
|
+
if (!VALID_DIRECTIONS.has(value)) {
|
|
1647
|
+
throw new RangeError(
|
|
1648
|
+
`${optionName} must be one of "TB", "BT", "LR", "RL"; got ${JSON.stringify(value)}`
|
|
1649
|
+
);
|
|
1526
1650
|
}
|
|
1527
|
-
return String(obj);
|
|
1528
1651
|
}
|
|
1529
1652
|
var GraphBuilder = class {
|
|
1530
1653
|
nodes = /* @__PURE__ */ new Map();
|
|
@@ -1544,13 +1667,15 @@ var GraphBuilder = class {
|
|
|
1544
1667
|
const eventName = transition.getEventName();
|
|
1545
1668
|
if (eventName) {
|
|
1546
1669
|
parts.push(`E: ${eventName}`);
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1670
|
+
if (state.hasEvent(eventName)) {
|
|
1671
|
+
const event = state.getEvent(eventName);
|
|
1672
|
+
const observerNames = [];
|
|
1673
|
+
for (const observer of event.getObservers()) {
|
|
1674
|
+
observerNames.push(nameOrString(observer));
|
|
1675
|
+
}
|
|
1676
|
+
if (observerNames.length > 0) {
|
|
1677
|
+
parts.push(`C: ${observerNames.join(", ")}`);
|
|
1678
|
+
}
|
|
1554
1679
|
}
|
|
1555
1680
|
}
|
|
1556
1681
|
const conditionName = transition.getConditionName();
|
|
@@ -1599,17 +1724,18 @@ var GraphBuilder = class {
|
|
|
1599
1724
|
toDot(options) {
|
|
1600
1725
|
const graph = this.getGraph();
|
|
1601
1726
|
const rankdir = options?.rankdir ?? "LR";
|
|
1727
|
+
assertDirection(rankdir, "rankdir");
|
|
1602
1728
|
const lines = [];
|
|
1603
1729
|
lines.push("digraph {");
|
|
1604
1730
|
lines.push(` rankdir=${rankdir};`);
|
|
1605
1731
|
for (const node of graph.nodes) {
|
|
1606
|
-
const label =
|
|
1732
|
+
const label = escapeDotString(node.label);
|
|
1607
1733
|
lines.push(` "${label}" [label="${label}"];`);
|
|
1608
1734
|
}
|
|
1609
1735
|
for (const edge of graph.edges) {
|
|
1610
|
-
const source =
|
|
1611
|
-
const target =
|
|
1612
|
-
const label =
|
|
1736
|
+
const source = escapeDotString(edge.source);
|
|
1737
|
+
const target = escapeDotString(edge.target);
|
|
1738
|
+
const label = escapeDotString(edge.label);
|
|
1613
1739
|
lines.push(` "${source}" -> "${target}" [label="${label}"];`);
|
|
1614
1740
|
}
|
|
1615
1741
|
lines.push("}");
|
|
@@ -1618,6 +1744,7 @@ var GraphBuilder = class {
|
|
|
1618
1744
|
toMermaid(options) {
|
|
1619
1745
|
const graph = this.getGraph();
|
|
1620
1746
|
const direction = options?.direction ?? "LR";
|
|
1747
|
+
assertDirection(direction, "direction");
|
|
1621
1748
|
const lines = [];
|
|
1622
1749
|
lines.push(`stateDiagram-v2`);
|
|
1623
1750
|
lines.push(` direction ${direction}`);
|
|
@@ -1673,6 +1800,8 @@ var GraphBuilder = class {
|
|
|
1673
1800
|
ProcessBuilder,
|
|
1674
1801
|
ProcessFinalizedError,
|
|
1675
1802
|
ProcessNotFoundError,
|
|
1803
|
+
QueueLimitExceededError,
|
|
1804
|
+
ReentrancyError,
|
|
1676
1805
|
ScoreTransition,
|
|
1677
1806
|
SingleProcessDetector,
|
|
1678
1807
|
State,
|