@vedmalex/statemachine 1.0.0-beta.1 → 1.0.0-beta.2
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/README.md +36 -1
- package/dist/index.cjs +434 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +434 -18
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/types/config_validator.d.ts +32 -0
- package/types/index.d.ts +10 -0
- package/types/state_machine.d.ts +101 -0
- package/types/types.d.ts +11 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vedmalex/statemachine",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"description": "Hierarchical state machine for TypeScript with monitoring, validation, and persistence (lite-only DI-free surface).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"state-machine",
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
"clean": "rm -rf dist types coverage tsconfig.tsbuildinfo",
|
|
46
46
|
"build": "npm run clean && tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
47
47
|
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
48
|
+
"lint": "biome check .",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"check": "biome check . && tsc --noEmit",
|
|
48
51
|
"test": "vitest run",
|
|
49
52
|
"test:watch": "vitest",
|
|
50
53
|
"test:coverage": "vitest run --coverage",
|
|
@@ -53,11 +56,12 @@
|
|
|
53
56
|
"test:browser": "playwright test",
|
|
54
57
|
"knip": "knip --no-progress",
|
|
55
58
|
"api:check": "api-extractor run --local --verbose",
|
|
56
|
-
"prepublishOnly": "npm run build && node test/verify-dist.cjs"
|
|
59
|
+
"prepublishOnly": "npm run check && npm run build && node test/verify-dist.cjs"
|
|
57
60
|
},
|
|
58
61
|
"dependencies": {},
|
|
59
62
|
"peerDependencies": {},
|
|
60
63
|
"devDependencies": {
|
|
64
|
+
"@biomejs/biome": "2.4.14",
|
|
61
65
|
"@changesets/cli": "^2.31.0",
|
|
62
66
|
"@microsoft/api-extractor": "^7.58.7",
|
|
63
67
|
"@playwright/test": "^1.59.1",
|
|
@@ -65,6 +65,38 @@ export declare class ConfigValidator {
|
|
|
65
65
|
private validateInitialState;
|
|
66
66
|
private validateCrossReferences;
|
|
67
67
|
private collectStateNames;
|
|
68
|
+
/**
|
|
69
|
+
* Collects every state in the tree as {dottedPath, config} entries, walking
|
|
70
|
+
* region containers (region names are NOT registered states, so they are part
|
|
71
|
+
* of the path but never emitted as an entry — mirroring runtime expansion).
|
|
72
|
+
*/
|
|
73
|
+
private collectStateEntries;
|
|
74
|
+
/**
|
|
75
|
+
* True when `composite` has at least one `final:true` atomic substate reachable
|
|
76
|
+
* through its (possibly nested) region tree — i.e. some region of the composite
|
|
77
|
+
* can become done. Used by REGION_NO_REACHABLE_FINAL.
|
|
78
|
+
*/
|
|
79
|
+
private hasReachableFinal;
|
|
80
|
+
/**
|
|
81
|
+
* SCXML/UML final-state and all-regions-final (done.state.<C>) join validation.
|
|
82
|
+
*
|
|
83
|
+
* - FINAL_STATE_HAS_OUTGOING (error): a `final:true` state is the `from` of a
|
|
84
|
+
* transition (a final pseudo-state cannot itself transition out).
|
|
85
|
+
* - FINAL_ON_COMPOSITE (warning): `final:true` on a state that has regions
|
|
86
|
+
* (final is meaningful only on an atomic leaf; ignored at runtime).
|
|
87
|
+
* - REGION_NO_REACHABLE_FINAL (warning): a `done.state.<C>` transition exists
|
|
88
|
+
* but composite C declares no `final` substate, so the join can never fire.
|
|
89
|
+
* - DONE_VS_PARALLEL_EXIT_AMBIGUITY (warning): the same composite C has BOTH a
|
|
90
|
+
* `done.state.C` join AND a plain `from:'C'` user-event parallel-exit, which
|
|
91
|
+
* can preempt the all-final join (the user event is eligible on ANY leaf).
|
|
92
|
+
* - REGION_MISSING_INITIAL (advisory warning, valid:true): a region omits an
|
|
93
|
+
* explicit `initial`, so expansion relies on first-key insertion order.
|
|
94
|
+
*/
|
|
95
|
+
private validateFinalStates;
|
|
96
|
+
/**
|
|
97
|
+
* Collects the dotted paths of every `final:true` atomic leaf in the tree.
|
|
98
|
+
*/
|
|
99
|
+
private collectFinalStatePaths;
|
|
68
100
|
private validatePerformanceConstraints;
|
|
69
101
|
private countStates;
|
|
70
102
|
private addError;
|
package/types/index.d.ts
CHANGED
|
@@ -37,6 +37,16 @@ export type { State } from './types';
|
|
|
37
37
|
* @category Unstable
|
|
38
38
|
*/
|
|
39
39
|
export type { States, Events, Event, Adapter, StateMachineOptions, RegionsConfig, StateInvocation, StateName, EventName, RegionName, EventAction, ErrorHandler, ActionOrString, ErrorHandlerOrString, RegionStateName, NestedStateName, DeepNestedStateName, StatePaths, SimpleStateName, MethodsOf, PropertiesOf, KeysOf, ExtractAdaptee, ErrorContext, } from './types';
|
|
40
|
+
/**
|
|
41
|
+
* @category Unstable
|
|
42
|
+
* @unstable — validation preset/config shape used by validation helpers and presets.
|
|
43
|
+
*/
|
|
44
|
+
export type { ValidationConfig } from './config_validator';
|
|
45
|
+
/**
|
|
46
|
+
* @category Unstable
|
|
47
|
+
* @unstable — monitoring preset/config shape used by monitoring helpers and presets.
|
|
48
|
+
*/
|
|
49
|
+
export type { MonitoringConfig } from './monitoring';
|
|
40
50
|
export { MemoryAdapter, LocalStorageAdapter, SessionStorageAdapter, ServerAdapter, StateMachineError, isAdapter, } from './types';
|
|
41
51
|
export { createEnhancedError, EnhancedStateMachineError, ErrorAnalytics, ErrorCategory, type ErrorCategory as ErrorCategoryType, ErrorHandler as EnhancedErrorHandler, type ErrorRecoveryStrategy, ErrorSeverity, type ErrorSeverity as ErrorSeverityType, type ExtendedErrorContext, FallbackStateRecoveryStrategy, isRecoverableError, RetryRecoveryStrategy, } from './error_handling';
|
|
42
52
|
export { validateConfig, validateConfigStrict, isValidConfig, type ValidationResult, type ValidationError, type ValidationWarning, } from './config_validator';
|
package/types/state_machine.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export declare class StateMachine<TOwner extends object, SMConfig extends StateM
|
|
|
47
47
|
get currentState(): string;
|
|
48
48
|
constructor(config: SMConfig, adaptee?: Adapter<PropertiesOf<TOwner>> | PropertiesOf<TOwner>, options?: StateMachineOptions);
|
|
49
49
|
setContext(context: MethodsOf<TOwner>): void;
|
|
50
|
+
private resolveCallbackOwner;
|
|
50
51
|
private enqueueEvent;
|
|
51
52
|
private raiseEvent;
|
|
52
53
|
private scheduleProcessing;
|
|
@@ -145,6 +146,106 @@ export declare class StateMachine<TOwner extends object, SMConfig extends StateM
|
|
|
145
146
|
private getInitialCompositeState;
|
|
146
147
|
private getDirectChildren;
|
|
147
148
|
private getInitialStatesForRegions;
|
|
149
|
+
/**
|
|
150
|
+
* Whether `leaf` is a UML/SCXML `<final>` atomic state of its region.
|
|
151
|
+
*
|
|
152
|
+
* Reads the `final` marker straight from the flattened state map populated by
|
|
153
|
+
* processStates, so it works for any registered atomic leaf regardless of
|
|
154
|
+
* nesting depth. Returns `false` for unregistered names and for composites
|
|
155
|
+
* (the all-regions-final join derives doneness from atomic leaves, not from a
|
|
156
|
+
* `final` flag on a composite parent).
|
|
157
|
+
*/
|
|
158
|
+
private isStateFinal;
|
|
159
|
+
/**
|
|
160
|
+
* Whether composite `compositeId` has reached its UML/SCXML "done"
|
|
161
|
+
* configuration: every one of its regions has its active atomic leaf in a
|
|
162
|
+
* `final` state (recursively, for nested composites).
|
|
163
|
+
*
|
|
164
|
+
* D10 (mustFix): doneness is derived by scanning the active atomic `|`-leaves
|
|
165
|
+
* against the STATIC regions tree (`this.states.get(C)?.regions`), NEVER via a
|
|
166
|
+
* region-key Map lookup (`configMap.get`) — that map keys leaves by their
|
|
167
|
+
* deepest region container and so cannot answer "which leaf is active in
|
|
168
|
+
* region X of composite C". For each region we locate the active leaf via the
|
|
169
|
+
* `C.region.` dotted prefix; the region is final iff that leaf is `final`, or
|
|
170
|
+
* the leaf lives under a nested composite that is itself `isCompositeDone`.
|
|
171
|
+
*
|
|
172
|
+
* Returns `false` for a non-composite id, for a composite with no active leaf
|
|
173
|
+
* in some region, or when no substate under it is ever `final` (cheap miss).
|
|
174
|
+
*/
|
|
175
|
+
private isCompositeDone;
|
|
176
|
+
/**
|
|
177
|
+
* The OUTERMOST registered composite (regions-bearing) ancestor of `leaf`
|
|
178
|
+
* that lives strictly under `regionPrefix`, or `undefined` if the region
|
|
179
|
+
* holds a simple atomic state directly. Used by {@link isCompositeDone} to
|
|
180
|
+
* delegate a region's completeness to its nested composite (recursing over
|
|
181
|
+
* every parallel branch), independent of whether any single branch leaf
|
|
182
|
+
* happens to carry the `final` flag.
|
|
183
|
+
*
|
|
184
|
+
* ancestorChain is root-to-leaf, so the FIRST matching ancestor is the
|
|
185
|
+
* region's direct composite child (e.g. `C.p.D` for leaf `C.p.D.s.s2` under
|
|
186
|
+
* region prefix `C.p.`); isCompositeDone then recurses into its regions.
|
|
187
|
+
*/
|
|
188
|
+
private regionComposite;
|
|
189
|
+
/**
|
|
190
|
+
* Whether composite `compositeId` has reached its all-regions-final ("done")
|
|
191
|
+
* configuration in the CURRENT active state. Public guard surface (`@stable`)
|
|
192
|
+
* for authoring a join as `guard: () => sm.isDone('C')` instead of (or
|
|
193
|
+
* alongside) listening on the engine `done.state.<C>` event.
|
|
194
|
+
*
|
|
195
|
+
* @param compositeId - The dotted id of the composite/parallel state.
|
|
196
|
+
* @returns `true` iff every region's active atomic leaf is `final`.
|
|
197
|
+
*/
|
|
198
|
+
isDone(compositeId: string, adaptee?: Adapter<PropertiesOf<TOwner>>): boolean;
|
|
199
|
+
/**
|
|
200
|
+
* SCXML completion hook (D10/D11/D12): after a new configuration is written,
|
|
201
|
+
* raise `done.state.<C>` for each composite that became all-regions-final.
|
|
202
|
+
*
|
|
203
|
+
* - Scans only composites that GAINED an active leaf (the ancestor chain of
|
|
204
|
+
* each `|`-leaf of `newState`), so unaffected composites are not re-checked.
|
|
205
|
+
* - Emits INNERMOST-first (a deeper composite's `done.state` precedes its
|
|
206
|
+
* parent's), matching SCXML's inner-before-outer completion ordering, via a
|
|
207
|
+
* per-config emitted-id Set so each id is raised at most once per call.
|
|
208
|
+
* - Gates each emission on `this.events.has('done.state.'+C)` (D11 mustFix):
|
|
209
|
+
* raising an undeclared event would hit the `Invalid event` throw
|
|
210
|
+
* (executeQueuedTransition) as an unhandled microtask rejection. No declared
|
|
211
|
+
* `done.state.<C>` event => no emission, no crash, no observable effect.
|
|
212
|
+
* - Uses the internal queue (`raiseEvent`) + `scheduleProcessing` so the
|
|
213
|
+
* completion event is processed before subsequent external events.
|
|
214
|
+
*/
|
|
215
|
+
private checkCompletion;
|
|
216
|
+
/**
|
|
217
|
+
* Build the registered ancestor chain for an atomic leaf, ordered root-to-leaf.
|
|
218
|
+
*
|
|
219
|
+
* Walks every dot-prefix of `leaf` and keeps only the ones that are real
|
|
220
|
+
* registered states (`this.states.has`). Region containers are never
|
|
221
|
+
* registered (only composite parents and atomic leaves are — see
|
|
222
|
+
* processStates/processRegions), so they are filtered out automatically. The
|
|
223
|
+
* result is exactly `[parent..leaf]` for a leaf inside a composite, and
|
|
224
|
+
* `[leaf]` for a flat state.
|
|
225
|
+
*
|
|
226
|
+
* Example: `ancestorChain('a.r1.c1')` -> `['a', 'a.r1.c1']` (the `a.r1`
|
|
227
|
+
* region container is excluded). Nested:
|
|
228
|
+
* `ancestorChain('a.r1.c1.r3.x')` -> `['a', 'a.r1.c1', 'a.r1.c1.r3.x']`.
|
|
229
|
+
*/
|
|
230
|
+
private ancestorChain;
|
|
231
|
+
/**
|
|
232
|
+
* Compute the ordered enter/exit sets between two composite configurations
|
|
233
|
+
* (SCXML ancestor-first entry / descendant-first exit).
|
|
234
|
+
*
|
|
235
|
+
* For each `|`-separated atomic leaf in `oldComposite` and `newComposite` the
|
|
236
|
+
* union of its {@link ancestorChain} forms the old/new active ancestry. A
|
|
237
|
+
* state shared by both ancestries lands in NEITHER diff, so a surviving
|
|
238
|
+
* ancestor is never re-entered nor exited (no onEnter/onExit re-fire, no
|
|
239
|
+
* timer re-arm/leak).
|
|
240
|
+
*
|
|
241
|
+
* - `enterStates` = new ancestry MINUS old, sorted ascending by depth then
|
|
242
|
+
* document (insertion) order -> root-to-leaf entry.
|
|
243
|
+
* - `exitStates` = old ancestry MINUS new, sorted descending by depth ->
|
|
244
|
+
* leaf-to-root exit.
|
|
245
|
+
*
|
|
246
|
+
* Consumed by BOTH R1 (applyTransition / setInitialState / reset) and R2.
|
|
247
|
+
*/
|
|
248
|
+
private computeEnterExitSets;
|
|
148
249
|
private validateCompositeState;
|
|
149
250
|
private processStates;
|
|
150
251
|
private processRegions;
|
package/types/types.d.ts
CHANGED
|
@@ -119,7 +119,7 @@ export type DeepNestedStateName<S> = {
|
|
|
119
119
|
}[keyof R & string] : never : never;
|
|
120
120
|
}[keyof S & string];
|
|
121
121
|
export type StatePaths<S> = SimpleStateName<S> | RegionStateName<S> | NestedStateName<S> | DeepNestedStateName<S>;
|
|
122
|
-
export type ActionOrString<T extends object, R = void> = KeysOf<T, EventAction<T, R>> | EventAction<
|
|
122
|
+
export type ActionOrString<T extends object, R = void> = KeysOf<T, EventAction<T, R>> | EventAction<T, R>;
|
|
123
123
|
export type ErrorHandlerOrString<T extends object> = KeysOf<T, ErrorHandler<T>> | ErrorHandler<T>;
|
|
124
124
|
export type RegionName = string;
|
|
125
125
|
export type RegionsConfig<T extends object> = Record<RegionName, StateMachineConfig<T>['states']>;
|
|
@@ -138,6 +138,15 @@ export type State<T extends object> = {
|
|
|
138
138
|
regions?: RegionsConfig<T>;
|
|
139
139
|
initial?: StateName;
|
|
140
140
|
history?: 'deep' | 'shallow';
|
|
141
|
+
/**
|
|
142
|
+
* Marks this atomic state as the SCXML/UML `<final>` pseudo-substate of its
|
|
143
|
+
* region. When every region of a composite has its active atomic leaf marked
|
|
144
|
+
* `final`, that composite is "done": the engine raises the `done.state.<id>`
|
|
145
|
+
* event and {@link StateMachine.isDone} returns `true`. Only meaningful on a
|
|
146
|
+
* leaf state (a state without `regions`); set on a composite it is ignored at
|
|
147
|
+
* runtime and flagged by the config validator.
|
|
148
|
+
*/
|
|
149
|
+
final?: boolean;
|
|
141
150
|
invoke?: StateInvocation<T>[];
|
|
142
151
|
};
|
|
143
152
|
export interface StateInvocation<T extends object> {
|
|
@@ -188,7 +197,7 @@ export interface StateMachineConfig<T extends object = object> {
|
|
|
188
197
|
initialState: keyof StateMachineConfig<T>['states'];
|
|
189
198
|
events: Record<EventName, Omit<Event<T, StateMachineConfig<T>['states']>, 'name'>>;
|
|
190
199
|
states: States<T>;
|
|
191
|
-
onError?:
|
|
200
|
+
onError?: ErrorHandlerOrString<T>;
|
|
192
201
|
}
|
|
193
202
|
export interface StatePersistenceAdapter {
|
|
194
203
|
save(state?: {
|