@camcima/finita 4.1.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 +125 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -9
- package/dist/index.d.ts +88 -9
- package/dist/index.js +124 -21
- 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,
|
|
@@ -104,8 +105,10 @@ var Event = class {
|
|
|
104
105
|
await observer.update(this, args);
|
|
105
106
|
}
|
|
106
107
|
}
|
|
108
|
+
/** Snapshot — detaching later does not change an already-returned list,
|
|
109
|
+
* and mutating it does not change the event's registrations. */
|
|
107
110
|
getObservers() {
|
|
108
|
-
return this.observers;
|
|
111
|
+
return [...this.observers];
|
|
109
112
|
}
|
|
110
113
|
getMetadata() {
|
|
111
114
|
return Object.fromEntries(this.metadata);
|
|
@@ -666,12 +669,30 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
666
669
|
var AmbiguousTransitionError = class extends FinitaError {
|
|
667
670
|
code = "ambiguousTransition";
|
|
668
671
|
activeCount;
|
|
669
|
-
|
|
670
|
-
|
|
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
|
+
);
|
|
671
680
|
this.name = "AmbiguousTransitionError";
|
|
672
681
|
this.activeCount = activeCount;
|
|
682
|
+
this.candidates = Object.freeze(list);
|
|
673
683
|
}
|
|
674
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
|
+
}
|
|
675
696
|
|
|
676
697
|
// src/selector/OneOrNoneActiveTransition.ts
|
|
677
698
|
var OneOrNoneActiveTransition = class {
|
|
@@ -683,7 +704,15 @@ var OneOrNoneActiveTransition = class {
|
|
|
683
704
|
case 1:
|
|
684
705
|
return arr[0];
|
|
685
706
|
default:
|
|
686
|
-
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
|
+
);
|
|
687
716
|
}
|
|
688
717
|
}
|
|
689
718
|
};
|
|
@@ -760,6 +789,15 @@ var LockCanNotBeAcquiredError = class extends FinitaError {
|
|
|
760
789
|
}
|
|
761
790
|
};
|
|
762
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
|
+
|
|
763
801
|
// src/error/AutomaticTransitionCycleError.ts
|
|
764
802
|
var AutomaticTransitionCycleError = class extends FinitaError {
|
|
765
803
|
code = "automaticTransitionCycle";
|
|
@@ -862,8 +900,10 @@ var Statemachine = class {
|
|
|
862
900
|
const idx = this.beforeObservers.indexOf(observer);
|
|
863
901
|
if (idx >= 0) this.beforeObservers.splice(idx, 1);
|
|
864
902
|
}
|
|
903
|
+
/** Snapshot — detaching later does not change an already-returned list,
|
|
904
|
+
* and mutating it does not change the machine's registrations. */
|
|
865
905
|
getBeforeObservers() {
|
|
866
|
-
return this.beforeObservers;
|
|
906
|
+
return [...this.beforeObservers];
|
|
867
907
|
}
|
|
868
908
|
attachAfter(observer) {
|
|
869
909
|
if (this.afterObservers.includes(observer)) return;
|
|
@@ -873,15 +913,23 @@ var Statemachine = class {
|
|
|
873
913
|
const idx = this.afterObservers.indexOf(observer);
|
|
874
914
|
if (idx >= 0) this.afterObservers.splice(idx, 1);
|
|
875
915
|
}
|
|
916
|
+
/** Snapshot — see getBeforeObservers. */
|
|
876
917
|
getAfterObservers() {
|
|
877
|
-
return this.afterObservers;
|
|
918
|
+
return [...this.afterObservers];
|
|
878
919
|
}
|
|
879
920
|
// --- public locking ---
|
|
880
921
|
async acquireLock() {
|
|
881
922
|
return this.mutex.acquireLock();
|
|
882
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
|
+
*/
|
|
883
931
|
async releaseLock() {
|
|
884
|
-
await this.
|
|
932
|
+
await this.releaseMutex();
|
|
885
933
|
}
|
|
886
934
|
isLockAcquired() {
|
|
887
935
|
return this.mutex.isAcquired();
|
|
@@ -911,8 +959,14 @@ var Statemachine = class {
|
|
|
911
959
|
* EnqueueContext.enqueue(), has completed. Resolves immediately if the
|
|
912
960
|
* machine is already idle. Note this is a quiescence point, not a
|
|
913
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.
|
|
914
967
|
*/
|
|
915
968
|
whenIdle() {
|
|
969
|
+
this.assertNotReentrant("whenIdle()");
|
|
916
970
|
if (!this.running && this.queue.isEmpty()) {
|
|
917
971
|
return Promise.resolve();
|
|
918
972
|
}
|
|
@@ -991,15 +1045,8 @@ var Statemachine = class {
|
|
|
991
1045
|
failure = { err };
|
|
992
1046
|
} finally {
|
|
993
1047
|
if (acquiredHere && this.autoreleaseLock) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
} catch (err) {
|
|
997
|
-
try {
|
|
998
|
-
this.onReleaseError?.(err);
|
|
999
|
-
} catch {
|
|
1000
|
-
}
|
|
1001
|
-
if (!failure) failure = { err };
|
|
1002
|
-
}
|
|
1048
|
+
const releaseFailure = await this.releaseMutex();
|
|
1049
|
+
if (releaseFailure && !failure) failure = releaseFailure;
|
|
1003
1050
|
}
|
|
1004
1051
|
}
|
|
1005
1052
|
if (failure) {
|
|
@@ -1008,6 +1055,36 @@ var Statemachine = class {
|
|
|
1008
1055
|
op.resolve();
|
|
1009
1056
|
}
|
|
1010
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
|
+
}
|
|
1011
1088
|
resolveEvent(name) {
|
|
1012
1089
|
if (!this.currentState.hasEvent(name)) {
|
|
1013
1090
|
throw new WrongEventForStateError(this.currentState.getName(), name);
|
|
@@ -1481,15 +1558,31 @@ var LockAdapterMutex = class {
|
|
|
1481
1558
|
lockAdapter;
|
|
1482
1559
|
resourceName;
|
|
1483
1560
|
acquired = false;
|
|
1561
|
+
pendingAcquire = null;
|
|
1484
1562
|
constructor(lockAdapter, resourceName) {
|
|
1485
1563
|
this.lockAdapter = lockAdapter;
|
|
1486
1564
|
this.resourceName = resourceName;
|
|
1487
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
|
+
*/
|
|
1488
1573
|
async acquireLock() {
|
|
1489
|
-
if (
|
|
1490
|
-
|
|
1491
|
-
}
|
|
1492
|
-
|
|
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;
|
|
1493
1586
|
}
|
|
1494
1587
|
async releaseLock() {
|
|
1495
1588
|
if (this.acquired) {
|
|
@@ -1533,9 +1626,18 @@ var Factory = class {
|
|
|
1533
1626
|
afterObservers = /* @__PURE__ */ new Set();
|
|
1534
1627
|
transitionSelector = null;
|
|
1535
1628
|
mutexFactory = null;
|
|
1536
|
-
|
|
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 = {}) {
|
|
1537
1638
|
this.processDetector = processDetector;
|
|
1538
1639
|
this.stateNameDetector = stateNameDetector ?? null;
|
|
1640
|
+
this.options = { ...options };
|
|
1539
1641
|
}
|
|
1540
1642
|
setMutexFactory(factory) {
|
|
1541
1643
|
this.mutexFactory = factory;
|
|
@@ -1560,6 +1662,7 @@ var Factory = class {
|
|
|
1560
1662
|
const stateName = this.stateNameDetector ? this.stateNameDetector.detectCurrentStateName(subject) : void 0;
|
|
1561
1663
|
const mutex = this.mutexFactory ? await this.mutexFactory.createMutex(subject) : void 0;
|
|
1562
1664
|
const sm = new Statemachine(subject, process, {
|
|
1665
|
+
...this.options,
|
|
1563
1666
|
initialStateName: stateName ?? void 0,
|
|
1564
1667
|
transitionSelector: this.transitionSelector ?? void 0,
|
|
1565
1668
|
mutex: mutex ?? void 0
|
|
@@ -1790,6 +1893,7 @@ var GraphBuilder = class {
|
|
|
1790
1893
|
InvalidSubjectError,
|
|
1791
1894
|
LockAdapterMutex,
|
|
1792
1895
|
LockCanNotBeAcquiredError,
|
|
1896
|
+
LockCanNotBeReleasedError,
|
|
1793
1897
|
MutexFactory,
|
|
1794
1898
|
Not,
|
|
1795
1899
|
NullMutex,
|