@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vedmalex/statemachine",
3
- "version": "1.0.0-beta.1",
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';
@@ -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<Adapter<T>, R>;
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?: KeysOf<T, ErrorHandler<T>>;
200
+ onError?: ErrorHandlerOrString<T>;
192
201
  }
193
202
  export interface StatePersistenceAdapter {
194
203
  save(state?: {