@camcima/finita 4.2.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 ADDED
@@ -0,0 +1,141 @@
1
+ # Changelog
2
+
3
+ ## [4.3.0](https://github.com/camcima/finita/compare/v4.2.0...v4.3.0) (2026-09-27)
4
+
5
+ ### Features
6
+
7
+ * address principal architecture review findings ([#74](https://github.com/camcima/finita/issues/74)) ([c7a8ca4](https://github.com/camcima/finita/commit/c7a8ca48dd8a1ec1010821591c612e2e30d71718)), closes [#63](https://github.com/camcima/finita/issues/63), references [#75](https://github.com/camcima/finita/issues/75)
8
+
9
+ ### Documentation
10
+
11
+ * document persisted-state ownership and the real operation flow ([#76](https://github.com/camcima/finita/issues/76)) ([d9ab220](https://github.com/camcima/finita/commit/d9ab220ae90cf6f8a5a32904c88f858e786c4217))
12
+
13
+ ## [4.2.0](https://github.com/camcima/finita/compare/v4.1.0...v4.2.0) (2026-08-19)
14
+
15
+ Remediation of the 2026-08 architecture review ([#52](https://github.com/camcima/finita/pull/52)). Full notes: [v4.2.0 release](https://github.com/camcima/finita/releases/tag/v4.2.0).
16
+
17
+ ### Features
18
+
19
+ * `Factory` accepts a `FactoryStatemachineOptions` template, forwarding engine options to every machine it creates ([f67bb12](https://github.com/camcima/finita/commit/f67bb127134e2f710cc3d9640705c8b2f3108a0c))
20
+ * add `LockCanNotBeReleasedError` for a release that fails by returning `false` ([f67bb12](https://github.com/camcima/finita/commit/f67bb127134e2f710cc3d9640705c8b2f3108a0c))
21
+ * add `AmbiguousTransitionError.candidates` carrying the competing transitions ([f67bb12](https://github.com/camcima/finita/commit/f67bb127134e2f710cc3d9640705c8b2f3108a0c))
22
+
23
+ ### Bug Fixes
24
+
25
+ * surface a failed lock release instead of resolving as if the lock were freed ([f67bb12](https://github.com/camcima/finita/commit/f67bb127134e2f710cc3d9640705c8b2f3108a0c))
26
+ * guard `whenIdle()` against re-entrant calls that deadlocked the machine ([f67bb12](https://github.com/camcima/finita/commit/f67bb127134e2f710cc3d9640705c8b2f3108a0c))
27
+ * share one in-flight acquire in `LockAdapterMutex` ([f67bb12](https://github.com/camcima/finita/commit/f67bb127134e2f710cc3d9640705c8b2f3108a0c))
28
+ * return observer snapshots from the accessor methods ([f67bb12](https://github.com/camcima/finita/commit/f67bb127134e2f710cc3d9640705c8b2f3108a0c))
29
+
30
+ ## [4.1.0](https://github.com/camcima/finita/compare/v4.0.0...v4.1.0) (2026-07-10)
31
+
32
+ ### Features
33
+
34
+ * add maxQueueLength back-pressure option ([83a425b](https://github.com/camcima/finita/commit/83a425b3a18f422e1e6dabdf952e9a5efe153d21))
35
+ * add onChainedOperationError sink and document completion boundary ([0d94ddf](https://github.com/camcima/finita/commit/0d94ddf161b1ca195f473574c81116c559508e91))
36
+ * add onReleaseError diagnostic hook for lock release failures ([f6058bc](https://github.com/camcima/finita/commit/f6058bcc3bf580022d50ee49cbf08bdfbdc504a9))
37
+ * add Statemachine.whenIdle() to await full queue drain ([14d601b](https://github.com/camcima/finita/commit/14d601b46984fd97e307e310478c93646ce2af48))
38
+
39
+ ### Bug Fixes
40
+
41
+ * freeze State and Transition to match documented immutability ([d466c7d](https://github.com/camcima/finita/commit/d466c7d0a1b27c11e9708e884549cfd6a2a8f489))
42
+ * keep ProcessBuilder reusable after a failed build ([457da0c](https://github.com/camcima/finita/commit/457da0c7f4b10c2056dd346684c595d290363965))
43
+ * make Statemachine observer attach idempotent ([aeba9db](https://github.com/camcima/finita/commit/aeba9db1f2f5415242f2c3493f229a06cd9e96fd))
44
+ * restrict graph rankdir/direction options to valid values ([6607d81](https://github.com/camcima/finita/commit/6607d819954d5e2a1b594ecef5a2538d6911b9c2))
45
+ * validate transition weights and WeightTransition epsilon ([98b07b2](https://github.com/camcima/finita/commit/98b07b20f3c39a6e05d61a35ee170c09c6ed1fe2))
46
+
47
+ ## [4.0.0](https://github.com/camcima/finita/compare/v3.0.1...v4.0.0) (2026-06-23)
48
+
49
+ ### Features
50
+
51
+ * **observer:** expose subject on TransitionFrame; share one frame per transition ([44ae06c](https://github.com/camcima/finita/commit/44ae06c38d9f924fce507064d8938939c40134bc))
52
+
53
+ ### Bug Fixes
54
+
55
+ * **builder:** detect conflicting weights on duplicate transitions, unify identity key ([fbd571d](https://github.com/camcima/finita/commit/fbd571d7dad7e61fabf03819fcc2e2bd6888baec))
56
+ * **builder:** validate state names; unify name rule across state/event/condition ([ac7b38d](https://github.com/camcima/finita/commit/ac7b38d08004444bd467c51a2dbbe17c55d266d8))
57
+ * **condition:** timeout throws InvalidSubjectError; allocation-free time check ([4ab8a8e](https://github.com/camcima/finita/commit/4ab8a8ecfc8d84d49e1e2df9dbbd49a397a4b344))
58
+ * **event:** pass invoke args to observers; drop racy invokeArgs field; inline Dispatcher ([8bb6581](https://github.com/camcima/finita/commit/8bb6581410c617b94ed7b2ad83725b4d51029c2d))
59
+ * **graph:** escape backslashes in DOT/mermaid output; guard event lookup in labels ([2f3fd72](https://github.com/camcima/finita/commit/2f3fd7209244b1aa151b4f44349ebf54c46dbe15))
60
+ * **observer:** onEnter fires only for rest states via enqueue ifStateName guard ([726024a](https://github.com/camcima/finita/commit/726024a79a2292757bee3e832fa1d386c3323f9b))
61
+ * **observer:** statefulStatusChanger writes to frame.subject; correct factory docs ([d72ce2b](https://github.com/camcima/finita/commit/d72ce2bc7993868b5f2898d837326c8e449b99fd))
62
+ * **selector:** make WeightTransition epsilon ties order-independent ([44f30d2](https://github.com/camcima/finita/commit/44f30d2fa01841d2137b3ce3998a93ef4f8ecece))
63
+ * **statemachine:** bound automatic loops with maxAutomaticHops instead of first-revisit detection ([16cc1b5](https://github.com/camcima/finita/commit/16cc1b5fbe08c17d78bd3a472c00cc478b94eb29))
64
+ * **statemachine:** guard every event observer and transition condition against re-entrancy ([cb80489](https://github.com/camcima/finita/commit/cb80489702d00e57a9adcb381bb4159bd95e06de)), closes [#29](https://github.com/camcima/finita/issues/29)
65
+ * **statemachine:** kick runner from EnqueueContext.enqueue; single enqueue path ([7e84451](https://github.com/camcima/finita/commit/7e8445106ab37cfdc476ccbf6dec29f714d91e94))
66
+ * **statemachine:** reject (not throw) on re-entrancy; broaden re-entrancy tests ([5ad2514](https://github.com/camcima/finita/commit/5ad251432fe33625f022b4990182619499925d08))
67
+ * **statemachine:** reject empty initialStateName instead of silently restarting ([36272aa](https://github.com/camcima/finita/commit/36272aa9375df186e1aca4fa3f2d741349e5d694))
68
+ * **statemachine:** settle caller promise only after mutex release completes ([7501123](https://github.com/camcima/finita/commit/7501123521ee53158380589c1fb2c904e4cc4e4c))
69
+ * **statemachine:** snapshot observer lists before notifying ([eed53a7](https://github.com/camcima/finita/commit/eed53a75cd6a24b3fca96e8ebe988ce90fd61aa6))
70
+ * **statemachine:** surface lock-release failures; guard selector; restore zero-arg callback ([129280b](https://github.com/camcima/finita/commit/129280bbfce9ae87dedbb539a663c3e96e4941c1))
71
+ * **statemachine:** throw ReentrancyError on re-entrant trigger instead of deadlocking ([3655026](https://github.com/camcima/finita/commit/3655026be68c485fcd3d5a71b19721451d23403f))
72
+ * **statemachine:** validate maxAutomaticHops is a positive integer ([e9a4001](https://github.com/camcima/finita/commit/e9a400180492c63bc36382b4c7ed103444a71bbc))
73
+
74
+ ## [3.0.1](https://github.com/camcima/finita/compare/v3.0.0...v3.0.1) (2026-05-30)
75
+
76
+ ### Features
77
+
78
+ * **builder:** introduce ProcessBuilder and freeze graph ([d9a9209](https://github.com/camcima/finita/commit/d9a920937371ecf2afb0982f065f9e6c074cda06)), closes [#3](https://github.com/camcima/finita/issues/3) [#5](https://github.com/camcima/finita/issues/5) [#6](https://github.com/camcima/finita/issues/6)
79
+ * **builder:** reject empty/whitespace condition names ([0de6159](https://github.com/camcima/finita/commit/0de615935b6b09efc7e0bc2ddc855fe86817803c))
80
+ * **builder:** reject whitespace-padded event names; rename code to invalidEventName ([8dbd3a7](https://github.com/camcima/finita/commit/8dbd3a7e0f42e5c5b5aaa496ce8f254438b65439))
81
+ * **error:** add AmbiguousTransitionError and retrofit OneOrNoneActiveTransition ([e7028db](https://github.com/camcima/finita/commit/e7028dbd97f9bed58022f0beebad2feb8da04efa))
82
+ * **error:** add AutomaticTransitionCycleError and retrofit Statemachine ([821156b](https://github.com/camcima/finita/commit/821156b9b6b76b5853c753488a82494bdee9bb17))
83
+ * **error:** add DuplicateTransitionError ([905fc72](https://github.com/camcima/finita/commit/905fc72f5ffc304a35193e5287c28e54b5792caa))
84
+ * **error:** add GraphValidationError with typed code ([5027b73](https://github.com/camcima/finita/commit/5027b73194f812eea864b4743f0eb1b1211a1d9f))
85
+ * **error:** add InvalidSubjectError and retrofit StatefulStateNameDetector ([2dd9a89](https://github.com/camcima/finita/commit/2dd9a89d9b9729e1f44c04757ab6cccb25df8cad))
86
+ * **error:** add ProcessFinalizedError ([e8b82a2](https://github.com/camcima/finita/commit/e8b82a2f788ba6cd25c614de2cae9fb62179dc37))
87
+ * **error:** add ProcessNotFoundError and retrofit AbstractNamedProcessDetector ([10e5fd0](https://github.com/camcima/finita/commit/10e5fd017c5591d606a3b1b7142c0fdf63a1a0ff))
88
+ * **error:** add StateEventNotFoundError and retrofit State.getEvent ([6db3b6d](https://github.com/camcima/finita/commit/6db3b6d3a065eb7825e3230cf55f912c2075da4d))
89
+ * **error:** add StateNotFoundError and retrofit StateCollection.getState ([1a2ac20](https://github.com/camcima/finita/commit/1a2ac20a7186426963b646c510710184e3d03f16))
90
+ * **error:** export FinitaError and new error classes from package entry ([84a6c80](https://github.com/camcima/finita/commit/84a6c807181ef621760a5739bed4c5151aaac0cc))
91
+ * **error:** introduce FinitaError base; retrofit existing classes with code discriminant ([527be3d](https://github.com/camcima/finita/commit/527be3d15e2d7778a2950a8cc8e6b54ba8e2ab9b))
92
+ * **interfaces:** add BeforeTransitionObserver and AfterTransitionObserver ([66a920c](https://github.com/camcima/finita/commit/66a920c83fbab10c888f10d4aa49b116cd659ac6))
93
+ * **interfaces:** add StatemachineOptions type ([8c415f9](https://github.com/camcima/finita/commit/8c415f915c141486967e53bdb791f9cd4a703eca))
94
+ * **interfaces:** add TransitionFrame and ProposedTransitionFrame ([ab5f8d2](https://github.com/camcima/finita/commit/ab5f8d2c818332fb9bbdf2a5a4796c3b59b1cb69))
95
+ * **internal:** add construction key symbol for graph classes ([aa17a1c](https://github.com/camcima/finita/commit/aa17a1c5b0f67acc51e2b8f969c8bb22b22603e8))
96
+ * **internal:** add OperationQueue for FIFO Statemachine operations ([7dbb4aa](https://github.com/camcima/finita/commit/7dbb4aad81ebd30f4f95e2b2361ce6a177cc2be5))
97
+ * migrate Factory and remove SetupHelper/StateCollectionMerger ([19cdd59](https://github.com/camcima/finita/commit/19cdd59c0e217d57f1fbb8ec6418324614c5596d))
98
+ * **observer:** migrate TransitionLogger, StatefulStatusChanger, CallbackObserver to v3 ([af9d082](https://github.com/camcima/finita/commit/af9d0826b3bae76a68d1d65ed922d4d46b16772f))
99
+ * **observer:** reborn OnEnterObserver as queueing after-observer ([a76e88a](https://github.com/camcima/finita/commit/a76e88ae13297d4856a6eb2ea6fd46bf9d89e018))
100
+ * **statemachine:** rewrite execution engine ([283404b](https://github.com/camcima/finita/commit/283404b14dfa394c5fb8abae2b2da5d448638d5e)), closes [#1](https://github.com/camcima/finita/issues/1) [#4](https://github.com/camcima/finita/issues/4)
101
+
102
+ ### Bug Fixes
103
+
104
+ * **builder:** correct transition target identity under cycles ([7fbbf90](https://github.com/camcima/finita/commit/7fbbf907f9e70562619bf77d5c2e04ba4bb6b1f4))
105
+ * **builder:** point transitions at final State instances ([8444883](https://github.com/camcima/finita/commit/8444883fd69d6b9c5611a26a30ac36248d45ea29))
106
+ * **builder:** treat different condition instances as conflicts regardless of name ([8a36af0](https://github.com/camcima/finita/commit/8a36af0d2a03356401e6cf6ae7c77df267337c23)), closes [#6](https://github.com/camcima/finita/issues/6)
107
+ * **ci:** pin pnpm to 9.15.0 and ignore basic-ftp dev-only advisory ([7472321](https://github.com/camcima/finita/commit/7472321953889518b95b2a32670639d530256841))
108
+ * **graph:** make GraphBuilder.addState idempotent (closes [#14](https://github.com/camcima/finita/issues/14)) ([95800db](https://github.com/camcima/finita/commit/95800dbcafaf0ce9164fad97b714855b6e73843b))
109
+ * **mutex:** runoperation honors an already-held mutex ([4d28dad](https://github.com/camcima/finita/commit/4d28dad101edb6abdc69187fc1702398c9491513))
110
+ * **security:** override undici to resolve OSV-Scanner vulnerabilities ([7aba9c1](https://github.com/camcima/finita/commit/7aba9c110ae69b0ed81d12a8323faa5df5532b76))
111
+ * **statemachine:** event observers fire on event resolution ([5757c23](https://github.com/camcima/finita/commit/5757c23e40cd1ffddc10d6effe7e7b3f1fa54946))
112
+
113
+ ## [2.2.0](https://github.com/camcima/finita/compare/v2.1.0...v2.2.0) (2026-04-06)
114
+
115
+ ### Bug Fixes
116
+
117
+ * **build:** add ignoreDeprecations for tsup baseUrl TS6 compat ([e5f66dd](https://github.com/camcima/finita/commit/e5f66ddcb5453622b17a172326ba11b403d184dd))
118
+ * **deps:** resolve known vulnerabilities in dev dependencies ([fb394d3](https://github.com/camcima/finita/commit/fb394d3011c031ba1088c7334f923dc4e67d7abd))
119
+ * drop Node.js 18 from CI matrix ([b6ce0f2](https://github.com/camcima/finita/commit/b6ce0f21c50ffcb1cd14c564154095c6ba1050c3))
120
+
121
+ ### Reverts
122
+
123
+ * restore vitest v3 and Node.js 18 support ([da591ec](https://github.com/camcima/finita/commit/da591ec05a7e2862f8fe70601bc16dc4440d35b5))
124
+
125
+ ## [2.1.0](https://github.com/camcima/finita/compare/v2.0.0...v2.1.0) (2026-04-04)
126
+
127
+ ### Features
128
+
129
+ * add dual ESM/CJS build with tsup ([2f390ba](https://github.com/camcima/finita/commit/2f390bace6ca21c1537d3e74a9234abbd8ae7ba5))
130
+
131
+ ### Bug Fixes
132
+
133
+ * add @types/node for setTimeout in test tsconfig ([e7f174c](https://github.com/camcima/finita/commit/e7f174c5b757eac699d2f36feb3c53489581d82d))
134
+
135
+ ## [2.0.0](https://github.com/camcima/finita/compare/v1.0.0...v2.0.0) (2026-03-25)
136
+
137
+ ### Features
138
+
139
+ * add TSubject generic parameter for type-safe subject access ([d3ffc3c](https://github.com/camcima/finita/commit/d3ffc3c9bff30d1df6d85c22741d780bccf038be))
140
+
141
+ ## 1.0.0 (2026-03-25)
package/dist/index.cjs CHANGED
@@ -43,6 +43,7 @@ __export(index_exports, {
43
43
  LockAdapterMutex: () => LockAdapterMutex,
44
44
  LockCanNotBeAcquiredError: () => LockCanNotBeAcquiredError,
45
45
  LockCanNotBeReleasedError: () => LockCanNotBeReleasedError,
46
+ LockOwnershipUncertainError: () => LockOwnershipUncertainError,
46
47
  MutexFactory: () => MutexFactory,
47
48
  Not: () => Not,
48
49
  NullMutex: () => NullMutex,
@@ -134,8 +135,8 @@ var INTERNAL_CONSTRUCTION_KEY = /* @__PURE__ */ Symbol(
134
135
 
135
136
  // src/error/FinitaError.ts
136
137
  var FinitaError = class _FinitaError extends Error {
137
- constructor(message) {
138
- super(message);
138
+ constructor(message, options) {
139
+ super(message, options);
139
140
  if (new.target === _FinitaError) {
140
141
  throw new TypeError(
141
142
  "FinitaError is abstract and cannot be instantiated directly"
@@ -265,11 +266,13 @@ var State = class {
265
266
  getName() {
266
267
  return this.name;
267
268
  }
269
+ /** Snapshot — the graph is shared by every machine built from the
270
+ * process, so callers must never receive the collection itself. */
268
271
  getTransitions() {
269
272
  if (this._transitions === null) {
270
273
  return [];
271
274
  }
272
- return this._transitions;
275
+ return Array.from(this._transitions);
273
276
  }
274
277
  getEventNames() {
275
278
  return Array.from(this.events.keys());
@@ -548,9 +551,11 @@ var ProcessBuilder = class _ProcessBuilder {
548
551
  }
549
552
  }
550
553
  /** Transition identity: (fromState, eventName, toState). Used by both the
551
- * 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. */
552
557
  static transitionKey(t) {
553
- return `${t.fromState}\0${t.eventName ?? ""}\0${t.toState}`;
558
+ return JSON.stringify([t.fromState, t.eventName, t.toState]);
554
559
  }
555
560
  validateNoConflictingDuplicates() {
556
561
  const seen = /* @__PURE__ */ new Map();
@@ -737,19 +742,34 @@ var NullMutex = class {
737
742
  };
738
743
 
739
744
  // src/internal/OperationQueue.ts
740
- var OperationQueue = class {
745
+ var OperationQueue = class _OperationQueue {
746
+ static COMPACT_THRESHOLD = 1024;
741
747
  items = [];
748
+ head = 0;
742
749
  enqueue(op) {
743
750
  this.items.push(op);
744
751
  }
745
752
  dequeue() {
746
- return this.items.shift();
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;
747
767
  }
748
768
  isEmpty() {
749
- return this.items.length === 0;
769
+ return this.head === this.items.length;
750
770
  }
751
771
  size() {
752
- return this.items.length;
772
+ return this.items.length - this.head;
753
773
  }
754
774
  };
755
775
 
@@ -798,6 +818,18 @@ var LockCanNotBeReleasedError = class extends FinitaError {
798
818
  }
799
819
  };
800
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
+
801
833
  // src/error/AutomaticTransitionCycleError.ts
802
834
  var AutomaticTransitionCycleError = class extends FinitaError {
803
835
  code = "automaticTransitionCycle";
@@ -835,6 +867,18 @@ var QueueLimitExceededError = class extends FinitaError {
835
867
  }
836
868
  };
837
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
+
838
882
  // src/Statemachine.ts
839
883
  var Statemachine = class {
840
884
  subject;
@@ -850,6 +894,8 @@ var Statemachine = class {
850
894
  running = false;
851
895
  idleWaiters = [];
852
896
  inSyncCallback = false;
897
+ /** Set when releasing a held lock fails; see LockOwnershipUncertainError. */
898
+ ownershipUncertainty = null;
853
899
  beforeObservers = [];
854
900
  afterObservers = [];
855
901
  onChainedOperationError;
@@ -927,6 +973,9 @@ var Statemachine = class {
927
973
  * so manual lock management keeps its existing control flow. Inspect
928
974
  * isLockAcquired() (or the hook) to learn whether the lock was actually
929
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.
930
979
  */
931
980
  async releaseLock() {
932
981
  await this.releaseMutex();
@@ -1026,6 +1075,10 @@ var Statemachine = class {
1026
1075
  }
1027
1076
  }
1028
1077
  async runOperation(op) {
1078
+ if (this.ownershipUncertainty) {
1079
+ op.reject(new LockOwnershipUncertainError(this.ownershipUncertainty.err));
1080
+ return;
1081
+ }
1029
1082
  if (op.ifStateName !== void 0 && this.currentState.getName() !== op.ifStateName) {
1030
1083
  op.resolve();
1031
1084
  return;
@@ -1066,9 +1119,15 @@ var Statemachine = class {
1066
1119
  * operation also failed, the rejection carries the operation error and this
1067
1120
  * hook is the only place the release error appears.
1068
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
+ *
1069
1127
  * @returns null on success, or the failure wrapped for the caller to raise.
1070
1128
  */
1071
1129
  async releaseMutex() {
1130
+ const held = this.mutex.isAcquired();
1072
1131
  let failure = null;
1073
1132
  try {
1074
1133
  if (!await this.mutex.releaseLock()) {
@@ -1078,13 +1137,31 @@ var Statemachine = class {
1078
1137
  failure = { err };
1079
1138
  }
1080
1139
  if (failure) {
1081
- try {
1082
- this.onReleaseError?.(failure.err);
1083
- } catch {
1084
- }
1140
+ if (held) this.ownershipUncertainty = failure;
1141
+ const err = failure.err;
1142
+ this.callDiagnosticHook(() => this.onReleaseError?.(err));
1143
+ } else {
1144
+ this.ownershipUncertainty = null;
1085
1145
  }
1086
1146
  return failure;
1087
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
+ }
1088
1165
  resolveEvent(name) {
1089
1166
  if (!this.currentState.hasEvent(name)) {
1090
1167
  throw new WrongEventForStateError(this.currentState.getName(), name);
@@ -1156,12 +1233,11 @@ var Statemachine = class {
1156
1233
  () => {
1157
1234
  },
1158
1235
  (err) => {
1159
- try {
1160
- this.onChainedOperationError?.(err, {
1236
+ this.callDiagnosticHook(
1237
+ () => this.onChainedOperationError?.(err, {
1161
1238
  eventName: chainedEventName
1162
- });
1163
- } catch {
1164
- }
1239
+ })
1240
+ );
1165
1241
  },
1166
1242
  ifStateName
1167
1243
  );
@@ -1297,6 +1373,30 @@ var CompositeCondition = class {
1297
1373
  const names = this.conditions.map((c) => c.getName());
1298
1374
  return `(${names.join(` ${this.joinWord} `)})`;
1299
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
+ }
1300
1400
  };
1301
1401
 
1302
1402
  // src/condition/AndComposite.ts
@@ -1307,13 +1407,8 @@ var AndComposite = class extends CompositeCondition {
1307
1407
  addAnd(condition) {
1308
1408
  return this.addCondition(condition);
1309
1409
  }
1310
- async checkCondition(subject, context) {
1311
- for (const condition of this.conditions) {
1312
- if (!await condition.checkCondition(subject, context)) {
1313
- return false;
1314
- }
1315
- }
1316
- return true;
1410
+ checkCondition(subject, context) {
1411
+ return this.evaluate(subject, context, false);
1317
1412
  }
1318
1413
  };
1319
1414
 
@@ -1325,13 +1420,8 @@ var OrComposite = class extends CompositeCondition {
1325
1420
  addOr(condition) {
1326
1421
  return this.addCondition(condition);
1327
1422
  }
1328
- async checkCondition(subject, context) {
1329
- for (const condition of this.conditions) {
1330
- if (await condition.checkCondition(subject, context)) {
1331
- return true;
1332
- }
1333
- }
1334
- return false;
1423
+ checkCondition(subject, context) {
1424
+ return this.evaluate(subject, context, true);
1335
1425
  }
1336
1426
  };
1337
1427
 
@@ -1344,8 +1434,13 @@ var Not = class {
1344
1434
  getName() {
1345
1435
  return `not ( ${this.condition.getName()} )`;
1346
1436
  }
1347
- async checkCondition(subject, context) {
1348
- return !await this.condition.checkCondition(subject, context);
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;
1349
1444
  }
1350
1445
  };
1351
1446
 
@@ -1400,15 +1495,6 @@ var OnEnterObserver = class _OnEnterObserver {
1400
1495
  }
1401
1496
  };
1402
1497
 
1403
- // src/util/index.ts
1404
- function isNamed(obj) {
1405
- return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
1406
- }
1407
- function nameOrString(obj) {
1408
- if (isNamed(obj)) return obj.getName();
1409
- return String(obj);
1410
- }
1411
-
1412
1498
  // src/observer/TransitionLogger.ts
1413
1499
  var TransitionLogger = class {
1414
1500
  logger;
@@ -1569,19 +1655,27 @@ var LockAdapterMutex = class {
1569
1655
  * pass the check and acquire twice on a non-idempotent adapter (database
1570
1656
  * advisory locks, redis SET NX). The pending promise is cleared once it
1571
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.
1572
1663
  */
1573
1664
  async acquireLock() {
1574
1665
  if (this.acquired) {
1575
1666
  return true;
1576
1667
  }
1577
- this.pendingAcquire ??= (async () => {
1578
- try {
1668
+ if (!this.pendingAcquire) {
1669
+ const attempt = (async () => {
1579
1670
  this.acquired = await this.lockAdapter.acquireLock(this.resourceName);
1580
1671
  return this.acquired;
1581
- } finally {
1582
- this.pendingAcquire = null;
1583
- }
1584
- })();
1672
+ })();
1673
+ this.pendingAcquire = attempt;
1674
+ const clear = () => {
1675
+ if (this.pendingAcquire === attempt) this.pendingAcquire = null;
1676
+ };
1677
+ attempt.then(clear, clear);
1678
+ }
1585
1679
  return this.pendingAcquire;
1586
1680
  }
1587
1681
  async releaseLock() {
@@ -1894,6 +1988,7 @@ var GraphBuilder = class {
1894
1988
  LockAdapterMutex,
1895
1989
  LockCanNotBeAcquiredError,
1896
1990
  LockCanNotBeReleasedError,
1991
+ LockOwnershipUncertainError,
1897
1992
  MutexFactory,
1898
1993
  Not,
1899
1994
  NullMutex,