@camcima/finita 4.0.0 → 4.2.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
@@ -53,6 +53,8 @@ declare class Event implements EventInterface {
53
53
  attach(observer: Observer): void;
54
54
  detach(observer: Observer): void;
55
55
  notify(args?: readonly unknown[]): Promise<void>;
56
+ /** Snapshot — detaching later does not change an already-returned list,
57
+ * and mutating it does not change the event's registrations. */
56
58
  getObservers(): Iterable<Observer>;
57
59
  getMetadata(): Record<string, unknown>;
58
60
  getMetadataValue(key: string): unknown;
@@ -287,6 +289,12 @@ interface StatemachineInterface<TSubject = unknown> {
287
289
  getProcess(): ProcessInterface;
288
290
  triggerEvent(name: string, context?: Map<string, unknown>): Promise<void>;
289
291
  checkTransitions(context?: Map<string, unknown>): Promise<void>;
292
+ /**
293
+ * Resolves once the operation queue is empty and the runner is idle,
294
+ * including operations chained via EnqueueContext.enqueue(). Resolves
295
+ * immediately if the machine is already idle.
296
+ */
297
+ whenIdle(): Promise<void>;
290
298
  attachBefore(observer: BeforeTransitionObserver<TSubject>): void;
291
299
  detachBefore(observer: BeforeTransitionObserver<TSubject>): void;
292
300
  getBeforeObservers(): Iterable<BeforeTransitionObserver<TSubject>>;
@@ -316,7 +324,16 @@ interface StatemachineOptions<TSubject = unknown> {
316
324
  initialStateName?: string;
317
325
  /** Defaults to OneOrNoneActiveTransition. */
318
326
  transitionSelector?: TransitionSelectorInterface<TSubject>;
319
- /** Defaults to NullMutex (no cross-process serialization). */
327
+ /**
328
+ * Defaults to NullMutex (no cross-process serialization).
329
+ *
330
+ * Must be exclusive to this machine — never share one MutexInterface
331
+ * instance between machines: the engine reads isAcquired() as "this
332
+ * machine holds the lock", so a shared instance silently disables mutual
333
+ * exclusion. To coordinate machines, share the underlying
334
+ * LockAdapterInterface (same resource name) and construct one mutex per
335
+ * machine, as MutexFactory does.
336
+ */
320
337
  mutex?: MutexInterface;
321
338
  /** When true, the engine releases the mutex at the end of each top-level operation. Defaults to true. */
322
339
  autoreleaseLock?: boolean;
@@ -330,6 +347,36 @@ interface StatemachineOptions<TSubject = unknown> {
330
347
  * Defaults to 100.
331
348
  */
332
349
  maxAutomaticHops?: number;
350
+ /**
351
+ * Called when an operation chained via EnqueueContext.enqueue() fails.
352
+ * Chained operations are not awaited by the caller whose transition
353
+ * enqueued them, so without this hook their errors are discarded.
354
+ * Exceptions thrown by the hook itself are swallowed — it must not be
355
+ * able to break the machine's drain loop.
356
+ */
357
+ onChainedOperationError?: (error: unknown, info: {
358
+ eventName: string;
359
+ }) => void;
360
+ /**
361
+ * Maximum number of operations that may wait in the queue (the running
362
+ * operation does not count). When the limit is reached, further
363
+ * triggerEvent/checkTransitions calls reject with
364
+ * QueueLimitExceededError, and EnqueueContext.enqueue() throws it into
365
+ * the enqueuing after-observer's error path. In that case the original
366
+ * caller's promise rejects even though its transition already
367
+ * committed — check the machine's state, not just the rejection, before
368
+ * retrying. Must be a positive integer when set. Defaults to Infinity
369
+ * (unbounded, the previous behavior).
370
+ */
371
+ maxQueueLength?: number;
372
+ /**
373
+ * Diagnostic hook called whenever the automatic post-operation lock
374
+ * release throws — including when the operation itself also failed, in
375
+ * which case the caller's rejection carries the operation error and the
376
+ * release error would otherwise be discarded. Does not change rejection
377
+ * behavior. Exceptions thrown by the hook itself are swallowed.
378
+ */
379
+ onReleaseError?: (error: unknown) => void;
333
380
  }
334
381
 
335
382
  declare class Statemachine<TSubject = unknown> implements StatemachineInterface<TSubject> {
@@ -341,11 +388,15 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
341
388
  private lastState;
342
389
  private autoreleaseLock;
343
390
  private readonly maxAutomaticHops;
391
+ private readonly maxQueueLength;
344
392
  private readonly queue;
345
393
  private running;
394
+ private idleWaiters;
346
395
  private inSyncCallback;
347
396
  private readonly beforeObservers;
348
397
  private readonly afterObservers;
398
+ private readonly onChainedOperationError?;
399
+ private readonly onReleaseError?;
349
400
  constructor(subject: TSubject, process: ProcessInterface, options?: StatemachineOptions<TSubject>);
350
401
  getCurrentState(): StateInterface;
351
402
  getLastState(): StateInterface | null;
@@ -353,17 +404,40 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
353
404
  getProcess(): ProcessInterface;
354
405
  attachBefore(observer: BeforeTransitionObserver<TSubject>): void;
355
406
  detachBefore(observer: BeforeTransitionObserver<TSubject>): void;
407
+ /** Snapshot — detaching later does not change an already-returned list,
408
+ * and mutating it does not change the machine's registrations. */
356
409
  getBeforeObservers(): Iterable<BeforeTransitionObserver<TSubject>>;
357
410
  attachAfter(observer: AfterTransitionObserver<TSubject>): void;
358
411
  detachAfter(observer: AfterTransitionObserver<TSubject>): void;
412
+ /** Snapshot — see getBeforeObservers. */
359
413
  getAfterObservers(): Iterable<AfterTransitionObserver<TSubject>>;
360
414
  acquireLock(): Promise<boolean>;
415
+ /**
416
+ * Releases the mutex. A failed release — whether the mutex throws or
417
+ * returns false — is reported to the onReleaseError hook; it is not thrown,
418
+ * so manual lock management keeps its existing control flow. Inspect
419
+ * isLockAcquired() (or the hook) to learn whether the lock was actually
420
+ * freed.
421
+ */
361
422
  releaseLock(): Promise<void>;
362
423
  isLockAcquired(): boolean;
363
424
  isAutoreleaseLock(): boolean;
364
425
  setAutoreleaseLock(autorelease: boolean): void;
365
426
  triggerEvent(name: string, context?: Map<string, unknown>): Promise<void>;
366
427
  checkTransitions(context?: Map<string, unknown>): Promise<void>;
428
+ /**
429
+ * Resolves once the operation queue is empty and the runner is idle —
430
+ * i.e. every operation enqueued so far, including operations chained via
431
+ * EnqueueContext.enqueue(), has completed. Resolves immediately if the
432
+ * machine is already idle. Note this is a quiescence point, not a
433
+ * receipt: work scheduled later (e.g. from a timer) starts a new drain.
434
+ *
435
+ * Like triggerEvent/checkTransitions, this may not be called from inside an
436
+ * observer or condition of the same machine: the machine cannot reach idle
437
+ * while the runner is blocked on that very callback, so awaiting it there
438
+ * always deadlocks.
439
+ */
440
+ whenIdle(): Promise<void>;
367
441
  /** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
368
442
  * the flag is cleared as soon as fn returns (before any promise it returned
369
443
  * is awaited), so concurrent external callers are never affected. This
@@ -376,6 +450,20 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
376
450
  private enqueueOperation;
377
451
  private runIfIdle;
378
452
  private runOperation;
453
+ /**
454
+ * Releases the mutex, normalizing its two failure modes into one result: a
455
+ * thrown error, and a false return — the failure signal MutexInterface /
456
+ * LockAdapterInterface define (a PostgreSQL advisory unlock that returns
457
+ * false, a Redis DEL that removed nothing). A false return means the lock
458
+ * may still be held, so it must never be mistaken for a successful release.
459
+ *
460
+ * Every failure is surfaced through the diagnostic hook — when the
461
+ * operation also failed, the rejection carries the operation error and this
462
+ * hook is the only place the release error appears.
463
+ *
464
+ * @returns null on success, or the failure wrapped for the caller to raise.
465
+ */
466
+ private releaseMutex;
379
467
  private resolveEvent;
380
468
  /**
381
469
  * Drive transitions starting from the current state, following automatic
@@ -423,11 +511,11 @@ interface LastStateHasChangedDateInterface {
423
511
  getLastStateHasChangedDate(): Date;
424
512
  }
425
513
 
426
- /** @deprecated No longer used internally; will be removed in v4. */
514
+ /** @deprecated No longer used internally; will be removed in v5. */
427
515
  interface CallbackInterface {
428
516
  invoke(): MaybePromise<void>;
429
517
  }
430
- /** @deprecated No longer used internally; will be removed in v4. */
518
+ /** @deprecated No longer used internally; will be removed in v5. */
431
519
  interface DispatcherInterface extends CallbackInterface {
432
520
  dispatch(event: EventInterface, args?: unknown[]): void;
433
521
  invoke(): Promise<void>;
@@ -498,11 +586,11 @@ declare class Not<TSubject = unknown> implements ConditionInterface<TSubject> {
498
586
  }
499
587
 
500
588
  /**
501
- * Legacy Observer for Event observers (commands attached to specific events).
589
+ * Observer for Event observers (commands attached to specific events).
502
590
  *
503
- * In v3 this is no longer used as a Statemachine observer. To run a
504
- * callback after every transition, implement AfterTransitionObserver
505
- * directly or compose a small wrapper.
591
+ * This is not a Statemachine observer. To run a callback after every
592
+ * transition, implement AfterTransitionObserver directly or compose a small
593
+ * wrapper.
506
594
  */
507
595
  declare class CallbackObserver implements Observer {
508
596
  private readonly callback;
@@ -609,7 +697,15 @@ declare class LockAdapterMutex implements MutexInterface {
609
697
  private readonly lockAdapter;
610
698
  private readonly resourceName;
611
699
  private acquired;
700
+ private pendingAcquire;
612
701
  constructor(lockAdapter: LockAdapterInterface, resourceName: string);
702
+ /**
703
+ * Overlapping calls share one underlying acquire: the `acquired` flag is
704
+ * only set after the adapter resolves, so without this both callers would
705
+ * pass the check and acquire twice on a non-idempotent adapter (database
706
+ * advisory locks, redis SET NX). The pending promise is cleared once it
707
+ * settles, so a failed acquire can still be retried.
708
+ */
613
709
  acquireLock(): Promise<boolean>;
614
710
  releaseLock(): Promise<boolean>;
615
711
  isAcquired(): boolean;
@@ -624,6 +720,15 @@ declare class MutexFactory<TSubject = unknown> implements MutexFactoryInterface<
624
720
  createMutex(subject: TSubject): MutexInterface;
625
721
  }
626
722
 
723
+ /**
724
+ * Engine options applied to every machine the factory creates.
725
+ *
726
+ * `initialStateName`, `mutex` and `transitionSelector` are excluded: the
727
+ * factory derives them per subject from the state-name detector, the mutex
728
+ * factory and setTransitionSelector, so a template value could only
729
+ * contradict them.
730
+ */
731
+ type FactoryStatemachineOptions<TSubject = unknown> = Omit<StatemachineOptions<TSubject>, "initialStateName" | "mutex" | "transitionSelector">;
627
732
  declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject> {
628
733
  private readonly processDetector;
629
734
  private readonly stateNameDetector;
@@ -631,7 +736,15 @@ declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject>
631
736
  private readonly afterObservers;
632
737
  private transitionSelector;
633
738
  private mutexFactory;
634
- constructor(processDetector: ProcessDetectorInterface<TSubject>, stateNameDetector?: StateNameDetectorInterface<TSubject> | null);
739
+ private readonly options;
740
+ /**
741
+ * @param options Engine options applied to every machine this factory
742
+ * creates — back-pressure (maxQueueLength), the automatic-hop bound, lock
743
+ * autorelease, and the onChainedOperationError / onReleaseError diagnostic
744
+ * sinks. Without them, factory-created machines would silently run on
745
+ * defaults, which is precisely where those sinks matter most.
746
+ */
747
+ constructor(processDetector: ProcessDetectorInterface<TSubject>, stateNameDetector?: StateNameDetectorInterface<TSubject> | null, options?: FactoryStatemachineOptions<TSubject>);
635
748
  setMutexFactory(factory: MutexFactoryInterface<TSubject> | null): void;
636
749
  setTransitionSelector(selector: TransitionSelectorInterface<TSubject>): void;
637
750
  attachBeforeObserver(observer: BeforeTransitionObserver<TSubject>): void;
@@ -674,11 +787,12 @@ interface Graph {
674
787
  nodes: GraphNode[];
675
788
  edges: GraphEdge[];
676
789
  }
790
+ type GraphDirection = "TB" | "BT" | "LR" | "RL";
677
791
  interface DotOptions {
678
- rankdir?: string;
792
+ rankdir?: GraphDirection;
679
793
  }
680
794
  interface MermaidOptions {
681
- direction?: string;
795
+ direction?: GraphDirection;
682
796
  }
683
797
  declare class GraphBuilder {
684
798
  private readonly nodes;
@@ -711,6 +825,20 @@ declare class LockCanNotBeAcquiredError extends FinitaError {
711
825
  constructor(message?: string);
712
826
  }
713
827
 
828
+ /**
829
+ * The mutex reported a failed release by returning false, as
830
+ * LockAdapterInterface specifies (e.g. a PostgreSQL advisory unlock that
831
+ * returns false, or a Redis DEL that removed nothing).
832
+ *
833
+ * The lock must be assumed to still be held: the engine surfaces this so a
834
+ * failed release can never be mistaken for a successful one, which would let
835
+ * every later operation piggyback on — and never release — a stuck lock.
836
+ */
837
+ declare class LockCanNotBeReleasedError extends FinitaError {
838
+ readonly code = "lockCanNotBeReleased";
839
+ constructor(message?: string);
840
+ }
841
+
714
842
  declare class DuplicateStateError extends FinitaError {
715
843
  readonly code = "duplicateState";
716
844
  readonly stateName: string;
@@ -723,7 +851,7 @@ declare class ProcessFinalizedError extends FinitaError {
723
851
  constructor(processName: string);
724
852
  }
725
853
 
726
- type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidStateName" | "invalidEventName" | "invalidConditionName" | "orphanState";
854
+ type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidStateName" | "invalidEventName" | "invalidConditionName" | "invalidTransitionWeight" | "orphanState";
727
855
  declare class GraphValidationError extends FinitaError {
728
856
  readonly code: GraphValidationCode;
729
857
  readonly details: Readonly<Record<string, unknown>>;
@@ -773,10 +901,19 @@ declare class InvalidSubjectError extends FinitaError {
773
901
  constructor(expectedInterface: string, missingMembers: Iterable<string>);
774
902
  }
775
903
 
904
+ /** One of the simultaneously-active transitions that caused the ambiguity. */
905
+ interface AmbiguousTransitionCandidate {
906
+ targetStateName: string;
907
+ eventName: string | null;
908
+ conditionName: string | null;
909
+ weight: number;
910
+ }
776
911
  declare class AmbiguousTransitionError extends FinitaError {
777
912
  readonly code = "ambiguousTransition";
778
913
  readonly activeCount: number;
779
- constructor(activeCount: number);
914
+ /** The competing transitions — what you need to resolve the ambiguity. */
915
+ readonly candidates: readonly Readonly<AmbiguousTransitionCandidate>[];
916
+ constructor(activeCount: number, candidates?: Iterable<AmbiguousTransitionCandidate>);
780
917
  }
781
918
 
782
919
  declare class AutomaticTransitionCycleError extends FinitaError {
@@ -791,4 +928,9 @@ declare class ReentrancyError extends FinitaError {
791
928
  constructor(operation: string);
792
929
  }
793
930
 
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 };
931
+ declare class QueueLimitExceededError extends FinitaError {
932
+ readonly code = "queueLimitExceeded";
933
+ constructor(limit: number, eventName: string | null);
934
+ }
935
+
936
+ export { AbstractNamedProcessDetector, ActiveTransitionFilter, type AddStateOptions, type AddTransitionOptions, type AfterTransitionObserver, type AmbiguousTransitionCandidate, 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, type FactoryStatemachineOptions, FilterStateByEvent, FilterStateByFinalState, FilterStateByTransition, FilterTransitionByEvent, FinitaError, type Graph, GraphBuilder, type GraphDirection, type GraphEdge, type GraphNode, type GraphValidationCode, GraphValidationError, InvalidSubjectError, type LastStateHasChangedDateInterface, type LockAdapterInterface, LockAdapterMutex, LockCanNotBeAcquiredError, LockCanNotBeReleasedError, 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, QueueLimitExceededError, 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
@@ -53,6 +53,8 @@ declare class Event implements EventInterface {
53
53
  attach(observer: Observer): void;
54
54
  detach(observer: Observer): void;
55
55
  notify(args?: readonly unknown[]): Promise<void>;
56
+ /** Snapshot — detaching later does not change an already-returned list,
57
+ * and mutating it does not change the event's registrations. */
56
58
  getObservers(): Iterable<Observer>;
57
59
  getMetadata(): Record<string, unknown>;
58
60
  getMetadataValue(key: string): unknown;
@@ -287,6 +289,12 @@ interface StatemachineInterface<TSubject = unknown> {
287
289
  getProcess(): ProcessInterface;
288
290
  triggerEvent(name: string, context?: Map<string, unknown>): Promise<void>;
289
291
  checkTransitions(context?: Map<string, unknown>): Promise<void>;
292
+ /**
293
+ * Resolves once the operation queue is empty and the runner is idle,
294
+ * including operations chained via EnqueueContext.enqueue(). Resolves
295
+ * immediately if the machine is already idle.
296
+ */
297
+ whenIdle(): Promise<void>;
290
298
  attachBefore(observer: BeforeTransitionObserver<TSubject>): void;
291
299
  detachBefore(observer: BeforeTransitionObserver<TSubject>): void;
292
300
  getBeforeObservers(): Iterable<BeforeTransitionObserver<TSubject>>;
@@ -316,7 +324,16 @@ interface StatemachineOptions<TSubject = unknown> {
316
324
  initialStateName?: string;
317
325
  /** Defaults to OneOrNoneActiveTransition. */
318
326
  transitionSelector?: TransitionSelectorInterface<TSubject>;
319
- /** Defaults to NullMutex (no cross-process serialization). */
327
+ /**
328
+ * Defaults to NullMutex (no cross-process serialization).
329
+ *
330
+ * Must be exclusive to this machine — never share one MutexInterface
331
+ * instance between machines: the engine reads isAcquired() as "this
332
+ * machine holds the lock", so a shared instance silently disables mutual
333
+ * exclusion. To coordinate machines, share the underlying
334
+ * LockAdapterInterface (same resource name) and construct one mutex per
335
+ * machine, as MutexFactory does.
336
+ */
320
337
  mutex?: MutexInterface;
321
338
  /** When true, the engine releases the mutex at the end of each top-level operation. Defaults to true. */
322
339
  autoreleaseLock?: boolean;
@@ -330,6 +347,36 @@ interface StatemachineOptions<TSubject = unknown> {
330
347
  * Defaults to 100.
331
348
  */
332
349
  maxAutomaticHops?: number;
350
+ /**
351
+ * Called when an operation chained via EnqueueContext.enqueue() fails.
352
+ * Chained operations are not awaited by the caller whose transition
353
+ * enqueued them, so without this hook their errors are discarded.
354
+ * Exceptions thrown by the hook itself are swallowed — it must not be
355
+ * able to break the machine's drain loop.
356
+ */
357
+ onChainedOperationError?: (error: unknown, info: {
358
+ eventName: string;
359
+ }) => void;
360
+ /**
361
+ * Maximum number of operations that may wait in the queue (the running
362
+ * operation does not count). When the limit is reached, further
363
+ * triggerEvent/checkTransitions calls reject with
364
+ * QueueLimitExceededError, and EnqueueContext.enqueue() throws it into
365
+ * the enqueuing after-observer's error path. In that case the original
366
+ * caller's promise rejects even though its transition already
367
+ * committed — check the machine's state, not just the rejection, before
368
+ * retrying. Must be a positive integer when set. Defaults to Infinity
369
+ * (unbounded, the previous behavior).
370
+ */
371
+ maxQueueLength?: number;
372
+ /**
373
+ * Diagnostic hook called whenever the automatic post-operation lock
374
+ * release throws — including when the operation itself also failed, in
375
+ * which case the caller's rejection carries the operation error and the
376
+ * release error would otherwise be discarded. Does not change rejection
377
+ * behavior. Exceptions thrown by the hook itself are swallowed.
378
+ */
379
+ onReleaseError?: (error: unknown) => void;
333
380
  }
334
381
 
335
382
  declare class Statemachine<TSubject = unknown> implements StatemachineInterface<TSubject> {
@@ -341,11 +388,15 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
341
388
  private lastState;
342
389
  private autoreleaseLock;
343
390
  private readonly maxAutomaticHops;
391
+ private readonly maxQueueLength;
344
392
  private readonly queue;
345
393
  private running;
394
+ private idleWaiters;
346
395
  private inSyncCallback;
347
396
  private readonly beforeObservers;
348
397
  private readonly afterObservers;
398
+ private readonly onChainedOperationError?;
399
+ private readonly onReleaseError?;
349
400
  constructor(subject: TSubject, process: ProcessInterface, options?: StatemachineOptions<TSubject>);
350
401
  getCurrentState(): StateInterface;
351
402
  getLastState(): StateInterface | null;
@@ -353,17 +404,40 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
353
404
  getProcess(): ProcessInterface;
354
405
  attachBefore(observer: BeforeTransitionObserver<TSubject>): void;
355
406
  detachBefore(observer: BeforeTransitionObserver<TSubject>): void;
407
+ /** Snapshot — detaching later does not change an already-returned list,
408
+ * and mutating it does not change the machine's registrations. */
356
409
  getBeforeObservers(): Iterable<BeforeTransitionObserver<TSubject>>;
357
410
  attachAfter(observer: AfterTransitionObserver<TSubject>): void;
358
411
  detachAfter(observer: AfterTransitionObserver<TSubject>): void;
412
+ /** Snapshot — see getBeforeObservers. */
359
413
  getAfterObservers(): Iterable<AfterTransitionObserver<TSubject>>;
360
414
  acquireLock(): Promise<boolean>;
415
+ /**
416
+ * Releases the mutex. A failed release — whether the mutex throws or
417
+ * returns false — is reported to the onReleaseError hook; it is not thrown,
418
+ * so manual lock management keeps its existing control flow. Inspect
419
+ * isLockAcquired() (or the hook) to learn whether the lock was actually
420
+ * freed.
421
+ */
361
422
  releaseLock(): Promise<void>;
362
423
  isLockAcquired(): boolean;
363
424
  isAutoreleaseLock(): boolean;
364
425
  setAutoreleaseLock(autorelease: boolean): void;
365
426
  triggerEvent(name: string, context?: Map<string, unknown>): Promise<void>;
366
427
  checkTransitions(context?: Map<string, unknown>): Promise<void>;
428
+ /**
429
+ * Resolves once the operation queue is empty and the runner is idle —
430
+ * i.e. every operation enqueued so far, including operations chained via
431
+ * EnqueueContext.enqueue(), has completed. Resolves immediately if the
432
+ * machine is already idle. Note this is a quiescence point, not a
433
+ * receipt: work scheduled later (e.g. from a timer) starts a new drain.
434
+ *
435
+ * Like triggerEvent/checkTransitions, this may not be called from inside an
436
+ * observer or condition of the same machine: the machine cannot reach idle
437
+ * while the runner is blocked on that very callback, so awaiting it there
438
+ * always deadlocks.
439
+ */
440
+ whenIdle(): Promise<void>;
367
441
  /** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
368
442
  * the flag is cleared as soon as fn returns (before any promise it returned
369
443
  * is awaited), so concurrent external callers are never affected. This
@@ -376,6 +450,20 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
376
450
  private enqueueOperation;
377
451
  private runIfIdle;
378
452
  private runOperation;
453
+ /**
454
+ * Releases the mutex, normalizing its two failure modes into one result: a
455
+ * thrown error, and a false return — the failure signal MutexInterface /
456
+ * LockAdapterInterface define (a PostgreSQL advisory unlock that returns
457
+ * false, a Redis DEL that removed nothing). A false return means the lock
458
+ * may still be held, so it must never be mistaken for a successful release.
459
+ *
460
+ * Every failure is surfaced through the diagnostic hook — when the
461
+ * operation also failed, the rejection carries the operation error and this
462
+ * hook is the only place the release error appears.
463
+ *
464
+ * @returns null on success, or the failure wrapped for the caller to raise.
465
+ */
466
+ private releaseMutex;
379
467
  private resolveEvent;
380
468
  /**
381
469
  * Drive transitions starting from the current state, following automatic
@@ -423,11 +511,11 @@ interface LastStateHasChangedDateInterface {
423
511
  getLastStateHasChangedDate(): Date;
424
512
  }
425
513
 
426
- /** @deprecated No longer used internally; will be removed in v4. */
514
+ /** @deprecated No longer used internally; will be removed in v5. */
427
515
  interface CallbackInterface {
428
516
  invoke(): MaybePromise<void>;
429
517
  }
430
- /** @deprecated No longer used internally; will be removed in v4. */
518
+ /** @deprecated No longer used internally; will be removed in v5. */
431
519
  interface DispatcherInterface extends CallbackInterface {
432
520
  dispatch(event: EventInterface, args?: unknown[]): void;
433
521
  invoke(): Promise<void>;
@@ -498,11 +586,11 @@ declare class Not<TSubject = unknown> implements ConditionInterface<TSubject> {
498
586
  }
499
587
 
500
588
  /**
501
- * Legacy Observer for Event observers (commands attached to specific events).
589
+ * Observer for Event observers (commands attached to specific events).
502
590
  *
503
- * In v3 this is no longer used as a Statemachine observer. To run a
504
- * callback after every transition, implement AfterTransitionObserver
505
- * directly or compose a small wrapper.
591
+ * This is not a Statemachine observer. To run a callback after every
592
+ * transition, implement AfterTransitionObserver directly or compose a small
593
+ * wrapper.
506
594
  */
507
595
  declare class CallbackObserver implements Observer {
508
596
  private readonly callback;
@@ -609,7 +697,15 @@ declare class LockAdapterMutex implements MutexInterface {
609
697
  private readonly lockAdapter;
610
698
  private readonly resourceName;
611
699
  private acquired;
700
+ private pendingAcquire;
612
701
  constructor(lockAdapter: LockAdapterInterface, resourceName: string);
702
+ /**
703
+ * Overlapping calls share one underlying acquire: the `acquired` flag is
704
+ * only set after the adapter resolves, so without this both callers would
705
+ * pass the check and acquire twice on a non-idempotent adapter (database
706
+ * advisory locks, redis SET NX). The pending promise is cleared once it
707
+ * settles, so a failed acquire can still be retried.
708
+ */
613
709
  acquireLock(): Promise<boolean>;
614
710
  releaseLock(): Promise<boolean>;
615
711
  isAcquired(): boolean;
@@ -624,6 +720,15 @@ declare class MutexFactory<TSubject = unknown> implements MutexFactoryInterface<
624
720
  createMutex(subject: TSubject): MutexInterface;
625
721
  }
626
722
 
723
+ /**
724
+ * Engine options applied to every machine the factory creates.
725
+ *
726
+ * `initialStateName`, `mutex` and `transitionSelector` are excluded: the
727
+ * factory derives them per subject from the state-name detector, the mutex
728
+ * factory and setTransitionSelector, so a template value could only
729
+ * contradict them.
730
+ */
731
+ type FactoryStatemachineOptions<TSubject = unknown> = Omit<StatemachineOptions<TSubject>, "initialStateName" | "mutex" | "transitionSelector">;
627
732
  declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject> {
628
733
  private readonly processDetector;
629
734
  private readonly stateNameDetector;
@@ -631,7 +736,15 @@ declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject>
631
736
  private readonly afterObservers;
632
737
  private transitionSelector;
633
738
  private mutexFactory;
634
- constructor(processDetector: ProcessDetectorInterface<TSubject>, stateNameDetector?: StateNameDetectorInterface<TSubject> | null);
739
+ private readonly options;
740
+ /**
741
+ * @param options Engine options applied to every machine this factory
742
+ * creates — back-pressure (maxQueueLength), the automatic-hop bound, lock
743
+ * autorelease, and the onChainedOperationError / onReleaseError diagnostic
744
+ * sinks. Without them, factory-created machines would silently run on
745
+ * defaults, which is precisely where those sinks matter most.
746
+ */
747
+ constructor(processDetector: ProcessDetectorInterface<TSubject>, stateNameDetector?: StateNameDetectorInterface<TSubject> | null, options?: FactoryStatemachineOptions<TSubject>);
635
748
  setMutexFactory(factory: MutexFactoryInterface<TSubject> | null): void;
636
749
  setTransitionSelector(selector: TransitionSelectorInterface<TSubject>): void;
637
750
  attachBeforeObserver(observer: BeforeTransitionObserver<TSubject>): void;
@@ -674,11 +787,12 @@ interface Graph {
674
787
  nodes: GraphNode[];
675
788
  edges: GraphEdge[];
676
789
  }
790
+ type GraphDirection = "TB" | "BT" | "LR" | "RL";
677
791
  interface DotOptions {
678
- rankdir?: string;
792
+ rankdir?: GraphDirection;
679
793
  }
680
794
  interface MermaidOptions {
681
- direction?: string;
795
+ direction?: GraphDirection;
682
796
  }
683
797
  declare class GraphBuilder {
684
798
  private readonly nodes;
@@ -711,6 +825,20 @@ declare class LockCanNotBeAcquiredError extends FinitaError {
711
825
  constructor(message?: string);
712
826
  }
713
827
 
828
+ /**
829
+ * The mutex reported a failed release by returning false, as
830
+ * LockAdapterInterface specifies (e.g. a PostgreSQL advisory unlock that
831
+ * returns false, or a Redis DEL that removed nothing).
832
+ *
833
+ * The lock must be assumed to still be held: the engine surfaces this so a
834
+ * failed release can never be mistaken for a successful one, which would let
835
+ * every later operation piggyback on — and never release — a stuck lock.
836
+ */
837
+ declare class LockCanNotBeReleasedError extends FinitaError {
838
+ readonly code = "lockCanNotBeReleased";
839
+ constructor(message?: string);
840
+ }
841
+
714
842
  declare class DuplicateStateError extends FinitaError {
715
843
  readonly code = "duplicateState";
716
844
  readonly stateName: string;
@@ -723,7 +851,7 @@ declare class ProcessFinalizedError extends FinitaError {
723
851
  constructor(processName: string);
724
852
  }
725
853
 
726
- type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidStateName" | "invalidEventName" | "invalidConditionName" | "orphanState";
854
+ type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidStateName" | "invalidEventName" | "invalidConditionName" | "invalidTransitionWeight" | "orphanState";
727
855
  declare class GraphValidationError extends FinitaError {
728
856
  readonly code: GraphValidationCode;
729
857
  readonly details: Readonly<Record<string, unknown>>;
@@ -773,10 +901,19 @@ declare class InvalidSubjectError extends FinitaError {
773
901
  constructor(expectedInterface: string, missingMembers: Iterable<string>);
774
902
  }
775
903
 
904
+ /** One of the simultaneously-active transitions that caused the ambiguity. */
905
+ interface AmbiguousTransitionCandidate {
906
+ targetStateName: string;
907
+ eventName: string | null;
908
+ conditionName: string | null;
909
+ weight: number;
910
+ }
776
911
  declare class AmbiguousTransitionError extends FinitaError {
777
912
  readonly code = "ambiguousTransition";
778
913
  readonly activeCount: number;
779
- constructor(activeCount: number);
914
+ /** The competing transitions — what you need to resolve the ambiguity. */
915
+ readonly candidates: readonly Readonly<AmbiguousTransitionCandidate>[];
916
+ constructor(activeCount: number, candidates?: Iterable<AmbiguousTransitionCandidate>);
780
917
  }
781
918
 
782
919
  declare class AutomaticTransitionCycleError extends FinitaError {
@@ -791,4 +928,9 @@ declare class ReentrancyError extends FinitaError {
791
928
  constructor(operation: string);
792
929
  }
793
930
 
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 };
931
+ declare class QueueLimitExceededError extends FinitaError {
932
+ readonly code = "queueLimitExceeded";
933
+ constructor(limit: number, eventName: string | null);
934
+ }
935
+
936
+ export { AbstractNamedProcessDetector, ActiveTransitionFilter, type AddStateOptions, type AddTransitionOptions, type AfterTransitionObserver, type AmbiguousTransitionCandidate, 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, type FactoryStatemachineOptions, FilterStateByEvent, FilterStateByFinalState, FilterStateByTransition, FilterTransitionByEvent, FinitaError, type Graph, GraphBuilder, type GraphDirection, type GraphEdge, type GraphNode, type GraphValidationCode, GraphValidationError, InvalidSubjectError, type LastStateHasChangedDateInterface, type LockAdapterInterface, LockAdapterMutex, LockCanNotBeAcquiredError, LockCanNotBeReleasedError, 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, QueueLimitExceededError, 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 };