@camcima/finita 4.1.0 → 4.3.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/CHANGELOG.md +141 -0
- package/dist/index.cjs +258 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +156 -18
- package/dist/index.d.ts +156 -18
- package/dist/index.js +256 -59
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -31,8 +31,10 @@ var Event = class {
|
|
|
31
31
|
await observer.update(this, args);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/** Snapshot — detaching later does not change an already-returned list,
|
|
35
|
+
* and mutating it does not change the event's registrations. */
|
|
34
36
|
getObservers() {
|
|
35
|
-
return this.observers;
|
|
37
|
+
return [...this.observers];
|
|
36
38
|
}
|
|
37
39
|
getMetadata() {
|
|
38
40
|
return Object.fromEntries(this.metadata);
|
|
@@ -58,8 +60,8 @@ var INTERNAL_CONSTRUCTION_KEY = /* @__PURE__ */ Symbol(
|
|
|
58
60
|
|
|
59
61
|
// src/error/FinitaError.ts
|
|
60
62
|
var FinitaError = class _FinitaError extends Error {
|
|
61
|
-
constructor(message) {
|
|
62
|
-
super(message);
|
|
63
|
+
constructor(message, options) {
|
|
64
|
+
super(message, options);
|
|
63
65
|
if (new.target === _FinitaError) {
|
|
64
66
|
throw new TypeError(
|
|
65
67
|
"FinitaError is abstract and cannot be instantiated directly"
|
|
@@ -189,11 +191,13 @@ var State = class {
|
|
|
189
191
|
getName() {
|
|
190
192
|
return this.name;
|
|
191
193
|
}
|
|
194
|
+
/** Snapshot — the graph is shared by every machine built from the
|
|
195
|
+
* process, so callers must never receive the collection itself. */
|
|
192
196
|
getTransitions() {
|
|
193
197
|
if (this._transitions === null) {
|
|
194
198
|
return [];
|
|
195
199
|
}
|
|
196
|
-
return this._transitions;
|
|
200
|
+
return Array.from(this._transitions);
|
|
197
201
|
}
|
|
198
202
|
getEventNames() {
|
|
199
203
|
return Array.from(this.events.keys());
|
|
@@ -472,9 +476,11 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
472
476
|
}
|
|
473
477
|
}
|
|
474
478
|
/** Transition identity: (fromState, eventName, toState). Used by both the
|
|
475
|
-
* conflict check and the build-time dedup — keep them in lockstep.
|
|
479
|
+
* conflict check and the build-time dedup — keep them in lockstep.
|
|
480
|
+
* Encoded as a JSON tuple, not a delimiter join: names may contain any
|
|
481
|
+
* character, so no delimiter can keep distinct tuples distinct. */
|
|
476
482
|
static transitionKey(t) {
|
|
477
|
-
return
|
|
483
|
+
return JSON.stringify([t.fromState, t.eventName, t.toState]);
|
|
478
484
|
}
|
|
479
485
|
validateNoConflictingDuplicates() {
|
|
480
486
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -593,12 +599,30 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
593
599
|
var AmbiguousTransitionError = class extends FinitaError {
|
|
594
600
|
code = "ambiguousTransition";
|
|
595
601
|
activeCount;
|
|
596
|
-
|
|
597
|
-
|
|
602
|
+
/** The competing transitions — what you need to resolve the ambiguity. */
|
|
603
|
+
candidates;
|
|
604
|
+
constructor(activeCount, candidates = []) {
|
|
605
|
+
const list = Array.from(candidates, (c) => Object.freeze({ ...c }));
|
|
606
|
+
const detail = list.length > 0 ? ` Candidates: ${list.map(describeCandidate).join("; ")}.` : "";
|
|
607
|
+
super(
|
|
608
|
+
`More than one transition is active! (active count: ${activeCount})${detail}`
|
|
609
|
+
);
|
|
598
610
|
this.name = "AmbiguousTransitionError";
|
|
599
611
|
this.activeCount = activeCount;
|
|
612
|
+
this.candidates = Object.freeze(list);
|
|
600
613
|
}
|
|
601
614
|
};
|
|
615
|
+
function describeCandidate(candidate) {
|
|
616
|
+
const parts = [`-> "${candidate.targetStateName}"`];
|
|
617
|
+
parts.push(
|
|
618
|
+
candidate.eventName === null ? "on <automatic>" : `on event "${candidate.eventName}"`
|
|
619
|
+
);
|
|
620
|
+
if (candidate.conditionName !== null) {
|
|
621
|
+
parts.push(`if ${candidate.conditionName}`);
|
|
622
|
+
}
|
|
623
|
+
parts.push(`weight ${candidate.weight}`);
|
|
624
|
+
return parts.join(" ");
|
|
625
|
+
}
|
|
602
626
|
|
|
603
627
|
// src/selector/OneOrNoneActiveTransition.ts
|
|
604
628
|
var OneOrNoneActiveTransition = class {
|
|
@@ -610,7 +634,15 @@ var OneOrNoneActiveTransition = class {
|
|
|
610
634
|
case 1:
|
|
611
635
|
return arr[0];
|
|
612
636
|
default:
|
|
613
|
-
throw new AmbiguousTransitionError(
|
|
637
|
+
throw new AmbiguousTransitionError(
|
|
638
|
+
arr.length,
|
|
639
|
+
arr.map((transition) => ({
|
|
640
|
+
targetStateName: transition.getTargetState().getName(),
|
|
641
|
+
eventName: transition.getEventName(),
|
|
642
|
+
conditionName: transition.getConditionName(),
|
|
643
|
+
weight: transition.getWeight()
|
|
644
|
+
}))
|
|
645
|
+
);
|
|
614
646
|
}
|
|
615
647
|
}
|
|
616
648
|
};
|
|
@@ -635,19 +667,34 @@ var NullMutex = class {
|
|
|
635
667
|
};
|
|
636
668
|
|
|
637
669
|
// src/internal/OperationQueue.ts
|
|
638
|
-
var OperationQueue = class {
|
|
670
|
+
var OperationQueue = class _OperationQueue {
|
|
671
|
+
static COMPACT_THRESHOLD = 1024;
|
|
639
672
|
items = [];
|
|
673
|
+
head = 0;
|
|
640
674
|
enqueue(op) {
|
|
641
675
|
this.items.push(op);
|
|
642
676
|
}
|
|
643
677
|
dequeue() {
|
|
644
|
-
|
|
678
|
+
if (this.head >= this.items.length) {
|
|
679
|
+
return void 0;
|
|
680
|
+
}
|
|
681
|
+
const op = this.items[this.head];
|
|
682
|
+
this.items[this.head] = void 0;
|
|
683
|
+
this.head++;
|
|
684
|
+
if (this.head === this.items.length) {
|
|
685
|
+
this.items = [];
|
|
686
|
+
this.head = 0;
|
|
687
|
+
} else if (this.head >= _OperationQueue.COMPACT_THRESHOLD && this.head * 2 >= this.items.length) {
|
|
688
|
+
this.items = this.items.slice(this.head);
|
|
689
|
+
this.head = 0;
|
|
690
|
+
}
|
|
691
|
+
return op;
|
|
645
692
|
}
|
|
646
693
|
isEmpty() {
|
|
647
|
-
return this.items.length
|
|
694
|
+
return this.head === this.items.length;
|
|
648
695
|
}
|
|
649
696
|
size() {
|
|
650
|
-
return this.items.length;
|
|
697
|
+
return this.items.length - this.head;
|
|
651
698
|
}
|
|
652
699
|
};
|
|
653
700
|
|
|
@@ -687,6 +734,27 @@ var LockCanNotBeAcquiredError = class extends FinitaError {
|
|
|
687
734
|
}
|
|
688
735
|
};
|
|
689
736
|
|
|
737
|
+
// src/error/LockCanNotBeReleasedError.ts
|
|
738
|
+
var LockCanNotBeReleasedError = class extends FinitaError {
|
|
739
|
+
code = "lockCanNotBeReleased";
|
|
740
|
+
constructor(message = "Lock can not be released! releaseLock() returned false; the lock may still be held.") {
|
|
741
|
+
super(message);
|
|
742
|
+
this.name = "LockCanNotBeReleasedError";
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// src/error/LockOwnershipUncertainError.ts
|
|
747
|
+
var LockOwnershipUncertainError = class extends FinitaError {
|
|
748
|
+
code = "lockOwnershipUncertain";
|
|
749
|
+
constructor(cause) {
|
|
750
|
+
super(
|
|
751
|
+
"Operation rejected: a previous lock release failed, so this machine cannot tell whether it still holds the lock. Call releaseLock() and confirm it succeeds, or discard the machine and rebuild it from persisted state.",
|
|
752
|
+
{ cause }
|
|
753
|
+
);
|
|
754
|
+
this.name = "LockOwnershipUncertainError";
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
690
758
|
// src/error/AutomaticTransitionCycleError.ts
|
|
691
759
|
var AutomaticTransitionCycleError = class extends FinitaError {
|
|
692
760
|
code = "automaticTransitionCycle";
|
|
@@ -724,6 +792,18 @@ var QueueLimitExceededError = class extends FinitaError {
|
|
|
724
792
|
}
|
|
725
793
|
};
|
|
726
794
|
|
|
795
|
+
// src/util/index.ts
|
|
796
|
+
function isNamed(obj) {
|
|
797
|
+
return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
|
|
798
|
+
}
|
|
799
|
+
function nameOrString(obj) {
|
|
800
|
+
if (isNamed(obj)) return obj.getName();
|
|
801
|
+
return String(obj);
|
|
802
|
+
}
|
|
803
|
+
function isPromiseLike(value) {
|
|
804
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
805
|
+
}
|
|
806
|
+
|
|
727
807
|
// src/Statemachine.ts
|
|
728
808
|
var Statemachine = class {
|
|
729
809
|
subject;
|
|
@@ -739,6 +819,8 @@ var Statemachine = class {
|
|
|
739
819
|
running = false;
|
|
740
820
|
idleWaiters = [];
|
|
741
821
|
inSyncCallback = false;
|
|
822
|
+
/** Set when releasing a held lock fails; see LockOwnershipUncertainError. */
|
|
823
|
+
ownershipUncertainty = null;
|
|
742
824
|
beforeObservers = [];
|
|
743
825
|
afterObservers = [];
|
|
744
826
|
onChainedOperationError;
|
|
@@ -789,8 +871,10 @@ var Statemachine = class {
|
|
|
789
871
|
const idx = this.beforeObservers.indexOf(observer);
|
|
790
872
|
if (idx >= 0) this.beforeObservers.splice(idx, 1);
|
|
791
873
|
}
|
|
874
|
+
/** Snapshot — detaching later does not change an already-returned list,
|
|
875
|
+
* and mutating it does not change the machine's registrations. */
|
|
792
876
|
getBeforeObservers() {
|
|
793
|
-
return this.beforeObservers;
|
|
877
|
+
return [...this.beforeObservers];
|
|
794
878
|
}
|
|
795
879
|
attachAfter(observer) {
|
|
796
880
|
if (this.afterObservers.includes(observer)) return;
|
|
@@ -800,15 +884,26 @@ var Statemachine = class {
|
|
|
800
884
|
const idx = this.afterObservers.indexOf(observer);
|
|
801
885
|
if (idx >= 0) this.afterObservers.splice(idx, 1);
|
|
802
886
|
}
|
|
887
|
+
/** Snapshot — see getBeforeObservers. */
|
|
803
888
|
getAfterObservers() {
|
|
804
|
-
return this.afterObservers;
|
|
889
|
+
return [...this.afterObservers];
|
|
805
890
|
}
|
|
806
891
|
// --- public locking ---
|
|
807
892
|
async acquireLock() {
|
|
808
893
|
return this.mutex.acquireLock();
|
|
809
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* Releases the mutex. A failed release — whether the mutex throws or
|
|
897
|
+
* returns false — is reported to the onReleaseError hook; it is not thrown,
|
|
898
|
+
* so manual lock management keeps its existing control flow. Inspect
|
|
899
|
+
* isLockAcquired() (or the hook) to learn whether the lock was actually
|
|
900
|
+
* freed.
|
|
901
|
+
*
|
|
902
|
+
* A failed release of a held lock makes every later operation reject with
|
|
903
|
+
* LockOwnershipUncertainError; a successful call here is how to recover.
|
|
904
|
+
*/
|
|
810
905
|
async releaseLock() {
|
|
811
|
-
await this.
|
|
906
|
+
await this.releaseMutex();
|
|
812
907
|
}
|
|
813
908
|
isLockAcquired() {
|
|
814
909
|
return this.mutex.isAcquired();
|
|
@@ -838,8 +933,14 @@ var Statemachine = class {
|
|
|
838
933
|
* EnqueueContext.enqueue(), has completed. Resolves immediately if the
|
|
839
934
|
* machine is already idle. Note this is a quiescence point, not a
|
|
840
935
|
* receipt: work scheduled later (e.g. from a timer) starts a new drain.
|
|
936
|
+
*
|
|
937
|
+
* Like triggerEvent/checkTransitions, this may not be called from inside an
|
|
938
|
+
* observer or condition of the same machine: the machine cannot reach idle
|
|
939
|
+
* while the runner is blocked on that very callback, so awaiting it there
|
|
940
|
+
* always deadlocks.
|
|
841
941
|
*/
|
|
842
942
|
whenIdle() {
|
|
943
|
+
this.assertNotReentrant("whenIdle()");
|
|
843
944
|
if (!this.running && this.queue.isEmpty()) {
|
|
844
945
|
return Promise.resolve();
|
|
845
946
|
}
|
|
@@ -899,6 +1000,10 @@ var Statemachine = class {
|
|
|
899
1000
|
}
|
|
900
1001
|
}
|
|
901
1002
|
async runOperation(op) {
|
|
1003
|
+
if (this.ownershipUncertainty) {
|
|
1004
|
+
op.reject(new LockOwnershipUncertainError(this.ownershipUncertainty.err));
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
902
1007
|
if (op.ifStateName !== void 0 && this.currentState.getName() !== op.ifStateName) {
|
|
903
1008
|
op.resolve();
|
|
904
1009
|
return;
|
|
@@ -918,15 +1023,8 @@ var Statemachine = class {
|
|
|
918
1023
|
failure = { err };
|
|
919
1024
|
} finally {
|
|
920
1025
|
if (acquiredHere && this.autoreleaseLock) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
} catch (err) {
|
|
924
|
-
try {
|
|
925
|
-
this.onReleaseError?.(err);
|
|
926
|
-
} catch {
|
|
927
|
-
}
|
|
928
|
-
if (!failure) failure = { err };
|
|
929
|
-
}
|
|
1026
|
+
const releaseFailure = await this.releaseMutex();
|
|
1027
|
+
if (releaseFailure && !failure) failure = releaseFailure;
|
|
930
1028
|
}
|
|
931
1029
|
}
|
|
932
1030
|
if (failure) {
|
|
@@ -935,6 +1033,60 @@ var Statemachine = class {
|
|
|
935
1033
|
op.resolve();
|
|
936
1034
|
}
|
|
937
1035
|
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Releases the mutex, normalizing its two failure modes into one result: a
|
|
1038
|
+
* thrown error, and a false return — the failure signal MutexInterface /
|
|
1039
|
+
* LockAdapterInterface define (a PostgreSQL advisory unlock that returns
|
|
1040
|
+
* false, a Redis DEL that removed nothing). A false return means the lock
|
|
1041
|
+
* may still be held, so it must never be mistaken for a successful release.
|
|
1042
|
+
*
|
|
1043
|
+
* Every failure is surfaced through the diagnostic hook — when the
|
|
1044
|
+
* operation also failed, the rejection carries the operation error and this
|
|
1045
|
+
* hook is the only place the release error appears.
|
|
1046
|
+
*
|
|
1047
|
+
* A failure while the mutex claimed to hold the lock leaves ownership
|
|
1048
|
+
* uncertain and blocks later operations; a success clears that state. A
|
|
1049
|
+
* failed release of a lock the mutex did not claim (a defensive manual
|
|
1050
|
+
* release) is still reported but changes nothing.
|
|
1051
|
+
*
|
|
1052
|
+
* @returns null on success, or the failure wrapped for the caller to raise.
|
|
1053
|
+
*/
|
|
1054
|
+
async releaseMutex() {
|
|
1055
|
+
const held = this.mutex.isAcquired();
|
|
1056
|
+
let failure = null;
|
|
1057
|
+
try {
|
|
1058
|
+
if (!await this.mutex.releaseLock()) {
|
|
1059
|
+
failure = { err: new LockCanNotBeReleasedError() };
|
|
1060
|
+
}
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
failure = { err };
|
|
1063
|
+
}
|
|
1064
|
+
if (failure) {
|
|
1065
|
+
if (held) this.ownershipUncertainty = failure;
|
|
1066
|
+
const err = failure.err;
|
|
1067
|
+
this.callDiagnosticHook(() => this.onReleaseError?.(err));
|
|
1068
|
+
} else {
|
|
1069
|
+
this.ownershipUncertainty = null;
|
|
1070
|
+
}
|
|
1071
|
+
return failure;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Runs a user diagnostic hook in isolation. Neither a synchronous throw nor
|
|
1075
|
+
* a rejection of a returned promise may reach the drain loop or the host:
|
|
1076
|
+
* an unavailable telemetry backend must not fail an operation or, via an
|
|
1077
|
+
* unhandled rejection, terminate the process. A returned promise is
|
|
1078
|
+
* deliberately not awaited — a slow reporter must not stall the runner.
|
|
1079
|
+
*/
|
|
1080
|
+
callDiagnosticHook(hook) {
|
|
1081
|
+
try {
|
|
1082
|
+
const result = hook();
|
|
1083
|
+
if (isPromiseLike(result)) {
|
|
1084
|
+
result.then(void 0, () => {
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
938
1090
|
resolveEvent(name) {
|
|
939
1091
|
if (!this.currentState.hasEvent(name)) {
|
|
940
1092
|
throw new WrongEventForStateError(this.currentState.getName(), name);
|
|
@@ -1006,12 +1158,11 @@ var Statemachine = class {
|
|
|
1006
1158
|
() => {
|
|
1007
1159
|
},
|
|
1008
1160
|
(err) => {
|
|
1009
|
-
|
|
1010
|
-
this.onChainedOperationError?.(err, {
|
|
1161
|
+
this.callDiagnosticHook(
|
|
1162
|
+
() => this.onChainedOperationError?.(err, {
|
|
1011
1163
|
eventName: chainedEventName
|
|
1012
|
-
})
|
|
1013
|
-
|
|
1014
|
-
}
|
|
1164
|
+
})
|
|
1165
|
+
);
|
|
1015
1166
|
},
|
|
1016
1167
|
ifStateName
|
|
1017
1168
|
);
|
|
@@ -1147,6 +1298,30 @@ var CompositeCondition = class {
|
|
|
1147
1298
|
const names = this.conditions.map((c) => c.getName());
|
|
1148
1299
|
return `(${names.join(` ${this.joinWord} `)})`;
|
|
1149
1300
|
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Evaluates children in order, stopping at the first whose result equals
|
|
1303
|
+
* `shortCircuitOn`. A child that returns a plain boolean is consumed
|
|
1304
|
+
* synchronously; only a returned promise is awaited. Awaiting plain values
|
|
1305
|
+
* would yield between children and end the machine's synchronous
|
|
1306
|
+
* re-entrancy guard, so a re-entrant later child would deadlock instead of
|
|
1307
|
+
* throwing ReentrancyError. For the same reason the composite itself
|
|
1308
|
+
* returns a plain boolean when every child it evaluated did.
|
|
1309
|
+
*/
|
|
1310
|
+
evaluate(subject, context, shortCircuitOn) {
|
|
1311
|
+
const from = (start) => {
|
|
1312
|
+
for (let i = start; i < this.conditions.length; i++) {
|
|
1313
|
+
const result = this.conditions[i].checkCondition(subject, context);
|
|
1314
|
+
if (isPromiseLike(result)) {
|
|
1315
|
+
return Promise.resolve(result).then(
|
|
1316
|
+
(value) => Boolean(value) === shortCircuitOn ? shortCircuitOn : from(i + 1)
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
if (Boolean(result) === shortCircuitOn) return shortCircuitOn;
|
|
1320
|
+
}
|
|
1321
|
+
return !shortCircuitOn;
|
|
1322
|
+
};
|
|
1323
|
+
return from(0);
|
|
1324
|
+
}
|
|
1150
1325
|
};
|
|
1151
1326
|
|
|
1152
1327
|
// src/condition/AndComposite.ts
|
|
@@ -1157,13 +1332,8 @@ var AndComposite = class extends CompositeCondition {
|
|
|
1157
1332
|
addAnd(condition) {
|
|
1158
1333
|
return this.addCondition(condition);
|
|
1159
1334
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
if (!await condition.checkCondition(subject, context)) {
|
|
1163
|
-
return false;
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
return true;
|
|
1335
|
+
checkCondition(subject, context) {
|
|
1336
|
+
return this.evaluate(subject, context, false);
|
|
1167
1337
|
}
|
|
1168
1338
|
};
|
|
1169
1339
|
|
|
@@ -1175,13 +1345,8 @@ var OrComposite = class extends CompositeCondition {
|
|
|
1175
1345
|
addOr(condition) {
|
|
1176
1346
|
return this.addCondition(condition);
|
|
1177
1347
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (await condition.checkCondition(subject, context)) {
|
|
1181
|
-
return true;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
return false;
|
|
1348
|
+
checkCondition(subject, context) {
|
|
1349
|
+
return this.evaluate(subject, context, true);
|
|
1185
1350
|
}
|
|
1186
1351
|
};
|
|
1187
1352
|
|
|
@@ -1194,8 +1359,13 @@ var Not = class {
|
|
|
1194
1359
|
getName() {
|
|
1195
1360
|
return `not ( ${this.condition.getName()} )`;
|
|
1196
1361
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1362
|
+
/** Stays synchronous for a synchronous child — see CompositeCondition. */
|
|
1363
|
+
checkCondition(subject, context) {
|
|
1364
|
+
const result = this.condition.checkCondition(subject, context);
|
|
1365
|
+
if (isPromiseLike(result)) {
|
|
1366
|
+
return Promise.resolve(result).then((value) => !value);
|
|
1367
|
+
}
|
|
1368
|
+
return !result;
|
|
1199
1369
|
}
|
|
1200
1370
|
};
|
|
1201
1371
|
|
|
@@ -1250,15 +1420,6 @@ var OnEnterObserver = class _OnEnterObserver {
|
|
|
1250
1420
|
}
|
|
1251
1421
|
};
|
|
1252
1422
|
|
|
1253
|
-
// src/util/index.ts
|
|
1254
|
-
function isNamed(obj) {
|
|
1255
|
-
return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
|
|
1256
|
-
}
|
|
1257
|
-
function nameOrString(obj) {
|
|
1258
|
-
if (isNamed(obj)) return obj.getName();
|
|
1259
|
-
return String(obj);
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
1423
|
// src/observer/TransitionLogger.ts
|
|
1263
1424
|
var TransitionLogger = class {
|
|
1264
1425
|
logger;
|
|
@@ -1408,15 +1569,39 @@ var LockAdapterMutex = class {
|
|
|
1408
1569
|
lockAdapter;
|
|
1409
1570
|
resourceName;
|
|
1410
1571
|
acquired = false;
|
|
1572
|
+
pendingAcquire = null;
|
|
1411
1573
|
constructor(lockAdapter, resourceName) {
|
|
1412
1574
|
this.lockAdapter = lockAdapter;
|
|
1413
1575
|
this.resourceName = resourceName;
|
|
1414
1576
|
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Overlapping calls share one underlying acquire: the `acquired` flag is
|
|
1579
|
+
* only set after the adapter resolves, so without this both callers would
|
|
1580
|
+
* pass the check and acquire twice on a non-idempotent adapter (database
|
|
1581
|
+
* advisory locks, redis SET NX). The pending promise is cleared once it
|
|
1582
|
+
* settles, so a failed acquire can still be retried.
|
|
1583
|
+
*
|
|
1584
|
+
* The clearing is attached to the attempt only after it is stored: an
|
|
1585
|
+
* adapter that throws synchronously settles the attempt before the
|
|
1586
|
+
* assignment would otherwise run, and clearing inside the attempt itself
|
|
1587
|
+
* would then leave the rejected promise cached forever.
|
|
1588
|
+
*/
|
|
1415
1589
|
async acquireLock() {
|
|
1416
|
-
if (
|
|
1417
|
-
|
|
1590
|
+
if (this.acquired) {
|
|
1591
|
+
return true;
|
|
1418
1592
|
}
|
|
1419
|
-
|
|
1593
|
+
if (!this.pendingAcquire) {
|
|
1594
|
+
const attempt = (async () => {
|
|
1595
|
+
this.acquired = await this.lockAdapter.acquireLock(this.resourceName);
|
|
1596
|
+
return this.acquired;
|
|
1597
|
+
})();
|
|
1598
|
+
this.pendingAcquire = attempt;
|
|
1599
|
+
const clear = () => {
|
|
1600
|
+
if (this.pendingAcquire === attempt) this.pendingAcquire = null;
|
|
1601
|
+
};
|
|
1602
|
+
attempt.then(clear, clear);
|
|
1603
|
+
}
|
|
1604
|
+
return this.pendingAcquire;
|
|
1420
1605
|
}
|
|
1421
1606
|
async releaseLock() {
|
|
1422
1607
|
if (this.acquired) {
|
|
@@ -1460,9 +1645,18 @@ var Factory = class {
|
|
|
1460
1645
|
afterObservers = /* @__PURE__ */ new Set();
|
|
1461
1646
|
transitionSelector = null;
|
|
1462
1647
|
mutexFactory = null;
|
|
1463
|
-
|
|
1648
|
+
options;
|
|
1649
|
+
/**
|
|
1650
|
+
* @param options Engine options applied to every machine this factory
|
|
1651
|
+
* creates — back-pressure (maxQueueLength), the automatic-hop bound, lock
|
|
1652
|
+
* autorelease, and the onChainedOperationError / onReleaseError diagnostic
|
|
1653
|
+
* sinks. Without them, factory-created machines would silently run on
|
|
1654
|
+
* defaults, which is precisely where those sinks matter most.
|
|
1655
|
+
*/
|
|
1656
|
+
constructor(processDetector, stateNameDetector, options = {}) {
|
|
1464
1657
|
this.processDetector = processDetector;
|
|
1465
1658
|
this.stateNameDetector = stateNameDetector ?? null;
|
|
1659
|
+
this.options = { ...options };
|
|
1466
1660
|
}
|
|
1467
1661
|
setMutexFactory(factory) {
|
|
1468
1662
|
this.mutexFactory = factory;
|
|
@@ -1487,6 +1681,7 @@ var Factory = class {
|
|
|
1487
1681
|
const stateName = this.stateNameDetector ? this.stateNameDetector.detectCurrentStateName(subject) : void 0;
|
|
1488
1682
|
const mutex = this.mutexFactory ? await this.mutexFactory.createMutex(subject) : void 0;
|
|
1489
1683
|
const sm = new Statemachine(subject, process, {
|
|
1684
|
+
...this.options,
|
|
1490
1685
|
initialStateName: stateName ?? void 0,
|
|
1491
1686
|
transitionSelector: this.transitionSelector ?? void 0,
|
|
1492
1687
|
mutex: mutex ?? void 0
|
|
@@ -1716,6 +1911,8 @@ export {
|
|
|
1716
1911
|
InvalidSubjectError,
|
|
1717
1912
|
LockAdapterMutex,
|
|
1718
1913
|
LockCanNotBeAcquiredError,
|
|
1914
|
+
LockCanNotBeReleasedError,
|
|
1915
|
+
LockOwnershipUncertainError,
|
|
1719
1916
|
MutexFactory,
|
|
1720
1917
|
Not,
|
|
1721
1918
|
NullMutex,
|