@camcima/finita 4.0.0 → 4.2.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/dist/index.cjs +218 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +155 -13
- package/dist/index.d.ts +155 -13
- package/dist/index.js +216 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,7 @@ __export(index_exports, {
|
|
|
42
42
|
InvalidSubjectError: () => InvalidSubjectError,
|
|
43
43
|
LockAdapterMutex: () => LockAdapterMutex,
|
|
44
44
|
LockCanNotBeAcquiredError: () => LockCanNotBeAcquiredError,
|
|
45
|
+
LockCanNotBeReleasedError: () => LockCanNotBeReleasedError,
|
|
45
46
|
MutexFactory: () => MutexFactory,
|
|
46
47
|
Not: () => Not,
|
|
47
48
|
NullMutex: () => NullMutex,
|
|
@@ -52,6 +53,7 @@ __export(index_exports, {
|
|
|
52
53
|
ProcessBuilder: () => ProcessBuilder,
|
|
53
54
|
ProcessFinalizedError: () => ProcessFinalizedError,
|
|
54
55
|
ProcessNotFoundError: () => ProcessNotFoundError,
|
|
56
|
+
QueueLimitExceededError: () => QueueLimitExceededError,
|
|
55
57
|
ReentrancyError: () => ReentrancyError,
|
|
56
58
|
ScoreTransition: () => ScoreTransition,
|
|
57
59
|
SingleProcessDetector: () => SingleProcessDetector,
|
|
@@ -103,8 +105,10 @@ var Event = class {
|
|
|
103
105
|
await observer.update(this, args);
|
|
104
106
|
}
|
|
105
107
|
}
|
|
108
|
+
/** Snapshot — detaching later does not change an already-returned list,
|
|
109
|
+
* and mutating it does not change the event's registrations. */
|
|
106
110
|
getObservers() {
|
|
107
|
-
return this.observers;
|
|
111
|
+
return [...this.observers];
|
|
108
112
|
}
|
|
109
113
|
getMetadata() {
|
|
110
114
|
return Object.fromEntries(this.metadata);
|
|
@@ -256,6 +260,7 @@ var State = class {
|
|
|
256
260
|
throw new Error(`State "${this.name}" transitions already set`);
|
|
257
261
|
}
|
|
258
262
|
this._transitions = new Set(transitions);
|
|
263
|
+
Object.freeze(this);
|
|
259
264
|
}
|
|
260
265
|
getName() {
|
|
261
266
|
return this.name;
|
|
@@ -306,6 +311,7 @@ var Transition = class {
|
|
|
306
311
|
this.eventName = eventName;
|
|
307
312
|
this.condition = condition;
|
|
308
313
|
this.weight = weight;
|
|
314
|
+
Object.freeze(this);
|
|
309
315
|
}
|
|
310
316
|
getTargetState() {
|
|
311
317
|
return this.targetState;
|
|
@@ -440,12 +446,20 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
440
446
|
{ fromState, toState, conditionName }
|
|
441
447
|
);
|
|
442
448
|
}
|
|
449
|
+
const weight = options.weight ?? 1;
|
|
450
|
+
if (!Number.isFinite(weight)) {
|
|
451
|
+
throw new GraphValidationError(
|
|
452
|
+
"invalidTransitionWeight",
|
|
453
|
+
`addTransition from "${fromState}" to "${toState}": weight must be a finite number; got ${String(weight)}`,
|
|
454
|
+
{ fromState, toState, eventName, weight }
|
|
455
|
+
);
|
|
456
|
+
}
|
|
443
457
|
this.transitionSpecs.push({
|
|
444
458
|
fromState,
|
|
445
459
|
toState,
|
|
446
460
|
eventName,
|
|
447
461
|
condition: options.condition ?? null,
|
|
448
|
-
weight
|
|
462
|
+
weight
|
|
449
463
|
});
|
|
450
464
|
return this;
|
|
451
465
|
}
|
|
@@ -453,7 +467,6 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
453
467
|
if (this.built) {
|
|
454
468
|
throw new ProcessFinalizedError(this.processName);
|
|
455
469
|
}
|
|
456
|
-
this.built = true;
|
|
457
470
|
this.validateInitialState();
|
|
458
471
|
this.validateTransitionEndpoints();
|
|
459
472
|
this.validateNoConflictingDuplicates();
|
|
@@ -464,6 +477,7 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
464
477
|
this.validateOrphans(finalStates, initialName);
|
|
465
478
|
}
|
|
466
479
|
const initialState = finalStates.get(initialName);
|
|
480
|
+
this.built = true;
|
|
467
481
|
return new Process(
|
|
468
482
|
INTERNAL_CONSTRUCTION_KEY,
|
|
469
483
|
this.processName,
|
|
@@ -655,12 +669,30 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
655
669
|
var AmbiguousTransitionError = class extends FinitaError {
|
|
656
670
|
code = "ambiguousTransition";
|
|
657
671
|
activeCount;
|
|
658
|
-
|
|
659
|
-
|
|
672
|
+
/** The competing transitions — what you need to resolve the ambiguity. */
|
|
673
|
+
candidates;
|
|
674
|
+
constructor(activeCount, candidates = []) {
|
|
675
|
+
const list = Array.from(candidates, (c) => Object.freeze({ ...c }));
|
|
676
|
+
const detail = list.length > 0 ? ` Candidates: ${list.map(describeCandidate).join("; ")}.` : "";
|
|
677
|
+
super(
|
|
678
|
+
`More than one transition is active! (active count: ${activeCount})${detail}`
|
|
679
|
+
);
|
|
660
680
|
this.name = "AmbiguousTransitionError";
|
|
661
681
|
this.activeCount = activeCount;
|
|
682
|
+
this.candidates = Object.freeze(list);
|
|
662
683
|
}
|
|
663
684
|
};
|
|
685
|
+
function describeCandidate(candidate) {
|
|
686
|
+
const parts = [`-> "${candidate.targetStateName}"`];
|
|
687
|
+
parts.push(
|
|
688
|
+
candidate.eventName === null ? "on <automatic>" : `on event "${candidate.eventName}"`
|
|
689
|
+
);
|
|
690
|
+
if (candidate.conditionName !== null) {
|
|
691
|
+
parts.push(`if ${candidate.conditionName}`);
|
|
692
|
+
}
|
|
693
|
+
parts.push(`weight ${candidate.weight}`);
|
|
694
|
+
return parts.join(" ");
|
|
695
|
+
}
|
|
664
696
|
|
|
665
697
|
// src/selector/OneOrNoneActiveTransition.ts
|
|
666
698
|
var OneOrNoneActiveTransition = class {
|
|
@@ -672,7 +704,15 @@ var OneOrNoneActiveTransition = class {
|
|
|
672
704
|
case 1:
|
|
673
705
|
return arr[0];
|
|
674
706
|
default:
|
|
675
|
-
throw new AmbiguousTransitionError(
|
|
707
|
+
throw new AmbiguousTransitionError(
|
|
708
|
+
arr.length,
|
|
709
|
+
arr.map((transition) => ({
|
|
710
|
+
targetStateName: transition.getTargetState().getName(),
|
|
711
|
+
eventName: transition.getEventName(),
|
|
712
|
+
conditionName: transition.getConditionName(),
|
|
713
|
+
weight: transition.getWeight()
|
|
714
|
+
}))
|
|
715
|
+
);
|
|
676
716
|
}
|
|
677
717
|
}
|
|
678
718
|
};
|
|
@@ -708,6 +748,9 @@ var OperationQueue = class {
|
|
|
708
748
|
isEmpty() {
|
|
709
749
|
return this.items.length === 0;
|
|
710
750
|
}
|
|
751
|
+
size() {
|
|
752
|
+
return this.items.length;
|
|
753
|
+
}
|
|
711
754
|
};
|
|
712
755
|
|
|
713
756
|
// src/filter/ActiveTransitionFilter.ts
|
|
@@ -746,6 +789,15 @@ var LockCanNotBeAcquiredError = class extends FinitaError {
|
|
|
746
789
|
}
|
|
747
790
|
};
|
|
748
791
|
|
|
792
|
+
// src/error/LockCanNotBeReleasedError.ts
|
|
793
|
+
var LockCanNotBeReleasedError = class extends FinitaError {
|
|
794
|
+
code = "lockCanNotBeReleased";
|
|
795
|
+
constructor(message = "Lock can not be released! releaseLock() returned false; the lock may still be held.") {
|
|
796
|
+
super(message);
|
|
797
|
+
this.name = "LockCanNotBeReleasedError";
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
749
801
|
// src/error/AutomaticTransitionCycleError.ts
|
|
750
802
|
var AutomaticTransitionCycleError = class extends FinitaError {
|
|
751
803
|
code = "automaticTransitionCycle";
|
|
@@ -772,6 +824,17 @@ var ReentrancyError = class extends FinitaError {
|
|
|
772
824
|
}
|
|
773
825
|
};
|
|
774
826
|
|
|
827
|
+
// src/error/QueueLimitExceededError.ts
|
|
828
|
+
var QueueLimitExceededError = class extends FinitaError {
|
|
829
|
+
code = "queueLimitExceeded";
|
|
830
|
+
constructor(limit, eventName) {
|
|
831
|
+
super(
|
|
832
|
+
`${eventName === null ? "checkTransitions()" : `triggerEvent("${eventName}")`} rejected: the operation queue already holds ${limit} pending operation(s) (maxQueueLength = ${limit}).`
|
|
833
|
+
);
|
|
834
|
+
this.name = "QueueLimitExceededError";
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
|
|
775
838
|
// src/Statemachine.ts
|
|
776
839
|
var Statemachine = class {
|
|
777
840
|
subject;
|
|
@@ -782,11 +845,15 @@ var Statemachine = class {
|
|
|
782
845
|
lastState = null;
|
|
783
846
|
autoreleaseLock;
|
|
784
847
|
maxAutomaticHops;
|
|
848
|
+
maxQueueLength;
|
|
785
849
|
queue = new OperationQueue();
|
|
786
850
|
running = false;
|
|
851
|
+
idleWaiters = [];
|
|
787
852
|
inSyncCallback = false;
|
|
788
853
|
beforeObservers = [];
|
|
789
854
|
afterObservers = [];
|
|
855
|
+
onChainedOperationError;
|
|
856
|
+
onReleaseError;
|
|
790
857
|
constructor(subject, process, options = {}) {
|
|
791
858
|
this.subject = subject;
|
|
792
859
|
this.process = process;
|
|
@@ -801,6 +868,15 @@ var Statemachine = class {
|
|
|
801
868
|
);
|
|
802
869
|
}
|
|
803
870
|
this.maxAutomaticHops = hops;
|
|
871
|
+
const maxQueue = options.maxQueueLength ?? Infinity;
|
|
872
|
+
if (maxQueue !== Infinity && (!Number.isInteger(maxQueue) || maxQueue < 1)) {
|
|
873
|
+
throw new RangeError(
|
|
874
|
+
`maxQueueLength must be a positive integer; got ${String(options.maxQueueLength)}`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
this.maxQueueLength = maxQueue;
|
|
878
|
+
this.onChainedOperationError = options.onChainedOperationError;
|
|
879
|
+
this.onReleaseError = options.onReleaseError;
|
|
804
880
|
}
|
|
805
881
|
// --- public getters ---
|
|
806
882
|
getCurrentState() {
|
|
@@ -817,31 +893,43 @@ var Statemachine = class {
|
|
|
817
893
|
}
|
|
818
894
|
// --- public observer attach/detach ---
|
|
819
895
|
attachBefore(observer) {
|
|
896
|
+
if (this.beforeObservers.includes(observer)) return;
|
|
820
897
|
this.beforeObservers.push(observer);
|
|
821
898
|
}
|
|
822
899
|
detachBefore(observer) {
|
|
823
900
|
const idx = this.beforeObservers.indexOf(observer);
|
|
824
901
|
if (idx >= 0) this.beforeObservers.splice(idx, 1);
|
|
825
902
|
}
|
|
903
|
+
/** Snapshot — detaching later does not change an already-returned list,
|
|
904
|
+
* and mutating it does not change the machine's registrations. */
|
|
826
905
|
getBeforeObservers() {
|
|
827
|
-
return this.beforeObservers;
|
|
906
|
+
return [...this.beforeObservers];
|
|
828
907
|
}
|
|
829
908
|
attachAfter(observer) {
|
|
909
|
+
if (this.afterObservers.includes(observer)) return;
|
|
830
910
|
this.afterObservers.push(observer);
|
|
831
911
|
}
|
|
832
912
|
detachAfter(observer) {
|
|
833
913
|
const idx = this.afterObservers.indexOf(observer);
|
|
834
914
|
if (idx >= 0) this.afterObservers.splice(idx, 1);
|
|
835
915
|
}
|
|
916
|
+
/** Snapshot — see getBeforeObservers. */
|
|
836
917
|
getAfterObservers() {
|
|
837
|
-
return this.afterObservers;
|
|
918
|
+
return [...this.afterObservers];
|
|
838
919
|
}
|
|
839
920
|
// --- public locking ---
|
|
840
921
|
async acquireLock() {
|
|
841
922
|
return this.mutex.acquireLock();
|
|
842
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* Releases the mutex. A failed release — whether the mutex throws or
|
|
926
|
+
* returns false — is reported to the onReleaseError hook; it is not thrown,
|
|
927
|
+
* so manual lock management keeps its existing control flow. Inspect
|
|
928
|
+
* isLockAcquired() (or the hook) to learn whether the lock was actually
|
|
929
|
+
* freed.
|
|
930
|
+
*/
|
|
843
931
|
async releaseLock() {
|
|
844
|
-
await this.
|
|
932
|
+
await this.releaseMutex();
|
|
845
933
|
}
|
|
846
934
|
isLockAcquired() {
|
|
847
935
|
return this.mutex.isAcquired();
|
|
@@ -865,6 +953,27 @@ var Statemachine = class {
|
|
|
865
953
|
this.enqueueOperation(null, context, resolve, reject);
|
|
866
954
|
});
|
|
867
955
|
}
|
|
956
|
+
/**
|
|
957
|
+
* Resolves once the operation queue is empty and the runner is idle —
|
|
958
|
+
* i.e. every operation enqueued so far, including operations chained via
|
|
959
|
+
* EnqueueContext.enqueue(), has completed. Resolves immediately if the
|
|
960
|
+
* machine is already idle. Note this is a quiescence point, not a
|
|
961
|
+
* receipt: work scheduled later (e.g. from a timer) starts a new drain.
|
|
962
|
+
*
|
|
963
|
+
* Like triggerEvent/checkTransitions, this may not be called from inside an
|
|
964
|
+
* observer or condition of the same machine: the machine cannot reach idle
|
|
965
|
+
* while the runner is blocked on that very callback, so awaiting it there
|
|
966
|
+
* always deadlocks.
|
|
967
|
+
*/
|
|
968
|
+
whenIdle() {
|
|
969
|
+
this.assertNotReentrant("whenIdle()");
|
|
970
|
+
if (!this.running && this.queue.isEmpty()) {
|
|
971
|
+
return Promise.resolve();
|
|
972
|
+
}
|
|
973
|
+
return new Promise((resolve) => {
|
|
974
|
+
this.idleWaiters.push(resolve);
|
|
975
|
+
});
|
|
976
|
+
}
|
|
868
977
|
/** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
|
|
869
978
|
* the flag is cleared as soon as fn returns (before any promise it returned
|
|
870
979
|
* is awaited), so concurrent external callers are never affected. This
|
|
@@ -886,6 +995,9 @@ var Statemachine = class {
|
|
|
886
995
|
}
|
|
887
996
|
/** Single entry point to the operation queue — every enqueue kicks the runner. */
|
|
888
997
|
enqueueOperation(eventName, context, resolve, reject, ifStateName) {
|
|
998
|
+
if (this.queue.size() >= this.maxQueueLength) {
|
|
999
|
+
throw new QueueLimitExceededError(this.maxQueueLength, eventName);
|
|
1000
|
+
}
|
|
889
1001
|
this.queue.enqueue({
|
|
890
1002
|
eventName,
|
|
891
1003
|
context: context ?? /* @__PURE__ */ new Map(),
|
|
@@ -906,6 +1018,11 @@ var Statemachine = class {
|
|
|
906
1018
|
}
|
|
907
1019
|
} finally {
|
|
908
1020
|
this.running = false;
|
|
1021
|
+
if (this.queue.isEmpty() && this.idleWaiters.length > 0) {
|
|
1022
|
+
const waiters = this.idleWaiters;
|
|
1023
|
+
this.idleWaiters = [];
|
|
1024
|
+
for (const waiter of waiters) waiter();
|
|
1025
|
+
}
|
|
909
1026
|
}
|
|
910
1027
|
}
|
|
911
1028
|
async runOperation(op) {
|
|
@@ -928,11 +1045,8 @@ var Statemachine = class {
|
|
|
928
1045
|
failure = { err };
|
|
929
1046
|
} finally {
|
|
930
1047
|
if (acquiredHere && this.autoreleaseLock) {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
} catch (err) {
|
|
934
|
-
if (!failure) failure = { err };
|
|
935
|
-
}
|
|
1048
|
+
const releaseFailure = await this.releaseMutex();
|
|
1049
|
+
if (releaseFailure && !failure) failure = releaseFailure;
|
|
936
1050
|
}
|
|
937
1051
|
}
|
|
938
1052
|
if (failure) {
|
|
@@ -941,6 +1055,36 @@ var Statemachine = class {
|
|
|
941
1055
|
op.resolve();
|
|
942
1056
|
}
|
|
943
1057
|
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Releases the mutex, normalizing its two failure modes into one result: a
|
|
1060
|
+
* thrown error, and a false return — the failure signal MutexInterface /
|
|
1061
|
+
* LockAdapterInterface define (a PostgreSQL advisory unlock that returns
|
|
1062
|
+
* false, a Redis DEL that removed nothing). A false return means the lock
|
|
1063
|
+
* may still be held, so it must never be mistaken for a successful release.
|
|
1064
|
+
*
|
|
1065
|
+
* Every failure is surfaced through the diagnostic hook — when the
|
|
1066
|
+
* operation also failed, the rejection carries the operation error and this
|
|
1067
|
+
* hook is the only place the release error appears.
|
|
1068
|
+
*
|
|
1069
|
+
* @returns null on success, or the failure wrapped for the caller to raise.
|
|
1070
|
+
*/
|
|
1071
|
+
async releaseMutex() {
|
|
1072
|
+
let failure = null;
|
|
1073
|
+
try {
|
|
1074
|
+
if (!await this.mutex.releaseLock()) {
|
|
1075
|
+
failure = { err: new LockCanNotBeReleasedError() };
|
|
1076
|
+
}
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
failure = { err };
|
|
1079
|
+
}
|
|
1080
|
+
if (failure) {
|
|
1081
|
+
try {
|
|
1082
|
+
this.onReleaseError?.(failure.err);
|
|
1083
|
+
} catch {
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return failure;
|
|
1087
|
+
}
|
|
944
1088
|
resolveEvent(name) {
|
|
945
1089
|
if (!this.currentState.hasEvent(name)) {
|
|
946
1090
|
throw new WrongEventForStateError(this.currentState.getName(), name);
|
|
@@ -1011,7 +1155,13 @@ var Statemachine = class {
|
|
|
1011
1155
|
chainedCtx,
|
|
1012
1156
|
() => {
|
|
1013
1157
|
},
|
|
1014
|
-
() => {
|
|
1158
|
+
(err) => {
|
|
1159
|
+
try {
|
|
1160
|
+
this.onChainedOperationError?.(err, {
|
|
1161
|
+
eventName: chainedEventName
|
|
1162
|
+
});
|
|
1163
|
+
} catch {
|
|
1164
|
+
}
|
|
1015
1165
|
},
|
|
1016
1166
|
ifStateName
|
|
1017
1167
|
);
|
|
@@ -1376,6 +1526,11 @@ var WeightTransition = class {
|
|
|
1376
1526
|
innerSelector;
|
|
1377
1527
|
epsilon;
|
|
1378
1528
|
constructor(innerSelector, epsilon = 1e-3) {
|
|
1529
|
+
if (!Number.isFinite(epsilon) || epsilon <= 0) {
|
|
1530
|
+
throw new RangeError(
|
|
1531
|
+
`WeightTransition epsilon must be a finite number greater than 0; got ${String(epsilon)}`
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1379
1534
|
this.innerSelector = innerSelector ?? new OneOrNoneActiveTransition();
|
|
1380
1535
|
this.epsilon = epsilon;
|
|
1381
1536
|
}
|
|
@@ -1384,6 +1539,11 @@ var WeightTransition = class {
|
|
|
1384
1539
|
let maxWeight = Number.NEGATIVE_INFINITY;
|
|
1385
1540
|
for (const transition of all) {
|
|
1386
1541
|
const weight = transition.getWeight();
|
|
1542
|
+
if (!Number.isFinite(weight)) {
|
|
1543
|
+
throw new RangeError(
|
|
1544
|
+
`WeightTransition: transition weights must be finite numbers; got ${String(weight)}`
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1387
1547
|
if (weight > maxWeight) maxWeight = weight;
|
|
1388
1548
|
}
|
|
1389
1549
|
const best = all.filter(
|
|
@@ -1398,15 +1558,31 @@ var LockAdapterMutex = class {
|
|
|
1398
1558
|
lockAdapter;
|
|
1399
1559
|
resourceName;
|
|
1400
1560
|
acquired = false;
|
|
1561
|
+
pendingAcquire = null;
|
|
1401
1562
|
constructor(lockAdapter, resourceName) {
|
|
1402
1563
|
this.lockAdapter = lockAdapter;
|
|
1403
1564
|
this.resourceName = resourceName;
|
|
1404
1565
|
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Overlapping calls share one underlying acquire: the `acquired` flag is
|
|
1568
|
+
* only set after the adapter resolves, so without this both callers would
|
|
1569
|
+
* pass the check and acquire twice on a non-idempotent adapter (database
|
|
1570
|
+
* advisory locks, redis SET NX). The pending promise is cleared once it
|
|
1571
|
+
* settles, so a failed acquire can still be retried.
|
|
1572
|
+
*/
|
|
1405
1573
|
async acquireLock() {
|
|
1406
|
-
if (
|
|
1407
|
-
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1574
|
+
if (this.acquired) {
|
|
1575
|
+
return true;
|
|
1576
|
+
}
|
|
1577
|
+
this.pendingAcquire ??= (async () => {
|
|
1578
|
+
try {
|
|
1579
|
+
this.acquired = await this.lockAdapter.acquireLock(this.resourceName);
|
|
1580
|
+
return this.acquired;
|
|
1581
|
+
} finally {
|
|
1582
|
+
this.pendingAcquire = null;
|
|
1583
|
+
}
|
|
1584
|
+
})();
|
|
1585
|
+
return this.pendingAcquire;
|
|
1410
1586
|
}
|
|
1411
1587
|
async releaseLock() {
|
|
1412
1588
|
if (this.acquired) {
|
|
@@ -1450,9 +1626,18 @@ var Factory = class {
|
|
|
1450
1626
|
afterObservers = /* @__PURE__ */ new Set();
|
|
1451
1627
|
transitionSelector = null;
|
|
1452
1628
|
mutexFactory = null;
|
|
1453
|
-
|
|
1629
|
+
options;
|
|
1630
|
+
/**
|
|
1631
|
+
* @param options Engine options applied to every machine this factory
|
|
1632
|
+
* creates — back-pressure (maxQueueLength), the automatic-hop bound, lock
|
|
1633
|
+
* autorelease, and the onChainedOperationError / onReleaseError diagnostic
|
|
1634
|
+
* sinks. Without them, factory-created machines would silently run on
|
|
1635
|
+
* defaults, which is precisely where those sinks matter most.
|
|
1636
|
+
*/
|
|
1637
|
+
constructor(processDetector, stateNameDetector, options = {}) {
|
|
1454
1638
|
this.processDetector = processDetector;
|
|
1455
1639
|
this.stateNameDetector = stateNameDetector ?? null;
|
|
1640
|
+
this.options = { ...options };
|
|
1456
1641
|
}
|
|
1457
1642
|
setMutexFactory(factory) {
|
|
1458
1643
|
this.mutexFactory = factory;
|
|
@@ -1477,6 +1662,7 @@ var Factory = class {
|
|
|
1477
1662
|
const stateName = this.stateNameDetector ? this.stateNameDetector.detectCurrentStateName(subject) : void 0;
|
|
1478
1663
|
const mutex = this.mutexFactory ? await this.mutexFactory.createMutex(subject) : void 0;
|
|
1479
1664
|
const sm = new Statemachine(subject, process, {
|
|
1665
|
+
...this.options,
|
|
1480
1666
|
initialStateName: stateName ?? void 0,
|
|
1481
1667
|
transitionSelector: this.transitionSelector ?? void 0,
|
|
1482
1668
|
mutex: mutex ?? void 0
|
|
@@ -1558,6 +1744,14 @@ function toMermaidId(name) {
|
|
|
1558
1744
|
function escapeMermaidLabel(str) {
|
|
1559
1745
|
return str.replace(/\\/g, "#92;").replace(/"/g, "#quot;");
|
|
1560
1746
|
}
|
|
1747
|
+
var VALID_DIRECTIONS = /* @__PURE__ */ new Set(["TB", "BT", "LR", "RL"]);
|
|
1748
|
+
function assertDirection(value, optionName) {
|
|
1749
|
+
if (!VALID_DIRECTIONS.has(value)) {
|
|
1750
|
+
throw new RangeError(
|
|
1751
|
+
`${optionName} must be one of "TB", "BT", "LR", "RL"; got ${JSON.stringify(value)}`
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1561
1755
|
var GraphBuilder = class {
|
|
1562
1756
|
nodes = /* @__PURE__ */ new Map();
|
|
1563
1757
|
edges = [];
|
|
@@ -1633,6 +1827,7 @@ var GraphBuilder = class {
|
|
|
1633
1827
|
toDot(options) {
|
|
1634
1828
|
const graph = this.getGraph();
|
|
1635
1829
|
const rankdir = options?.rankdir ?? "LR";
|
|
1830
|
+
assertDirection(rankdir, "rankdir");
|
|
1636
1831
|
const lines = [];
|
|
1637
1832
|
lines.push("digraph {");
|
|
1638
1833
|
lines.push(` rankdir=${rankdir};`);
|
|
@@ -1652,6 +1847,7 @@ var GraphBuilder = class {
|
|
|
1652
1847
|
toMermaid(options) {
|
|
1653
1848
|
const graph = this.getGraph();
|
|
1654
1849
|
const direction = options?.direction ?? "LR";
|
|
1850
|
+
assertDirection(direction, "direction");
|
|
1655
1851
|
const lines = [];
|
|
1656
1852
|
lines.push(`stateDiagram-v2`);
|
|
1657
1853
|
lines.push(` direction ${direction}`);
|
|
@@ -1697,6 +1893,7 @@ var GraphBuilder = class {
|
|
|
1697
1893
|
InvalidSubjectError,
|
|
1698
1894
|
LockAdapterMutex,
|
|
1699
1895
|
LockCanNotBeAcquiredError,
|
|
1896
|
+
LockCanNotBeReleasedError,
|
|
1700
1897
|
MutexFactory,
|
|
1701
1898
|
Not,
|
|
1702
1899
|
NullMutex,
|
|
@@ -1707,6 +1904,7 @@ var GraphBuilder = class {
|
|
|
1707
1904
|
ProcessBuilder,
|
|
1708
1905
|
ProcessFinalizedError,
|
|
1709
1906
|
ProcessNotFoundError,
|
|
1907
|
+
QueueLimitExceededError,
|
|
1710
1908
|
ReentrancyError,
|
|
1711
1909
|
ScoreTransition,
|
|
1712
1910
|
SingleProcessDetector,
|