@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.cjs
CHANGED
|
@@ -42,6 +42,8 @@ __export(index_exports, {
|
|
|
42
42
|
InvalidSubjectError: () => InvalidSubjectError,
|
|
43
43
|
LockAdapterMutex: () => LockAdapterMutex,
|
|
44
44
|
LockCanNotBeAcquiredError: () => LockCanNotBeAcquiredError,
|
|
45
|
+
LockCanNotBeReleasedError: () => LockCanNotBeReleasedError,
|
|
46
|
+
LockOwnershipUncertainError: () => LockOwnershipUncertainError,
|
|
45
47
|
MutexFactory: () => MutexFactory,
|
|
46
48
|
Not: () => Not,
|
|
47
49
|
NullMutex: () => NullMutex,
|
|
@@ -104,8 +106,10 @@ var Event = class {
|
|
|
104
106
|
await observer.update(this, args);
|
|
105
107
|
}
|
|
106
108
|
}
|
|
109
|
+
/** Snapshot — detaching later does not change an already-returned list,
|
|
110
|
+
* and mutating it does not change the event's registrations. */
|
|
107
111
|
getObservers() {
|
|
108
|
-
return this.observers;
|
|
112
|
+
return [...this.observers];
|
|
109
113
|
}
|
|
110
114
|
getMetadata() {
|
|
111
115
|
return Object.fromEntries(this.metadata);
|
|
@@ -131,8 +135,8 @@ var INTERNAL_CONSTRUCTION_KEY = /* @__PURE__ */ Symbol(
|
|
|
131
135
|
|
|
132
136
|
// src/error/FinitaError.ts
|
|
133
137
|
var FinitaError = class _FinitaError extends Error {
|
|
134
|
-
constructor(message) {
|
|
135
|
-
super(message);
|
|
138
|
+
constructor(message, options) {
|
|
139
|
+
super(message, options);
|
|
136
140
|
if (new.target === _FinitaError) {
|
|
137
141
|
throw new TypeError(
|
|
138
142
|
"FinitaError is abstract and cannot be instantiated directly"
|
|
@@ -262,11 +266,13 @@ var State = class {
|
|
|
262
266
|
getName() {
|
|
263
267
|
return this.name;
|
|
264
268
|
}
|
|
269
|
+
/** Snapshot — the graph is shared by every machine built from the
|
|
270
|
+
* process, so callers must never receive the collection itself. */
|
|
265
271
|
getTransitions() {
|
|
266
272
|
if (this._transitions === null) {
|
|
267
273
|
return [];
|
|
268
274
|
}
|
|
269
|
-
return this._transitions;
|
|
275
|
+
return Array.from(this._transitions);
|
|
270
276
|
}
|
|
271
277
|
getEventNames() {
|
|
272
278
|
return Array.from(this.events.keys());
|
|
@@ -545,9 +551,11 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
545
551
|
}
|
|
546
552
|
}
|
|
547
553
|
/** Transition identity: (fromState, eventName, toState). Used by both the
|
|
548
|
-
* conflict check and the build-time dedup — keep them in lockstep.
|
|
554
|
+
* conflict check and the build-time dedup — keep them in lockstep.
|
|
555
|
+
* Encoded as a JSON tuple, not a delimiter join: names may contain any
|
|
556
|
+
* character, so no delimiter can keep distinct tuples distinct. */
|
|
549
557
|
static transitionKey(t) {
|
|
550
|
-
return
|
|
558
|
+
return JSON.stringify([t.fromState, t.eventName, t.toState]);
|
|
551
559
|
}
|
|
552
560
|
validateNoConflictingDuplicates() {
|
|
553
561
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -666,12 +674,30 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
666
674
|
var AmbiguousTransitionError = class extends FinitaError {
|
|
667
675
|
code = "ambiguousTransition";
|
|
668
676
|
activeCount;
|
|
669
|
-
|
|
670
|
-
|
|
677
|
+
/** The competing transitions — what you need to resolve the ambiguity. */
|
|
678
|
+
candidates;
|
|
679
|
+
constructor(activeCount, candidates = []) {
|
|
680
|
+
const list = Array.from(candidates, (c) => Object.freeze({ ...c }));
|
|
681
|
+
const detail = list.length > 0 ? ` Candidates: ${list.map(describeCandidate).join("; ")}.` : "";
|
|
682
|
+
super(
|
|
683
|
+
`More than one transition is active! (active count: ${activeCount})${detail}`
|
|
684
|
+
);
|
|
671
685
|
this.name = "AmbiguousTransitionError";
|
|
672
686
|
this.activeCount = activeCount;
|
|
687
|
+
this.candidates = Object.freeze(list);
|
|
673
688
|
}
|
|
674
689
|
};
|
|
690
|
+
function describeCandidate(candidate) {
|
|
691
|
+
const parts = [`-> "${candidate.targetStateName}"`];
|
|
692
|
+
parts.push(
|
|
693
|
+
candidate.eventName === null ? "on <automatic>" : `on event "${candidate.eventName}"`
|
|
694
|
+
);
|
|
695
|
+
if (candidate.conditionName !== null) {
|
|
696
|
+
parts.push(`if ${candidate.conditionName}`);
|
|
697
|
+
}
|
|
698
|
+
parts.push(`weight ${candidate.weight}`);
|
|
699
|
+
return parts.join(" ");
|
|
700
|
+
}
|
|
675
701
|
|
|
676
702
|
// src/selector/OneOrNoneActiveTransition.ts
|
|
677
703
|
var OneOrNoneActiveTransition = class {
|
|
@@ -683,7 +709,15 @@ var OneOrNoneActiveTransition = class {
|
|
|
683
709
|
case 1:
|
|
684
710
|
return arr[0];
|
|
685
711
|
default:
|
|
686
|
-
throw new AmbiguousTransitionError(
|
|
712
|
+
throw new AmbiguousTransitionError(
|
|
713
|
+
arr.length,
|
|
714
|
+
arr.map((transition) => ({
|
|
715
|
+
targetStateName: transition.getTargetState().getName(),
|
|
716
|
+
eventName: transition.getEventName(),
|
|
717
|
+
conditionName: transition.getConditionName(),
|
|
718
|
+
weight: transition.getWeight()
|
|
719
|
+
}))
|
|
720
|
+
);
|
|
687
721
|
}
|
|
688
722
|
}
|
|
689
723
|
};
|
|
@@ -708,19 +742,34 @@ var NullMutex = class {
|
|
|
708
742
|
};
|
|
709
743
|
|
|
710
744
|
// src/internal/OperationQueue.ts
|
|
711
|
-
var OperationQueue = class {
|
|
745
|
+
var OperationQueue = class _OperationQueue {
|
|
746
|
+
static COMPACT_THRESHOLD = 1024;
|
|
712
747
|
items = [];
|
|
748
|
+
head = 0;
|
|
713
749
|
enqueue(op) {
|
|
714
750
|
this.items.push(op);
|
|
715
751
|
}
|
|
716
752
|
dequeue() {
|
|
717
|
-
|
|
753
|
+
if (this.head >= this.items.length) {
|
|
754
|
+
return void 0;
|
|
755
|
+
}
|
|
756
|
+
const op = this.items[this.head];
|
|
757
|
+
this.items[this.head] = void 0;
|
|
758
|
+
this.head++;
|
|
759
|
+
if (this.head === this.items.length) {
|
|
760
|
+
this.items = [];
|
|
761
|
+
this.head = 0;
|
|
762
|
+
} else if (this.head >= _OperationQueue.COMPACT_THRESHOLD && this.head * 2 >= this.items.length) {
|
|
763
|
+
this.items = this.items.slice(this.head);
|
|
764
|
+
this.head = 0;
|
|
765
|
+
}
|
|
766
|
+
return op;
|
|
718
767
|
}
|
|
719
768
|
isEmpty() {
|
|
720
|
-
return this.items.length
|
|
769
|
+
return this.head === this.items.length;
|
|
721
770
|
}
|
|
722
771
|
size() {
|
|
723
|
-
return this.items.length;
|
|
772
|
+
return this.items.length - this.head;
|
|
724
773
|
}
|
|
725
774
|
};
|
|
726
775
|
|
|
@@ -760,6 +809,27 @@ var LockCanNotBeAcquiredError = class extends FinitaError {
|
|
|
760
809
|
}
|
|
761
810
|
};
|
|
762
811
|
|
|
812
|
+
// src/error/LockCanNotBeReleasedError.ts
|
|
813
|
+
var LockCanNotBeReleasedError = class extends FinitaError {
|
|
814
|
+
code = "lockCanNotBeReleased";
|
|
815
|
+
constructor(message = "Lock can not be released! releaseLock() returned false; the lock may still be held.") {
|
|
816
|
+
super(message);
|
|
817
|
+
this.name = "LockCanNotBeReleasedError";
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
// src/error/LockOwnershipUncertainError.ts
|
|
822
|
+
var LockOwnershipUncertainError = class extends FinitaError {
|
|
823
|
+
code = "lockOwnershipUncertain";
|
|
824
|
+
constructor(cause) {
|
|
825
|
+
super(
|
|
826
|
+
"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.",
|
|
827
|
+
{ cause }
|
|
828
|
+
);
|
|
829
|
+
this.name = "LockOwnershipUncertainError";
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
|
|
763
833
|
// src/error/AutomaticTransitionCycleError.ts
|
|
764
834
|
var AutomaticTransitionCycleError = class extends FinitaError {
|
|
765
835
|
code = "automaticTransitionCycle";
|
|
@@ -797,6 +867,18 @@ var QueueLimitExceededError = class extends FinitaError {
|
|
|
797
867
|
}
|
|
798
868
|
};
|
|
799
869
|
|
|
870
|
+
// src/util/index.ts
|
|
871
|
+
function isNamed(obj) {
|
|
872
|
+
return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
|
|
873
|
+
}
|
|
874
|
+
function nameOrString(obj) {
|
|
875
|
+
if (isNamed(obj)) return obj.getName();
|
|
876
|
+
return String(obj);
|
|
877
|
+
}
|
|
878
|
+
function isPromiseLike(value) {
|
|
879
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
880
|
+
}
|
|
881
|
+
|
|
800
882
|
// src/Statemachine.ts
|
|
801
883
|
var Statemachine = class {
|
|
802
884
|
subject;
|
|
@@ -812,6 +894,8 @@ var Statemachine = class {
|
|
|
812
894
|
running = false;
|
|
813
895
|
idleWaiters = [];
|
|
814
896
|
inSyncCallback = false;
|
|
897
|
+
/** Set when releasing a held lock fails; see LockOwnershipUncertainError. */
|
|
898
|
+
ownershipUncertainty = null;
|
|
815
899
|
beforeObservers = [];
|
|
816
900
|
afterObservers = [];
|
|
817
901
|
onChainedOperationError;
|
|
@@ -862,8 +946,10 @@ var Statemachine = class {
|
|
|
862
946
|
const idx = this.beforeObservers.indexOf(observer);
|
|
863
947
|
if (idx >= 0) this.beforeObservers.splice(idx, 1);
|
|
864
948
|
}
|
|
949
|
+
/** Snapshot — detaching later does not change an already-returned list,
|
|
950
|
+
* and mutating it does not change the machine's registrations. */
|
|
865
951
|
getBeforeObservers() {
|
|
866
|
-
return this.beforeObservers;
|
|
952
|
+
return [...this.beforeObservers];
|
|
867
953
|
}
|
|
868
954
|
attachAfter(observer) {
|
|
869
955
|
if (this.afterObservers.includes(observer)) return;
|
|
@@ -873,15 +959,26 @@ var Statemachine = class {
|
|
|
873
959
|
const idx = this.afterObservers.indexOf(observer);
|
|
874
960
|
if (idx >= 0) this.afterObservers.splice(idx, 1);
|
|
875
961
|
}
|
|
962
|
+
/** Snapshot — see getBeforeObservers. */
|
|
876
963
|
getAfterObservers() {
|
|
877
|
-
return this.afterObservers;
|
|
964
|
+
return [...this.afterObservers];
|
|
878
965
|
}
|
|
879
966
|
// --- public locking ---
|
|
880
967
|
async acquireLock() {
|
|
881
968
|
return this.mutex.acquireLock();
|
|
882
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* Releases the mutex. A failed release — whether the mutex throws or
|
|
972
|
+
* returns false — is reported to the onReleaseError hook; it is not thrown,
|
|
973
|
+
* so manual lock management keeps its existing control flow. Inspect
|
|
974
|
+
* isLockAcquired() (or the hook) to learn whether the lock was actually
|
|
975
|
+
* freed.
|
|
976
|
+
*
|
|
977
|
+
* A failed release of a held lock makes every later operation reject with
|
|
978
|
+
* LockOwnershipUncertainError; a successful call here is how to recover.
|
|
979
|
+
*/
|
|
883
980
|
async releaseLock() {
|
|
884
|
-
await this.
|
|
981
|
+
await this.releaseMutex();
|
|
885
982
|
}
|
|
886
983
|
isLockAcquired() {
|
|
887
984
|
return this.mutex.isAcquired();
|
|
@@ -911,8 +1008,14 @@ var Statemachine = class {
|
|
|
911
1008
|
* EnqueueContext.enqueue(), has completed. Resolves immediately if the
|
|
912
1009
|
* machine is already idle. Note this is a quiescence point, not a
|
|
913
1010
|
* receipt: work scheduled later (e.g. from a timer) starts a new drain.
|
|
1011
|
+
*
|
|
1012
|
+
* Like triggerEvent/checkTransitions, this may not be called from inside an
|
|
1013
|
+
* observer or condition of the same machine: the machine cannot reach idle
|
|
1014
|
+
* while the runner is blocked on that very callback, so awaiting it there
|
|
1015
|
+
* always deadlocks.
|
|
914
1016
|
*/
|
|
915
1017
|
whenIdle() {
|
|
1018
|
+
this.assertNotReentrant("whenIdle()");
|
|
916
1019
|
if (!this.running && this.queue.isEmpty()) {
|
|
917
1020
|
return Promise.resolve();
|
|
918
1021
|
}
|
|
@@ -972,6 +1075,10 @@ var Statemachine = class {
|
|
|
972
1075
|
}
|
|
973
1076
|
}
|
|
974
1077
|
async runOperation(op) {
|
|
1078
|
+
if (this.ownershipUncertainty) {
|
|
1079
|
+
op.reject(new LockOwnershipUncertainError(this.ownershipUncertainty.err));
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
975
1082
|
if (op.ifStateName !== void 0 && this.currentState.getName() !== op.ifStateName) {
|
|
976
1083
|
op.resolve();
|
|
977
1084
|
return;
|
|
@@ -991,15 +1098,8 @@ var Statemachine = class {
|
|
|
991
1098
|
failure = { err };
|
|
992
1099
|
} finally {
|
|
993
1100
|
if (acquiredHere && this.autoreleaseLock) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
} catch (err) {
|
|
997
|
-
try {
|
|
998
|
-
this.onReleaseError?.(err);
|
|
999
|
-
} catch {
|
|
1000
|
-
}
|
|
1001
|
-
if (!failure) failure = { err };
|
|
1002
|
-
}
|
|
1101
|
+
const releaseFailure = await this.releaseMutex();
|
|
1102
|
+
if (releaseFailure && !failure) failure = releaseFailure;
|
|
1003
1103
|
}
|
|
1004
1104
|
}
|
|
1005
1105
|
if (failure) {
|
|
@@ -1008,6 +1108,60 @@ var Statemachine = class {
|
|
|
1008
1108
|
op.resolve();
|
|
1009
1109
|
}
|
|
1010
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Releases the mutex, normalizing its two failure modes into one result: a
|
|
1113
|
+
* thrown error, and a false return — the failure signal MutexInterface /
|
|
1114
|
+
* LockAdapterInterface define (a PostgreSQL advisory unlock that returns
|
|
1115
|
+
* false, a Redis DEL that removed nothing). A false return means the lock
|
|
1116
|
+
* may still be held, so it must never be mistaken for a successful release.
|
|
1117
|
+
*
|
|
1118
|
+
* Every failure is surfaced through the diagnostic hook — when the
|
|
1119
|
+
* operation also failed, the rejection carries the operation error and this
|
|
1120
|
+
* hook is the only place the release error appears.
|
|
1121
|
+
*
|
|
1122
|
+
* A failure while the mutex claimed to hold the lock leaves ownership
|
|
1123
|
+
* uncertain and blocks later operations; a success clears that state. A
|
|
1124
|
+
* failed release of a lock the mutex did not claim (a defensive manual
|
|
1125
|
+
* release) is still reported but changes nothing.
|
|
1126
|
+
*
|
|
1127
|
+
* @returns null on success, or the failure wrapped for the caller to raise.
|
|
1128
|
+
*/
|
|
1129
|
+
async releaseMutex() {
|
|
1130
|
+
const held = this.mutex.isAcquired();
|
|
1131
|
+
let failure = null;
|
|
1132
|
+
try {
|
|
1133
|
+
if (!await this.mutex.releaseLock()) {
|
|
1134
|
+
failure = { err: new LockCanNotBeReleasedError() };
|
|
1135
|
+
}
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
failure = { err };
|
|
1138
|
+
}
|
|
1139
|
+
if (failure) {
|
|
1140
|
+
if (held) this.ownershipUncertainty = failure;
|
|
1141
|
+
const err = failure.err;
|
|
1142
|
+
this.callDiagnosticHook(() => this.onReleaseError?.(err));
|
|
1143
|
+
} else {
|
|
1144
|
+
this.ownershipUncertainty = null;
|
|
1145
|
+
}
|
|
1146
|
+
return failure;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Runs a user diagnostic hook in isolation. Neither a synchronous throw nor
|
|
1150
|
+
* a rejection of a returned promise may reach the drain loop or the host:
|
|
1151
|
+
* an unavailable telemetry backend must not fail an operation or, via an
|
|
1152
|
+
* unhandled rejection, terminate the process. A returned promise is
|
|
1153
|
+
* deliberately not awaited — a slow reporter must not stall the runner.
|
|
1154
|
+
*/
|
|
1155
|
+
callDiagnosticHook(hook) {
|
|
1156
|
+
try {
|
|
1157
|
+
const result = hook();
|
|
1158
|
+
if (isPromiseLike(result)) {
|
|
1159
|
+
result.then(void 0, () => {
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
} catch {
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1011
1165
|
resolveEvent(name) {
|
|
1012
1166
|
if (!this.currentState.hasEvent(name)) {
|
|
1013
1167
|
throw new WrongEventForStateError(this.currentState.getName(), name);
|
|
@@ -1079,12 +1233,11 @@ var Statemachine = class {
|
|
|
1079
1233
|
() => {
|
|
1080
1234
|
},
|
|
1081
1235
|
(err) => {
|
|
1082
|
-
|
|
1083
|
-
this.onChainedOperationError?.(err, {
|
|
1236
|
+
this.callDiagnosticHook(
|
|
1237
|
+
() => this.onChainedOperationError?.(err, {
|
|
1084
1238
|
eventName: chainedEventName
|
|
1085
|
-
})
|
|
1086
|
-
|
|
1087
|
-
}
|
|
1239
|
+
})
|
|
1240
|
+
);
|
|
1088
1241
|
},
|
|
1089
1242
|
ifStateName
|
|
1090
1243
|
);
|
|
@@ -1220,6 +1373,30 @@ var CompositeCondition = class {
|
|
|
1220
1373
|
const names = this.conditions.map((c) => c.getName());
|
|
1221
1374
|
return `(${names.join(` ${this.joinWord} `)})`;
|
|
1222
1375
|
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Evaluates children in order, stopping at the first whose result equals
|
|
1378
|
+
* `shortCircuitOn`. A child that returns a plain boolean is consumed
|
|
1379
|
+
* synchronously; only a returned promise is awaited. Awaiting plain values
|
|
1380
|
+
* would yield between children and end the machine's synchronous
|
|
1381
|
+
* re-entrancy guard, so a re-entrant later child would deadlock instead of
|
|
1382
|
+
* throwing ReentrancyError. For the same reason the composite itself
|
|
1383
|
+
* returns a plain boolean when every child it evaluated did.
|
|
1384
|
+
*/
|
|
1385
|
+
evaluate(subject, context, shortCircuitOn) {
|
|
1386
|
+
const from = (start) => {
|
|
1387
|
+
for (let i = start; i < this.conditions.length; i++) {
|
|
1388
|
+
const result = this.conditions[i].checkCondition(subject, context);
|
|
1389
|
+
if (isPromiseLike(result)) {
|
|
1390
|
+
return Promise.resolve(result).then(
|
|
1391
|
+
(value) => Boolean(value) === shortCircuitOn ? shortCircuitOn : from(i + 1)
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
if (Boolean(result) === shortCircuitOn) return shortCircuitOn;
|
|
1395
|
+
}
|
|
1396
|
+
return !shortCircuitOn;
|
|
1397
|
+
};
|
|
1398
|
+
return from(0);
|
|
1399
|
+
}
|
|
1223
1400
|
};
|
|
1224
1401
|
|
|
1225
1402
|
// src/condition/AndComposite.ts
|
|
@@ -1230,13 +1407,8 @@ var AndComposite = class extends CompositeCondition {
|
|
|
1230
1407
|
addAnd(condition) {
|
|
1231
1408
|
return this.addCondition(condition);
|
|
1232
1409
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
if (!await condition.checkCondition(subject, context)) {
|
|
1236
|
-
return false;
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
return true;
|
|
1410
|
+
checkCondition(subject, context) {
|
|
1411
|
+
return this.evaluate(subject, context, false);
|
|
1240
1412
|
}
|
|
1241
1413
|
};
|
|
1242
1414
|
|
|
@@ -1248,13 +1420,8 @@ var OrComposite = class extends CompositeCondition {
|
|
|
1248
1420
|
addOr(condition) {
|
|
1249
1421
|
return this.addCondition(condition);
|
|
1250
1422
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
if (await condition.checkCondition(subject, context)) {
|
|
1254
|
-
return true;
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
return false;
|
|
1423
|
+
checkCondition(subject, context) {
|
|
1424
|
+
return this.evaluate(subject, context, true);
|
|
1258
1425
|
}
|
|
1259
1426
|
};
|
|
1260
1427
|
|
|
@@ -1267,8 +1434,13 @@ var Not = class {
|
|
|
1267
1434
|
getName() {
|
|
1268
1435
|
return `not ( ${this.condition.getName()} )`;
|
|
1269
1436
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1437
|
+
/** Stays synchronous for a synchronous child — see CompositeCondition. */
|
|
1438
|
+
checkCondition(subject, context) {
|
|
1439
|
+
const result = this.condition.checkCondition(subject, context);
|
|
1440
|
+
if (isPromiseLike(result)) {
|
|
1441
|
+
return Promise.resolve(result).then((value) => !value);
|
|
1442
|
+
}
|
|
1443
|
+
return !result;
|
|
1272
1444
|
}
|
|
1273
1445
|
};
|
|
1274
1446
|
|
|
@@ -1323,15 +1495,6 @@ var OnEnterObserver = class _OnEnterObserver {
|
|
|
1323
1495
|
}
|
|
1324
1496
|
};
|
|
1325
1497
|
|
|
1326
|
-
// src/util/index.ts
|
|
1327
|
-
function isNamed(obj) {
|
|
1328
|
-
return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
|
|
1329
|
-
}
|
|
1330
|
-
function nameOrString(obj) {
|
|
1331
|
-
if (isNamed(obj)) return obj.getName();
|
|
1332
|
-
return String(obj);
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
1498
|
// src/observer/TransitionLogger.ts
|
|
1336
1499
|
var TransitionLogger = class {
|
|
1337
1500
|
logger;
|
|
@@ -1481,15 +1644,39 @@ var LockAdapterMutex = class {
|
|
|
1481
1644
|
lockAdapter;
|
|
1482
1645
|
resourceName;
|
|
1483
1646
|
acquired = false;
|
|
1647
|
+
pendingAcquire = null;
|
|
1484
1648
|
constructor(lockAdapter, resourceName) {
|
|
1485
1649
|
this.lockAdapter = lockAdapter;
|
|
1486
1650
|
this.resourceName = resourceName;
|
|
1487
1651
|
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Overlapping calls share one underlying acquire: the `acquired` flag is
|
|
1654
|
+
* only set after the adapter resolves, so without this both callers would
|
|
1655
|
+
* pass the check and acquire twice on a non-idempotent adapter (database
|
|
1656
|
+
* advisory locks, redis SET NX). The pending promise is cleared once it
|
|
1657
|
+
* settles, so a failed acquire can still be retried.
|
|
1658
|
+
*
|
|
1659
|
+
* The clearing is attached to the attempt only after it is stored: an
|
|
1660
|
+
* adapter that throws synchronously settles the attempt before the
|
|
1661
|
+
* assignment would otherwise run, and clearing inside the attempt itself
|
|
1662
|
+
* would then leave the rejected promise cached forever.
|
|
1663
|
+
*/
|
|
1488
1664
|
async acquireLock() {
|
|
1489
|
-
if (
|
|
1490
|
-
|
|
1665
|
+
if (this.acquired) {
|
|
1666
|
+
return true;
|
|
1491
1667
|
}
|
|
1492
|
-
|
|
1668
|
+
if (!this.pendingAcquire) {
|
|
1669
|
+
const attempt = (async () => {
|
|
1670
|
+
this.acquired = await this.lockAdapter.acquireLock(this.resourceName);
|
|
1671
|
+
return this.acquired;
|
|
1672
|
+
})();
|
|
1673
|
+
this.pendingAcquire = attempt;
|
|
1674
|
+
const clear = () => {
|
|
1675
|
+
if (this.pendingAcquire === attempt) this.pendingAcquire = null;
|
|
1676
|
+
};
|
|
1677
|
+
attempt.then(clear, clear);
|
|
1678
|
+
}
|
|
1679
|
+
return this.pendingAcquire;
|
|
1493
1680
|
}
|
|
1494
1681
|
async releaseLock() {
|
|
1495
1682
|
if (this.acquired) {
|
|
@@ -1533,9 +1720,18 @@ var Factory = class {
|
|
|
1533
1720
|
afterObservers = /* @__PURE__ */ new Set();
|
|
1534
1721
|
transitionSelector = null;
|
|
1535
1722
|
mutexFactory = null;
|
|
1536
|
-
|
|
1723
|
+
options;
|
|
1724
|
+
/**
|
|
1725
|
+
* @param options Engine options applied to every machine this factory
|
|
1726
|
+
* creates — back-pressure (maxQueueLength), the automatic-hop bound, lock
|
|
1727
|
+
* autorelease, and the onChainedOperationError / onReleaseError diagnostic
|
|
1728
|
+
* sinks. Without them, factory-created machines would silently run on
|
|
1729
|
+
* defaults, which is precisely where those sinks matter most.
|
|
1730
|
+
*/
|
|
1731
|
+
constructor(processDetector, stateNameDetector, options = {}) {
|
|
1537
1732
|
this.processDetector = processDetector;
|
|
1538
1733
|
this.stateNameDetector = stateNameDetector ?? null;
|
|
1734
|
+
this.options = { ...options };
|
|
1539
1735
|
}
|
|
1540
1736
|
setMutexFactory(factory) {
|
|
1541
1737
|
this.mutexFactory = factory;
|
|
@@ -1560,6 +1756,7 @@ var Factory = class {
|
|
|
1560
1756
|
const stateName = this.stateNameDetector ? this.stateNameDetector.detectCurrentStateName(subject) : void 0;
|
|
1561
1757
|
const mutex = this.mutexFactory ? await this.mutexFactory.createMutex(subject) : void 0;
|
|
1562
1758
|
const sm = new Statemachine(subject, process, {
|
|
1759
|
+
...this.options,
|
|
1563
1760
|
initialStateName: stateName ?? void 0,
|
|
1564
1761
|
transitionSelector: this.transitionSelector ?? void 0,
|
|
1565
1762
|
mutex: mutex ?? void 0
|
|
@@ -1790,6 +1987,8 @@ var GraphBuilder = class {
|
|
|
1790
1987
|
InvalidSubjectError,
|
|
1791
1988
|
LockAdapterMutex,
|
|
1792
1989
|
LockCanNotBeAcquiredError,
|
|
1990
|
+
LockCanNotBeReleasedError,
|
|
1991
|
+
LockOwnershipUncertainError,
|
|
1793
1992
|
MutexFactory,
|
|
1794
1993
|
Not,
|
|
1795
1994
|
NullMutex,
|