@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 +141 -0
- package/dist/index.cjs +144 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -10
- package/dist/index.d.ts +69 -10
- package/dist/index.js +143 -49
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
1160
|
-
this.onChainedOperationError?.(err, {
|
|
1236
|
+
this.callDiagnosticHook(
|
|
1237
|
+
() => this.onChainedOperationError?.(err, {
|
|
1161
1238
|
eventName: chainedEventName
|
|
1162
|
-
})
|
|
1163
|
-
|
|
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
|
-
|
|
1311
|
-
|
|
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
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
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
|
|
1578
|
-
|
|
1668
|
+
if (!this.pendingAcquire) {
|
|
1669
|
+
const attempt = (async () => {
|
|
1579
1670
|
this.acquired = await this.lockAdapter.acquireLock(this.resourceName);
|
|
1580
1671
|
return this.acquired;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
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,
|