@camcima/finita 3.0.1 → 4.0.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/dist/index.d.cts CHANGED
@@ -9,16 +9,26 @@ interface Metadata {
9
9
  }
10
10
 
11
11
  interface Observer {
12
- update(subject: ObservableSubject): MaybePromise<void>;
12
+ /**
13
+ * @param args The arguments the notification was invoked with — for
14
+ * Statemachine events, [subject, context]. Passed per-call so shared
15
+ * Event instances carry no per-invocation state.
16
+ */
17
+ update(subject: ObservableSubject, args?: readonly unknown[]): MaybePromise<void>;
13
18
  }
14
19
  interface ObservableSubject {
15
20
  attach(observer: Observer): void;
16
21
  detach(observer: Observer): void;
17
- notify(): Promise<void>;
22
+ notify(args?: readonly unknown[]): Promise<void>;
18
23
  getObservers(): Iterable<Observer>;
19
24
  }
20
25
 
21
26
  interface EventInterface extends Named, Metadata, ObservableSubject {
27
+ /**
28
+ * @deprecated Always returns []. Invoke args are now passed directly to
29
+ * Observer.update — reading them from the event was racy when one
30
+ * Process served multiple Statemachines.
31
+ */
22
32
  getInvokeArgs(): unknown[];
23
33
  invoke(...args: unknown[]): Promise<void>;
24
34
  getMetadataValue(key: string): unknown;
@@ -31,14 +41,18 @@ declare class Event implements EventInterface {
31
41
  private readonly name;
32
42
  private readonly observers;
33
43
  private readonly metadata;
34
- private invokeArgs;
35
44
  constructor(name: string);
36
45
  getName(): string;
46
+ /**
47
+ * @deprecated Always returns []. Invoke args are now passed directly to
48
+ * Observer.update — reading them from the event was racy when one
49
+ * Process served multiple Statemachines.
50
+ */
37
51
  getInvokeArgs(): unknown[];
38
52
  invoke(...args: unknown[]): Promise<void>;
39
53
  attach(observer: Observer): void;
40
54
  detach(observer: Observer): void;
41
- notify(): Promise<void>;
55
+ notify(args?: readonly unknown[]): Promise<void>;
42
56
  getObservers(): Iterable<Observer>;
43
57
  getMetadata(): Record<string, unknown>;
44
58
  getMetadataValue(key: string): unknown;
@@ -167,9 +181,14 @@ declare class ProcessBuilder<TSubject = unknown> {
167
181
  addState(name: string, options?: AddStateOptions): this;
168
182
  addTransition(fromState: string, toState: string, options?: AddTransitionOptions<TSubject>): this;
169
183
  build(options?: BuildOptions): Process;
184
+ /** One name rule for every named entity: non-empty, no leading/trailing whitespace. */
185
+ private validateName;
170
186
  private validateInitialState;
171
187
  private findInitialStateName;
172
188
  private validateTransitionEndpoints;
189
+ /** Transition identity: (fromState, eventName, toState). Used by both the
190
+ * conflict check and the build-time dedup — keep them in lockstep. */
191
+ private static transitionKey;
173
192
  private validateNoConflictingDuplicates;
174
193
  private collectEventNamesByState;
175
194
  /**
@@ -187,13 +206,18 @@ declare class ProcessBuilder<TSubject = unknown> {
187
206
  }
188
207
 
189
208
  /**
190
- * Immutable snapshot passed to AfterTransitionObserver.notify().
209
+ * Immutable snapshot passed to transition observers.
191
210
  *
192
- * Captures the post-commit transition: state has already moved from
193
- * fromState to toState. Reading any field is safe and stable for the
194
- * duration of the observer call (and beyond — the frame is frozen).
211
+ * For AfterTransitionObserver.notify() the transition has committed:
212
+ * state has already moved from fromState to toState. For
213
+ * BeforeTransitionObserver.notify() the same shape represents the
214
+ * *proposed* transition — fromState is still the current state, and
215
+ * throwing aborts the commit. Reading any field is safe and stable for
216
+ * the duration of the observer call (and beyond — the frame is frozen).
195
217
  */
196
218
  interface TransitionFrame<TSubject = unknown> {
219
+ /** The subject this machine drives — identifies whose transition this is. */
220
+ readonly subject: TSubject;
197
221
  readonly fromState: StateInterface;
198
222
  readonly toState: StateInterface;
199
223
  readonly transition: TransitionInterface<TSubject>;
@@ -204,14 +228,10 @@ interface TransitionFrame<TSubject = unknown> {
204
228
  readonly machineName: string | null;
205
229
  }
206
230
  /**
207
- * Immutable snapshot passed to BeforeTransitionObserver.notify().
208
- *
209
- * Same shape as TransitionFrame but represents a *proposed* transition
210
- * — fromState is still the current state at notification time. Throwing
211
- * from a before-observer aborts the transition; otherwise commit proceeds.
231
+ * The frame as seen by BeforeTransitionObserver — same shape; the
232
+ * distinct name documents the pre-commit timing.
212
233
  */
213
- interface ProposedTransitionFrame<TSubject = unknown> extends TransitionFrame<TSubject> {
214
- }
234
+ type ProposedTransitionFrame<TSubject = unknown> = TransitionFrame<TSubject>;
215
235
 
216
236
  /**
217
237
  * Runs before a transition commits. Throwing aborts the transition —
@@ -219,7 +239,9 @@ interface ProposedTransitionFrame<TSubject = unknown> extends TransitionFrame<TS
219
239
  * the thrown error.
220
240
  *
221
241
  * Implementations must be pure relative to the FSM: they MUST NOT call
222
- * triggerEvent / checkTransitions on the same Statemachine. There is no
242
+ * triggerEvent / checkTransitions on the same Statemachine. Doing so throws
243
+ * ReentrancyError when the call happens before the observer's first await;
244
+ * calls made after an await cannot be detected and will deadlock. There is no
223
245
  * enqueue handle in the before phase by design — vetoes and validations
224
246
  * complete synchronously per observer; chained behaviour belongs in
225
247
  * AfterTransitionObserver.
@@ -237,7 +259,14 @@ interface BeforeTransitionObserver<TSubject = unknown> {
237
259
  * (and any auto-follow-on transitions) completes.
238
260
  */
239
261
  interface EnqueueContext {
240
- enqueue(event: string, context?: Map<string, unknown>): void;
262
+ /**
263
+ * @param ifStateName When provided, the enqueued event is silently
264
+ * skipped unless the machine is still in that state when the operation
265
+ * is dequeued — the machine may have moved on in the meantime.
266
+ * If the machine leaves and returns to that state, the op is not skipped
267
+ * — only the state name is compared, not entry identity or count.
268
+ */
269
+ enqueue(event: string, context?: Map<string, unknown>, ifStateName?: string): void;
241
270
  }
242
271
  /**
243
272
  * Runs after a transition has committed. State has already moved.
@@ -291,6 +320,16 @@ interface StatemachineOptions<TSubject = unknown> {
291
320
  mutex?: MutexInterface;
292
321
  /** When true, the engine releases the mutex at the end of each top-level operation. Defaults to true. */
293
322
  autoreleaseLock?: boolean;
323
+ /**
324
+ * Maximum number of automatic (eventless) transitions a single operation
325
+ * may take before AutomaticTransitionCycleError is thrown. Guards against
326
+ * non-terminating automatic loops while allowing legitimate bounded loops
327
+ * (e.g. condition-terminated retry cycles). Note: transitions committed
328
+ * before the limit is hit are NOT rolled back. Must be a positive integer;
329
+ * constructing a Statemachine with a value < 1 throws a RangeError.
330
+ * Defaults to 100.
331
+ */
332
+ maxAutomaticHops?: number;
294
333
  }
295
334
 
296
335
  declare class Statemachine<TSubject = unknown> implements StatemachineInterface<TSubject> {
@@ -301,8 +340,10 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
301
340
  private currentState;
302
341
  private lastState;
303
342
  private autoreleaseLock;
343
+ private readonly maxAutomaticHops;
304
344
  private readonly queue;
305
345
  private running;
346
+ private inSyncCallback;
306
347
  private readonly beforeObservers;
307
348
  private readonly afterObservers;
308
349
  constructor(subject: TSubject, process: ProcessInterface, options?: StatemachineOptions<TSubject>);
@@ -323,6 +364,16 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
323
364
  setAutoreleaseLock(autorelease: boolean): void;
324
365
  triggerEvent(name: string, context?: Map<string, unknown>): Promise<void>;
325
366
  checkTransitions(context?: Map<string, unknown>): Promise<void>;
367
+ /** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
368
+ * the flag is cleared as soon as fn returns (before any promise it returned
369
+ * is awaited), so concurrent external callers are never affected. This
370
+ * catches triggerEvent/checkTransitions calls made before a callback's first
371
+ * await; calls made after a prior await are not detectable without
372
+ * AsyncLocalStorage (Node-only) and will still deadlock — a documented gap. */
373
+ private guardSync;
374
+ private assertNotReentrant;
375
+ /** Single entry point to the operation queue — every enqueue kicks the runner. */
376
+ private enqueueOperation;
326
377
  private runIfIdle;
327
378
  private runOperation;
328
379
  private resolveEvent;
@@ -372,9 +423,11 @@ interface LastStateHasChangedDateInterface {
372
423
  getLastStateHasChangedDate(): Date;
373
424
  }
374
425
 
426
+ /** @deprecated No longer used internally; will be removed in v4. */
375
427
  interface CallbackInterface {
376
428
  invoke(): MaybePromise<void>;
377
429
  }
430
+ /** @deprecated No longer used internally; will be removed in v4. */
378
431
  interface DispatcherInterface extends CallbackInterface {
379
432
  dispatch(event: EventInterface, args?: unknown[]): void;
380
433
  invoke(): Promise<void>;
@@ -416,19 +469,24 @@ declare class Timeout implements ConditionInterface {
416
469
  checkCondition(subject: unknown, context: Map<string, unknown>): boolean;
417
470
  }
418
471
 
419
- declare class AndComposite<TSubject = unknown> implements ConditionInterface<TSubject> {
420
- private readonly conditions;
472
+ declare abstract class CompositeCondition<TSubject = unknown> implements ConditionInterface<TSubject> {
473
+ protected readonly conditions: ConditionInterface<TSubject>[];
474
+ private readonly joinWord;
475
+ constructor(joinWord: string, condition: ConditionInterface<TSubject>);
476
+ protected addCondition(condition: ConditionInterface<TSubject>): this;
477
+ getName(): string;
478
+ abstract checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
479
+ }
480
+
481
+ declare class AndComposite<TSubject = unknown> extends CompositeCondition<TSubject> {
421
482
  constructor(condition: ConditionInterface<TSubject>);
422
483
  addAnd(condition: ConditionInterface<TSubject>): this;
423
- getName(): string;
424
484
  checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
425
485
  }
426
486
 
427
- declare class OrComposite<TSubject = unknown> implements ConditionInterface<TSubject> {
428
- private readonly conditions;
487
+ declare class OrComposite<TSubject = unknown> extends CompositeCondition<TSubject> {
429
488
  constructor(condition: ConditionInterface<TSubject>);
430
489
  addOr(condition: ConditionInterface<TSubject>): this;
431
- getName(): string;
432
490
  checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
433
491
  }
434
492
 
@@ -449,12 +507,18 @@ declare class Not<TSubject = unknown> implements ConditionInterface<TSubject> {
449
507
  declare class CallbackObserver implements Observer {
450
508
  private readonly callback;
451
509
  constructor(callback: (...args: unknown[]) => MaybePromise<void>);
452
- update(subject: ObservableSubject): MaybePromise<void>;
510
+ update(subject: ObservableSubject, args?: readonly unknown[]): MaybePromise<void>;
453
511
  }
454
512
 
455
513
  declare class StatefulStatusChanger<TSubject extends StatefulInterface> implements AfterTransitionObserver<TSubject> {
456
514
  private readonly subject;
457
- constructor(subject: TSubject);
515
+ /**
516
+ * @param subject Optional explicit subject to write to. When omitted
517
+ * (recommended), the observer writes to frame.subject — the subject of
518
+ * whichever machine fired the transition — so a single instance can be
519
+ * shared safely across every machine a Factory creates.
520
+ */
521
+ constructor(subject?: TSubject);
458
522
  notify(frame: TransitionFrame<TSubject>): void;
459
523
  }
460
524
 
@@ -466,6 +530,10 @@ declare class StatefulStatusChanger<TSubject extends StatefulInterface> implemen
466
530
  * top-level operation after the current operation completes. Other
467
531
  * after-observers registered after OnEnterObserver still see the original
468
532
  * frame, not the chained one.
533
+ *
534
+ * The chained event only fires if the machine is still in the entered state
535
+ * when the queue drains — states passed through transiently by automatic
536
+ * transitions do not fire onEnter.
469
537
  */
470
538
  declare class OnEnterObserver<TSubject = unknown> implements AfterTransitionObserver<TSubject> {
471
539
  static readonly DEFAULT_EVENT_NAME = "onEnter";
@@ -482,7 +550,14 @@ declare class TransitionLogger<TSubject = unknown> implements AfterTransitionObs
482
550
  }
483
551
 
484
552
  declare class ActiveTransitionFilter {
485
- static filter<TSubject = unknown>(transitions: Iterable<TransitionInterface<TSubject>>, subject: TSubject, context: Map<string, unknown>, event?: EventInterface): Promise<TransitionInterface<TSubject>[]>;
553
+ static filter<TSubject = unknown>(transitions: Iterable<TransitionInterface<TSubject>>, subject: TSubject, context: Map<string, unknown>, event?: EventInterface,
554
+ /**
555
+ * Optional wrapper run around each individual isActive() evaluation. The
556
+ * Statemachine passes its re-entrancy guard here so that every condition —
557
+ * not just the first — is evaluated with the guard active; without per-item
558
+ * wrapping a re-entrant condition on a later transition would deadlock.
559
+ */
560
+ wrap?: <T>(fn: () => T) => T): Promise<TransitionInterface<TSubject>[]>;
486
561
  }
487
562
 
488
563
  declare class FilterStateByEvent {
@@ -648,7 +723,7 @@ declare class ProcessFinalizedError extends FinitaError {
648
723
  constructor(processName: string);
649
724
  }
650
725
 
651
- type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidEventName" | "invalidConditionName" | "orphanState";
726
+ type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidStateName" | "invalidEventName" | "invalidConditionName" | "orphanState";
652
727
  declare class GraphValidationError extends FinitaError {
653
728
  readonly code: GraphValidationCode;
654
729
  readonly details: Readonly<Record<string, unknown>>;
@@ -661,6 +736,8 @@ interface DuplicateTransitionConflict {
661
736
  eventName: string | null;
662
737
  existingConditionName: string | null;
663
738
  newConditionName: string | null;
739
+ existingWeight?: number;
740
+ newWeight?: number;
664
741
  }
665
742
  declare class DuplicateTransitionError extends FinitaError {
666
743
  readonly code = "duplicateTransition";
@@ -704,9 +781,14 @@ declare class AmbiguousTransitionError extends FinitaError {
704
781
 
705
782
  declare class AutomaticTransitionCycleError extends FinitaError {
706
783
  readonly code = "automaticTransitionCycle";
707
- readonly targetStateName: string;
708
- readonly visitedStateNames: readonly string[];
709
- constructor(targetStateName: string, visitedStateNames: Iterable<string>);
784
+ readonly stateName: string;
785
+ readonly hopLimit: number;
786
+ constructor(stateName: string, hopLimit: number);
787
+ }
788
+
789
+ declare class ReentrancyError extends FinitaError {
790
+ readonly code = "reentrancy";
791
+ constructor(operation: string);
710
792
  }
711
793
 
712
- export { AbstractNamedProcessDetector, ActiveTransitionFilter, type AddStateOptions, type AddTransitionOptions, type AfterTransitionObserver, AmbiguousTransitionError, AndComposite, AutomaticTransitionCycleError, type BeforeTransitionObserver, type BuildOptions, CallbackCondition, type CallbackInterface, CallbackObserver, type ConditionCallbackFn, type ConditionInterface, Contradiction, type DispatcherInterface, type DotOptions, DuplicateStateError, type DuplicateTransitionConflict, DuplicateTransitionError, type EnqueueContext, Event, type EventInterface, Factory, type FactoryInterface, FilterStateByEvent, FilterStateByFinalState, FilterStateByTransition, FilterTransitionByEvent, FinitaError, type Graph, GraphBuilder, type GraphEdge, type GraphNode, type GraphValidationCode, GraphValidationError, InvalidSubjectError, type LastStateHasChangedDateInterface, type LockAdapterInterface, LockAdapterMutex, LockCanNotBeAcquiredError, type LoggerInterface, type MaybePromise, type MermaidOptions, type Metadata, MutexFactory, type MutexFactoryInterface, type MutexInterface, type Named, Not, NullMutex, type ObservableSubject, type Observer, OnEnterObserver, OneOrNoneActiveTransition, OrComposite, Process, ProcessBuilder, type ProcessDetectorInterface, ProcessFinalizedError, type ProcessInterface, ProcessNotFoundError, type ProposedTransitionFrame, ScoreTransition, SingleProcessDetector, State, type StateCollectionInterface, StateEventNotFoundError, type StateInterface, type StateNameDetectorInterface, StateNotFoundError, type StatefulInterface, StatefulStateNameDetector, StatefulStatusChanger, Statemachine, type StatemachineInterface, type StatemachineOptions, type StringConverter, Tautology, Timeout, Transition, type TransitionFrame, type TransitionInterface, TransitionLogger, type TransitionSelectorInterface, WeightTransition, type Weighted, WrongEventForStateError };
794
+ export { AbstractNamedProcessDetector, ActiveTransitionFilter, type AddStateOptions, type AddTransitionOptions, type AfterTransitionObserver, AmbiguousTransitionError, AndComposite, AutomaticTransitionCycleError, type BeforeTransitionObserver, type BuildOptions, CallbackCondition, type CallbackInterface, CallbackObserver, type ConditionCallbackFn, type ConditionInterface, Contradiction, type DispatcherInterface, type DotOptions, DuplicateStateError, type DuplicateTransitionConflict, DuplicateTransitionError, type EnqueueContext, Event, type EventInterface, Factory, type FactoryInterface, FilterStateByEvent, FilterStateByFinalState, FilterStateByTransition, FilterTransitionByEvent, FinitaError, type Graph, GraphBuilder, type GraphEdge, type GraphNode, type GraphValidationCode, GraphValidationError, InvalidSubjectError, type LastStateHasChangedDateInterface, type LockAdapterInterface, LockAdapterMutex, LockCanNotBeAcquiredError, type LoggerInterface, type MaybePromise, type MermaidOptions, type Metadata, MutexFactory, type MutexFactoryInterface, type MutexInterface, type Named, Not, NullMutex, type ObservableSubject, type Observer, OnEnterObserver, OneOrNoneActiveTransition, OrComposite, Process, ProcessBuilder, type ProcessDetectorInterface, ProcessFinalizedError, type ProcessInterface, ProcessNotFoundError, type ProposedTransitionFrame, ReentrancyError, ScoreTransition, SingleProcessDetector, State, type StateCollectionInterface, StateEventNotFoundError, type StateInterface, type StateNameDetectorInterface, StateNotFoundError, type StatefulInterface, StatefulStateNameDetector, StatefulStatusChanger, Statemachine, type StatemachineInterface, type StatemachineOptions, type StringConverter, Tautology, Timeout, Transition, type TransitionFrame, type TransitionInterface, TransitionLogger, type TransitionSelectorInterface, WeightTransition, type Weighted, WrongEventForStateError };
package/dist/index.d.ts CHANGED
@@ -9,16 +9,26 @@ interface Metadata {
9
9
  }
10
10
 
11
11
  interface Observer {
12
- update(subject: ObservableSubject): MaybePromise<void>;
12
+ /**
13
+ * @param args The arguments the notification was invoked with — for
14
+ * Statemachine events, [subject, context]. Passed per-call so shared
15
+ * Event instances carry no per-invocation state.
16
+ */
17
+ update(subject: ObservableSubject, args?: readonly unknown[]): MaybePromise<void>;
13
18
  }
14
19
  interface ObservableSubject {
15
20
  attach(observer: Observer): void;
16
21
  detach(observer: Observer): void;
17
- notify(): Promise<void>;
22
+ notify(args?: readonly unknown[]): Promise<void>;
18
23
  getObservers(): Iterable<Observer>;
19
24
  }
20
25
 
21
26
  interface EventInterface extends Named, Metadata, ObservableSubject {
27
+ /**
28
+ * @deprecated Always returns []. Invoke args are now passed directly to
29
+ * Observer.update — reading them from the event was racy when one
30
+ * Process served multiple Statemachines.
31
+ */
22
32
  getInvokeArgs(): unknown[];
23
33
  invoke(...args: unknown[]): Promise<void>;
24
34
  getMetadataValue(key: string): unknown;
@@ -31,14 +41,18 @@ declare class Event implements EventInterface {
31
41
  private readonly name;
32
42
  private readonly observers;
33
43
  private readonly metadata;
34
- private invokeArgs;
35
44
  constructor(name: string);
36
45
  getName(): string;
46
+ /**
47
+ * @deprecated Always returns []. Invoke args are now passed directly to
48
+ * Observer.update — reading them from the event was racy when one
49
+ * Process served multiple Statemachines.
50
+ */
37
51
  getInvokeArgs(): unknown[];
38
52
  invoke(...args: unknown[]): Promise<void>;
39
53
  attach(observer: Observer): void;
40
54
  detach(observer: Observer): void;
41
- notify(): Promise<void>;
55
+ notify(args?: readonly unknown[]): Promise<void>;
42
56
  getObservers(): Iterable<Observer>;
43
57
  getMetadata(): Record<string, unknown>;
44
58
  getMetadataValue(key: string): unknown;
@@ -167,9 +181,14 @@ declare class ProcessBuilder<TSubject = unknown> {
167
181
  addState(name: string, options?: AddStateOptions): this;
168
182
  addTransition(fromState: string, toState: string, options?: AddTransitionOptions<TSubject>): this;
169
183
  build(options?: BuildOptions): Process;
184
+ /** One name rule for every named entity: non-empty, no leading/trailing whitespace. */
185
+ private validateName;
170
186
  private validateInitialState;
171
187
  private findInitialStateName;
172
188
  private validateTransitionEndpoints;
189
+ /** Transition identity: (fromState, eventName, toState). Used by both the
190
+ * conflict check and the build-time dedup — keep them in lockstep. */
191
+ private static transitionKey;
173
192
  private validateNoConflictingDuplicates;
174
193
  private collectEventNamesByState;
175
194
  /**
@@ -187,13 +206,18 @@ declare class ProcessBuilder<TSubject = unknown> {
187
206
  }
188
207
 
189
208
  /**
190
- * Immutable snapshot passed to AfterTransitionObserver.notify().
209
+ * Immutable snapshot passed to transition observers.
191
210
  *
192
- * Captures the post-commit transition: state has already moved from
193
- * fromState to toState. Reading any field is safe and stable for the
194
- * duration of the observer call (and beyond — the frame is frozen).
211
+ * For AfterTransitionObserver.notify() the transition has committed:
212
+ * state has already moved from fromState to toState. For
213
+ * BeforeTransitionObserver.notify() the same shape represents the
214
+ * *proposed* transition — fromState is still the current state, and
215
+ * throwing aborts the commit. Reading any field is safe and stable for
216
+ * the duration of the observer call (and beyond — the frame is frozen).
195
217
  */
196
218
  interface TransitionFrame<TSubject = unknown> {
219
+ /** The subject this machine drives — identifies whose transition this is. */
220
+ readonly subject: TSubject;
197
221
  readonly fromState: StateInterface;
198
222
  readonly toState: StateInterface;
199
223
  readonly transition: TransitionInterface<TSubject>;
@@ -204,14 +228,10 @@ interface TransitionFrame<TSubject = unknown> {
204
228
  readonly machineName: string | null;
205
229
  }
206
230
  /**
207
- * Immutable snapshot passed to BeforeTransitionObserver.notify().
208
- *
209
- * Same shape as TransitionFrame but represents a *proposed* transition
210
- * — fromState is still the current state at notification time. Throwing
211
- * from a before-observer aborts the transition; otherwise commit proceeds.
231
+ * The frame as seen by BeforeTransitionObserver — same shape; the
232
+ * distinct name documents the pre-commit timing.
212
233
  */
213
- interface ProposedTransitionFrame<TSubject = unknown> extends TransitionFrame<TSubject> {
214
- }
234
+ type ProposedTransitionFrame<TSubject = unknown> = TransitionFrame<TSubject>;
215
235
 
216
236
  /**
217
237
  * Runs before a transition commits. Throwing aborts the transition —
@@ -219,7 +239,9 @@ interface ProposedTransitionFrame<TSubject = unknown> extends TransitionFrame<TS
219
239
  * the thrown error.
220
240
  *
221
241
  * Implementations must be pure relative to the FSM: they MUST NOT call
222
- * triggerEvent / checkTransitions on the same Statemachine. There is no
242
+ * triggerEvent / checkTransitions on the same Statemachine. Doing so throws
243
+ * ReentrancyError when the call happens before the observer's first await;
244
+ * calls made after an await cannot be detected and will deadlock. There is no
223
245
  * enqueue handle in the before phase by design — vetoes and validations
224
246
  * complete synchronously per observer; chained behaviour belongs in
225
247
  * AfterTransitionObserver.
@@ -237,7 +259,14 @@ interface BeforeTransitionObserver<TSubject = unknown> {
237
259
  * (and any auto-follow-on transitions) completes.
238
260
  */
239
261
  interface EnqueueContext {
240
- enqueue(event: string, context?: Map<string, unknown>): void;
262
+ /**
263
+ * @param ifStateName When provided, the enqueued event is silently
264
+ * skipped unless the machine is still in that state when the operation
265
+ * is dequeued — the machine may have moved on in the meantime.
266
+ * If the machine leaves and returns to that state, the op is not skipped
267
+ * — only the state name is compared, not entry identity or count.
268
+ */
269
+ enqueue(event: string, context?: Map<string, unknown>, ifStateName?: string): void;
241
270
  }
242
271
  /**
243
272
  * Runs after a transition has committed. State has already moved.
@@ -291,6 +320,16 @@ interface StatemachineOptions<TSubject = unknown> {
291
320
  mutex?: MutexInterface;
292
321
  /** When true, the engine releases the mutex at the end of each top-level operation. Defaults to true. */
293
322
  autoreleaseLock?: boolean;
323
+ /**
324
+ * Maximum number of automatic (eventless) transitions a single operation
325
+ * may take before AutomaticTransitionCycleError is thrown. Guards against
326
+ * non-terminating automatic loops while allowing legitimate bounded loops
327
+ * (e.g. condition-terminated retry cycles). Note: transitions committed
328
+ * before the limit is hit are NOT rolled back. Must be a positive integer;
329
+ * constructing a Statemachine with a value < 1 throws a RangeError.
330
+ * Defaults to 100.
331
+ */
332
+ maxAutomaticHops?: number;
294
333
  }
295
334
 
296
335
  declare class Statemachine<TSubject = unknown> implements StatemachineInterface<TSubject> {
@@ -301,8 +340,10 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
301
340
  private currentState;
302
341
  private lastState;
303
342
  private autoreleaseLock;
343
+ private readonly maxAutomaticHops;
304
344
  private readonly queue;
305
345
  private running;
346
+ private inSyncCallback;
306
347
  private readonly beforeObservers;
307
348
  private readonly afterObservers;
308
349
  constructor(subject: TSubject, process: ProcessInterface, options?: StatemachineOptions<TSubject>);
@@ -323,6 +364,16 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
323
364
  setAutoreleaseLock(autorelease: boolean): void;
324
365
  triggerEvent(name: string, context?: Map<string, unknown>): Promise<void>;
325
366
  checkTransitions(context?: Map<string, unknown>): Promise<void>;
367
+ /** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
368
+ * the flag is cleared as soon as fn returns (before any promise it returned
369
+ * is awaited), so concurrent external callers are never affected. This
370
+ * catches triggerEvent/checkTransitions calls made before a callback's first
371
+ * await; calls made after a prior await are not detectable without
372
+ * AsyncLocalStorage (Node-only) and will still deadlock — a documented gap. */
373
+ private guardSync;
374
+ private assertNotReentrant;
375
+ /** Single entry point to the operation queue — every enqueue kicks the runner. */
376
+ private enqueueOperation;
326
377
  private runIfIdle;
327
378
  private runOperation;
328
379
  private resolveEvent;
@@ -372,9 +423,11 @@ interface LastStateHasChangedDateInterface {
372
423
  getLastStateHasChangedDate(): Date;
373
424
  }
374
425
 
426
+ /** @deprecated No longer used internally; will be removed in v4. */
375
427
  interface CallbackInterface {
376
428
  invoke(): MaybePromise<void>;
377
429
  }
430
+ /** @deprecated No longer used internally; will be removed in v4. */
378
431
  interface DispatcherInterface extends CallbackInterface {
379
432
  dispatch(event: EventInterface, args?: unknown[]): void;
380
433
  invoke(): Promise<void>;
@@ -416,19 +469,24 @@ declare class Timeout implements ConditionInterface {
416
469
  checkCondition(subject: unknown, context: Map<string, unknown>): boolean;
417
470
  }
418
471
 
419
- declare class AndComposite<TSubject = unknown> implements ConditionInterface<TSubject> {
420
- private readonly conditions;
472
+ declare abstract class CompositeCondition<TSubject = unknown> implements ConditionInterface<TSubject> {
473
+ protected readonly conditions: ConditionInterface<TSubject>[];
474
+ private readonly joinWord;
475
+ constructor(joinWord: string, condition: ConditionInterface<TSubject>);
476
+ protected addCondition(condition: ConditionInterface<TSubject>): this;
477
+ getName(): string;
478
+ abstract checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
479
+ }
480
+
481
+ declare class AndComposite<TSubject = unknown> extends CompositeCondition<TSubject> {
421
482
  constructor(condition: ConditionInterface<TSubject>);
422
483
  addAnd(condition: ConditionInterface<TSubject>): this;
423
- getName(): string;
424
484
  checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
425
485
  }
426
486
 
427
- declare class OrComposite<TSubject = unknown> implements ConditionInterface<TSubject> {
428
- private readonly conditions;
487
+ declare class OrComposite<TSubject = unknown> extends CompositeCondition<TSubject> {
429
488
  constructor(condition: ConditionInterface<TSubject>);
430
489
  addOr(condition: ConditionInterface<TSubject>): this;
431
- getName(): string;
432
490
  checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
433
491
  }
434
492
 
@@ -449,12 +507,18 @@ declare class Not<TSubject = unknown> implements ConditionInterface<TSubject> {
449
507
  declare class CallbackObserver implements Observer {
450
508
  private readonly callback;
451
509
  constructor(callback: (...args: unknown[]) => MaybePromise<void>);
452
- update(subject: ObservableSubject): MaybePromise<void>;
510
+ update(subject: ObservableSubject, args?: readonly unknown[]): MaybePromise<void>;
453
511
  }
454
512
 
455
513
  declare class StatefulStatusChanger<TSubject extends StatefulInterface> implements AfterTransitionObserver<TSubject> {
456
514
  private readonly subject;
457
- constructor(subject: TSubject);
515
+ /**
516
+ * @param subject Optional explicit subject to write to. When omitted
517
+ * (recommended), the observer writes to frame.subject — the subject of
518
+ * whichever machine fired the transition — so a single instance can be
519
+ * shared safely across every machine a Factory creates.
520
+ */
521
+ constructor(subject?: TSubject);
458
522
  notify(frame: TransitionFrame<TSubject>): void;
459
523
  }
460
524
 
@@ -466,6 +530,10 @@ declare class StatefulStatusChanger<TSubject extends StatefulInterface> implemen
466
530
  * top-level operation after the current operation completes. Other
467
531
  * after-observers registered after OnEnterObserver still see the original
468
532
  * frame, not the chained one.
533
+ *
534
+ * The chained event only fires if the machine is still in the entered state
535
+ * when the queue drains — states passed through transiently by automatic
536
+ * transitions do not fire onEnter.
469
537
  */
470
538
  declare class OnEnterObserver<TSubject = unknown> implements AfterTransitionObserver<TSubject> {
471
539
  static readonly DEFAULT_EVENT_NAME = "onEnter";
@@ -482,7 +550,14 @@ declare class TransitionLogger<TSubject = unknown> implements AfterTransitionObs
482
550
  }
483
551
 
484
552
  declare class ActiveTransitionFilter {
485
- static filter<TSubject = unknown>(transitions: Iterable<TransitionInterface<TSubject>>, subject: TSubject, context: Map<string, unknown>, event?: EventInterface): Promise<TransitionInterface<TSubject>[]>;
553
+ static filter<TSubject = unknown>(transitions: Iterable<TransitionInterface<TSubject>>, subject: TSubject, context: Map<string, unknown>, event?: EventInterface,
554
+ /**
555
+ * Optional wrapper run around each individual isActive() evaluation. The
556
+ * Statemachine passes its re-entrancy guard here so that every condition —
557
+ * not just the first — is evaluated with the guard active; without per-item
558
+ * wrapping a re-entrant condition on a later transition would deadlock.
559
+ */
560
+ wrap?: <T>(fn: () => T) => T): Promise<TransitionInterface<TSubject>[]>;
486
561
  }
487
562
 
488
563
  declare class FilterStateByEvent {
@@ -648,7 +723,7 @@ declare class ProcessFinalizedError extends FinitaError {
648
723
  constructor(processName: string);
649
724
  }
650
725
 
651
- type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidEventName" | "invalidConditionName" | "orphanState";
726
+ type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidStateName" | "invalidEventName" | "invalidConditionName" | "orphanState";
652
727
  declare class GraphValidationError extends FinitaError {
653
728
  readonly code: GraphValidationCode;
654
729
  readonly details: Readonly<Record<string, unknown>>;
@@ -661,6 +736,8 @@ interface DuplicateTransitionConflict {
661
736
  eventName: string | null;
662
737
  existingConditionName: string | null;
663
738
  newConditionName: string | null;
739
+ existingWeight?: number;
740
+ newWeight?: number;
664
741
  }
665
742
  declare class DuplicateTransitionError extends FinitaError {
666
743
  readonly code = "duplicateTransition";
@@ -704,9 +781,14 @@ declare class AmbiguousTransitionError extends FinitaError {
704
781
 
705
782
  declare class AutomaticTransitionCycleError extends FinitaError {
706
783
  readonly code = "automaticTransitionCycle";
707
- readonly targetStateName: string;
708
- readonly visitedStateNames: readonly string[];
709
- constructor(targetStateName: string, visitedStateNames: Iterable<string>);
784
+ readonly stateName: string;
785
+ readonly hopLimit: number;
786
+ constructor(stateName: string, hopLimit: number);
787
+ }
788
+
789
+ declare class ReentrancyError extends FinitaError {
790
+ readonly code = "reentrancy";
791
+ constructor(operation: string);
710
792
  }
711
793
 
712
- export { AbstractNamedProcessDetector, ActiveTransitionFilter, type AddStateOptions, type AddTransitionOptions, type AfterTransitionObserver, AmbiguousTransitionError, AndComposite, AutomaticTransitionCycleError, type BeforeTransitionObserver, type BuildOptions, CallbackCondition, type CallbackInterface, CallbackObserver, type ConditionCallbackFn, type ConditionInterface, Contradiction, type DispatcherInterface, type DotOptions, DuplicateStateError, type DuplicateTransitionConflict, DuplicateTransitionError, type EnqueueContext, Event, type EventInterface, Factory, type FactoryInterface, FilterStateByEvent, FilterStateByFinalState, FilterStateByTransition, FilterTransitionByEvent, FinitaError, type Graph, GraphBuilder, type GraphEdge, type GraphNode, type GraphValidationCode, GraphValidationError, InvalidSubjectError, type LastStateHasChangedDateInterface, type LockAdapterInterface, LockAdapterMutex, LockCanNotBeAcquiredError, type LoggerInterface, type MaybePromise, type MermaidOptions, type Metadata, MutexFactory, type MutexFactoryInterface, type MutexInterface, type Named, Not, NullMutex, type ObservableSubject, type Observer, OnEnterObserver, OneOrNoneActiveTransition, OrComposite, Process, ProcessBuilder, type ProcessDetectorInterface, ProcessFinalizedError, type ProcessInterface, ProcessNotFoundError, type ProposedTransitionFrame, ScoreTransition, SingleProcessDetector, State, type StateCollectionInterface, StateEventNotFoundError, type StateInterface, type StateNameDetectorInterface, StateNotFoundError, type StatefulInterface, StatefulStateNameDetector, StatefulStatusChanger, Statemachine, type StatemachineInterface, type StatemachineOptions, type StringConverter, Tautology, Timeout, Transition, type TransitionFrame, type TransitionInterface, TransitionLogger, type TransitionSelectorInterface, WeightTransition, type Weighted, WrongEventForStateError };
794
+ export { AbstractNamedProcessDetector, ActiveTransitionFilter, type AddStateOptions, type AddTransitionOptions, type AfterTransitionObserver, AmbiguousTransitionError, AndComposite, AutomaticTransitionCycleError, type BeforeTransitionObserver, type BuildOptions, CallbackCondition, type CallbackInterface, CallbackObserver, type ConditionCallbackFn, type ConditionInterface, Contradiction, type DispatcherInterface, type DotOptions, DuplicateStateError, type DuplicateTransitionConflict, DuplicateTransitionError, type EnqueueContext, Event, type EventInterface, Factory, type FactoryInterface, FilterStateByEvent, FilterStateByFinalState, FilterStateByTransition, FilterTransitionByEvent, FinitaError, type Graph, GraphBuilder, type GraphEdge, type GraphNode, type GraphValidationCode, GraphValidationError, InvalidSubjectError, type LastStateHasChangedDateInterface, type LockAdapterInterface, LockAdapterMutex, LockCanNotBeAcquiredError, type LoggerInterface, type MaybePromise, type MermaidOptions, type Metadata, MutexFactory, type MutexFactoryInterface, type MutexInterface, type Named, Not, NullMutex, type ObservableSubject, type Observer, OnEnterObserver, OneOrNoneActiveTransition, OrComposite, Process, ProcessBuilder, type ProcessDetectorInterface, ProcessFinalizedError, type ProcessInterface, ProcessNotFoundError, type ProposedTransitionFrame, ReentrancyError, ScoreTransition, SingleProcessDetector, State, type StateCollectionInterface, StateEventNotFoundError, type StateInterface, type StateNameDetectorInterface, StateNotFoundError, type StatefulInterface, StatefulStateNameDetector, StatefulStatusChanger, Statemachine, type StatemachineInterface, type StatemachineOptions, type StringConverter, Tautology, Timeout, Transition, type TransitionFrame, type TransitionInterface, TransitionLogger, type TransitionSelectorInterface, WeightTransition, type Weighted, WrongEventForStateError };