@camcima/finita 4.1.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;
@@ -402,11 +404,21 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
402
404
  getProcess(): ProcessInterface;
403
405
  attachBefore(observer: BeforeTransitionObserver<TSubject>): void;
404
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. */
405
409
  getBeforeObservers(): Iterable<BeforeTransitionObserver<TSubject>>;
406
410
  attachAfter(observer: AfterTransitionObserver<TSubject>): void;
407
411
  detachAfter(observer: AfterTransitionObserver<TSubject>): void;
412
+ /** Snapshot — see getBeforeObservers. */
408
413
  getAfterObservers(): Iterable<AfterTransitionObserver<TSubject>>;
409
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
+ */
410
422
  releaseLock(): Promise<void>;
411
423
  isLockAcquired(): boolean;
412
424
  isAutoreleaseLock(): boolean;
@@ -419,6 +431,11 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
419
431
  * EnqueueContext.enqueue(), has completed. Resolves immediately if the
420
432
  * machine is already idle. Note this is a quiescence point, not a
421
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.
422
439
  */
423
440
  whenIdle(): Promise<void>;
424
441
  /** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
@@ -433,6 +450,20 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
433
450
  private enqueueOperation;
434
451
  private runIfIdle;
435
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;
436
467
  private resolveEvent;
437
468
  /**
438
469
  * Drive transitions starting from the current state, following automatic
@@ -480,11 +511,11 @@ interface LastStateHasChangedDateInterface {
480
511
  getLastStateHasChangedDate(): Date;
481
512
  }
482
513
 
483
- /** @deprecated No longer used internally; will be removed in v4. */
514
+ /** @deprecated No longer used internally; will be removed in v5. */
484
515
  interface CallbackInterface {
485
516
  invoke(): MaybePromise<void>;
486
517
  }
487
- /** @deprecated No longer used internally; will be removed in v4. */
518
+ /** @deprecated No longer used internally; will be removed in v5. */
488
519
  interface DispatcherInterface extends CallbackInterface {
489
520
  dispatch(event: EventInterface, args?: unknown[]): void;
490
521
  invoke(): Promise<void>;
@@ -555,11 +586,11 @@ declare class Not<TSubject = unknown> implements ConditionInterface<TSubject> {
555
586
  }
556
587
 
557
588
  /**
558
- * Legacy Observer for Event observers (commands attached to specific events).
589
+ * Observer for Event observers (commands attached to specific events).
559
590
  *
560
- * In v3 this is no longer used as a Statemachine observer. To run a
561
- * callback after every transition, implement AfterTransitionObserver
562
- * 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.
563
594
  */
564
595
  declare class CallbackObserver implements Observer {
565
596
  private readonly callback;
@@ -666,7 +697,15 @@ declare class LockAdapterMutex implements MutexInterface {
666
697
  private readonly lockAdapter;
667
698
  private readonly resourceName;
668
699
  private acquired;
700
+ private pendingAcquire;
669
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
+ */
670
709
  acquireLock(): Promise<boolean>;
671
710
  releaseLock(): Promise<boolean>;
672
711
  isAcquired(): boolean;
@@ -681,6 +720,15 @@ declare class MutexFactory<TSubject = unknown> implements MutexFactoryInterface<
681
720
  createMutex(subject: TSubject): MutexInterface;
682
721
  }
683
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">;
684
732
  declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject> {
685
733
  private readonly processDetector;
686
734
  private readonly stateNameDetector;
@@ -688,7 +736,15 @@ declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject>
688
736
  private readonly afterObservers;
689
737
  private transitionSelector;
690
738
  private mutexFactory;
691
- 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>);
692
748
  setMutexFactory(factory: MutexFactoryInterface<TSubject> | null): void;
693
749
  setTransitionSelector(selector: TransitionSelectorInterface<TSubject>): void;
694
750
  attachBeforeObserver(observer: BeforeTransitionObserver<TSubject>): void;
@@ -769,6 +825,20 @@ declare class LockCanNotBeAcquiredError extends FinitaError {
769
825
  constructor(message?: string);
770
826
  }
771
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
+
772
842
  declare class DuplicateStateError extends FinitaError {
773
843
  readonly code = "duplicateState";
774
844
  readonly stateName: string;
@@ -831,10 +901,19 @@ declare class InvalidSubjectError extends FinitaError {
831
901
  constructor(expectedInterface: string, missingMembers: Iterable<string>);
832
902
  }
833
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
+ }
834
911
  declare class AmbiguousTransitionError extends FinitaError {
835
912
  readonly code = "ambiguousTransition";
836
913
  readonly activeCount: number;
837
- 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>);
838
917
  }
839
918
 
840
919
  declare class AutomaticTransitionCycleError extends FinitaError {
@@ -854,4 +933,4 @@ declare class QueueLimitExceededError extends FinitaError {
854
933
  constructor(limit: number, eventName: string | null);
855
934
  }
856
935
 
857
- 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 GraphDirection, 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, 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 };
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;
@@ -402,11 +404,21 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
402
404
  getProcess(): ProcessInterface;
403
405
  attachBefore(observer: BeforeTransitionObserver<TSubject>): void;
404
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. */
405
409
  getBeforeObservers(): Iterable<BeforeTransitionObserver<TSubject>>;
406
410
  attachAfter(observer: AfterTransitionObserver<TSubject>): void;
407
411
  detachAfter(observer: AfterTransitionObserver<TSubject>): void;
412
+ /** Snapshot — see getBeforeObservers. */
408
413
  getAfterObservers(): Iterable<AfterTransitionObserver<TSubject>>;
409
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
+ */
410
422
  releaseLock(): Promise<void>;
411
423
  isLockAcquired(): boolean;
412
424
  isAutoreleaseLock(): boolean;
@@ -419,6 +431,11 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
419
431
  * EnqueueContext.enqueue(), has completed. Resolves immediately if the
420
432
  * machine is already idle. Note this is a quiescence point, not a
421
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.
422
439
  */
423
440
  whenIdle(): Promise<void>;
424
441
  /** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
@@ -433,6 +450,20 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
433
450
  private enqueueOperation;
434
451
  private runIfIdle;
435
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;
436
467
  private resolveEvent;
437
468
  /**
438
469
  * Drive transitions starting from the current state, following automatic
@@ -480,11 +511,11 @@ interface LastStateHasChangedDateInterface {
480
511
  getLastStateHasChangedDate(): Date;
481
512
  }
482
513
 
483
- /** @deprecated No longer used internally; will be removed in v4. */
514
+ /** @deprecated No longer used internally; will be removed in v5. */
484
515
  interface CallbackInterface {
485
516
  invoke(): MaybePromise<void>;
486
517
  }
487
- /** @deprecated No longer used internally; will be removed in v4. */
518
+ /** @deprecated No longer used internally; will be removed in v5. */
488
519
  interface DispatcherInterface extends CallbackInterface {
489
520
  dispatch(event: EventInterface, args?: unknown[]): void;
490
521
  invoke(): Promise<void>;
@@ -555,11 +586,11 @@ declare class Not<TSubject = unknown> implements ConditionInterface<TSubject> {
555
586
  }
556
587
 
557
588
  /**
558
- * Legacy Observer for Event observers (commands attached to specific events).
589
+ * Observer for Event observers (commands attached to specific events).
559
590
  *
560
- * In v3 this is no longer used as a Statemachine observer. To run a
561
- * callback after every transition, implement AfterTransitionObserver
562
- * 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.
563
594
  */
564
595
  declare class CallbackObserver implements Observer {
565
596
  private readonly callback;
@@ -666,7 +697,15 @@ declare class LockAdapterMutex implements MutexInterface {
666
697
  private readonly lockAdapter;
667
698
  private readonly resourceName;
668
699
  private acquired;
700
+ private pendingAcquire;
669
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
+ */
670
709
  acquireLock(): Promise<boolean>;
671
710
  releaseLock(): Promise<boolean>;
672
711
  isAcquired(): boolean;
@@ -681,6 +720,15 @@ declare class MutexFactory<TSubject = unknown> implements MutexFactoryInterface<
681
720
  createMutex(subject: TSubject): MutexInterface;
682
721
  }
683
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">;
684
732
  declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject> {
685
733
  private readonly processDetector;
686
734
  private readonly stateNameDetector;
@@ -688,7 +736,15 @@ declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject>
688
736
  private readonly afterObservers;
689
737
  private transitionSelector;
690
738
  private mutexFactory;
691
- 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>);
692
748
  setMutexFactory(factory: MutexFactoryInterface<TSubject> | null): void;
693
749
  setTransitionSelector(selector: TransitionSelectorInterface<TSubject>): void;
694
750
  attachBeforeObserver(observer: BeforeTransitionObserver<TSubject>): void;
@@ -769,6 +825,20 @@ declare class LockCanNotBeAcquiredError extends FinitaError {
769
825
  constructor(message?: string);
770
826
  }
771
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
+
772
842
  declare class DuplicateStateError extends FinitaError {
773
843
  readonly code = "duplicateState";
774
844
  readonly stateName: string;
@@ -831,10 +901,19 @@ declare class InvalidSubjectError extends FinitaError {
831
901
  constructor(expectedInterface: string, missingMembers: Iterable<string>);
832
902
  }
833
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
+ }
834
911
  declare class AmbiguousTransitionError extends FinitaError {
835
912
  readonly code = "ambiguousTransition";
836
913
  readonly activeCount: number;
837
- 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>);
838
917
  }
839
918
 
840
919
  declare class AutomaticTransitionCycleError extends FinitaError {
@@ -854,4 +933,4 @@ declare class QueueLimitExceededError extends FinitaError {
854
933
  constructor(limit: number, eventName: string | null);
855
934
  }
856
935
 
857
- 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 GraphDirection, 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, 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 };
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.js CHANGED
@@ -31,8 +31,10 @@ var Event = class {
31
31
  await observer.update(this, args);
32
32
  }
33
33
  }
34
+ /** Snapshot — detaching later does not change an already-returned list,
35
+ * and mutating it does not change the event's registrations. */
34
36
  getObservers() {
35
- return this.observers;
37
+ return [...this.observers];
36
38
  }
37
39
  getMetadata() {
38
40
  return Object.fromEntries(this.metadata);
@@ -593,12 +595,30 @@ var ProcessBuilder = class _ProcessBuilder {
593
595
  var AmbiguousTransitionError = class extends FinitaError {
594
596
  code = "ambiguousTransition";
595
597
  activeCount;
596
- constructor(activeCount) {
597
- super(`More than one transition is active! (active count: ${activeCount})`);
598
+ /** The competing transitions — what you need to resolve the ambiguity. */
599
+ candidates;
600
+ constructor(activeCount, candidates = []) {
601
+ const list = Array.from(candidates, (c) => Object.freeze({ ...c }));
602
+ const detail = list.length > 0 ? ` Candidates: ${list.map(describeCandidate).join("; ")}.` : "";
603
+ super(
604
+ `More than one transition is active! (active count: ${activeCount})${detail}`
605
+ );
598
606
  this.name = "AmbiguousTransitionError";
599
607
  this.activeCount = activeCount;
608
+ this.candidates = Object.freeze(list);
600
609
  }
601
610
  };
611
+ function describeCandidate(candidate) {
612
+ const parts = [`-> "${candidate.targetStateName}"`];
613
+ parts.push(
614
+ candidate.eventName === null ? "on <automatic>" : `on event "${candidate.eventName}"`
615
+ );
616
+ if (candidate.conditionName !== null) {
617
+ parts.push(`if ${candidate.conditionName}`);
618
+ }
619
+ parts.push(`weight ${candidate.weight}`);
620
+ return parts.join(" ");
621
+ }
602
622
 
603
623
  // src/selector/OneOrNoneActiveTransition.ts
604
624
  var OneOrNoneActiveTransition = class {
@@ -610,7 +630,15 @@ var OneOrNoneActiveTransition = class {
610
630
  case 1:
611
631
  return arr[0];
612
632
  default:
613
- throw new AmbiguousTransitionError(arr.length);
633
+ throw new AmbiguousTransitionError(
634
+ arr.length,
635
+ arr.map((transition) => ({
636
+ targetStateName: transition.getTargetState().getName(),
637
+ eventName: transition.getEventName(),
638
+ conditionName: transition.getConditionName(),
639
+ weight: transition.getWeight()
640
+ }))
641
+ );
614
642
  }
615
643
  }
616
644
  };
@@ -687,6 +715,15 @@ var LockCanNotBeAcquiredError = class extends FinitaError {
687
715
  }
688
716
  };
689
717
 
718
+ // src/error/LockCanNotBeReleasedError.ts
719
+ var LockCanNotBeReleasedError = class extends FinitaError {
720
+ code = "lockCanNotBeReleased";
721
+ constructor(message = "Lock can not be released! releaseLock() returned false; the lock may still be held.") {
722
+ super(message);
723
+ this.name = "LockCanNotBeReleasedError";
724
+ }
725
+ };
726
+
690
727
  // src/error/AutomaticTransitionCycleError.ts
691
728
  var AutomaticTransitionCycleError = class extends FinitaError {
692
729
  code = "automaticTransitionCycle";
@@ -789,8 +826,10 @@ var Statemachine = class {
789
826
  const idx = this.beforeObservers.indexOf(observer);
790
827
  if (idx >= 0) this.beforeObservers.splice(idx, 1);
791
828
  }
829
+ /** Snapshot — detaching later does not change an already-returned list,
830
+ * and mutating it does not change the machine's registrations. */
792
831
  getBeforeObservers() {
793
- return this.beforeObservers;
832
+ return [...this.beforeObservers];
794
833
  }
795
834
  attachAfter(observer) {
796
835
  if (this.afterObservers.includes(observer)) return;
@@ -800,15 +839,23 @@ var Statemachine = class {
800
839
  const idx = this.afterObservers.indexOf(observer);
801
840
  if (idx >= 0) this.afterObservers.splice(idx, 1);
802
841
  }
842
+ /** Snapshot — see getBeforeObservers. */
803
843
  getAfterObservers() {
804
- return this.afterObservers;
844
+ return [...this.afterObservers];
805
845
  }
806
846
  // --- public locking ---
807
847
  async acquireLock() {
808
848
  return this.mutex.acquireLock();
809
849
  }
850
+ /**
851
+ * Releases the mutex. A failed release — whether the mutex throws or
852
+ * returns false — is reported to the onReleaseError hook; it is not thrown,
853
+ * so manual lock management keeps its existing control flow. Inspect
854
+ * isLockAcquired() (or the hook) to learn whether the lock was actually
855
+ * freed.
856
+ */
810
857
  async releaseLock() {
811
- await this.mutex.releaseLock();
858
+ await this.releaseMutex();
812
859
  }
813
860
  isLockAcquired() {
814
861
  return this.mutex.isAcquired();
@@ -838,8 +885,14 @@ var Statemachine = class {
838
885
  * EnqueueContext.enqueue(), has completed. Resolves immediately if the
839
886
  * machine is already idle. Note this is a quiescence point, not a
840
887
  * receipt: work scheduled later (e.g. from a timer) starts a new drain.
888
+ *
889
+ * Like triggerEvent/checkTransitions, this may not be called from inside an
890
+ * observer or condition of the same machine: the machine cannot reach idle
891
+ * while the runner is blocked on that very callback, so awaiting it there
892
+ * always deadlocks.
841
893
  */
842
894
  whenIdle() {
895
+ this.assertNotReentrant("whenIdle()");
843
896
  if (!this.running && this.queue.isEmpty()) {
844
897
  return Promise.resolve();
845
898
  }
@@ -918,15 +971,8 @@ var Statemachine = class {
918
971
  failure = { err };
919
972
  } finally {
920
973
  if (acquiredHere && this.autoreleaseLock) {
921
- try {
922
- await this.mutex.releaseLock();
923
- } catch (err) {
924
- try {
925
- this.onReleaseError?.(err);
926
- } catch {
927
- }
928
- if (!failure) failure = { err };
929
- }
974
+ const releaseFailure = await this.releaseMutex();
975
+ if (releaseFailure && !failure) failure = releaseFailure;
930
976
  }
931
977
  }
932
978
  if (failure) {
@@ -935,6 +981,36 @@ var Statemachine = class {
935
981
  op.resolve();
936
982
  }
937
983
  }
984
+ /**
985
+ * Releases the mutex, normalizing its two failure modes into one result: a
986
+ * thrown error, and a false return — the failure signal MutexInterface /
987
+ * LockAdapterInterface define (a PostgreSQL advisory unlock that returns
988
+ * false, a Redis DEL that removed nothing). A false return means the lock
989
+ * may still be held, so it must never be mistaken for a successful release.
990
+ *
991
+ * Every failure is surfaced through the diagnostic hook — when the
992
+ * operation also failed, the rejection carries the operation error and this
993
+ * hook is the only place the release error appears.
994
+ *
995
+ * @returns null on success, or the failure wrapped for the caller to raise.
996
+ */
997
+ async releaseMutex() {
998
+ let failure = null;
999
+ try {
1000
+ if (!await this.mutex.releaseLock()) {
1001
+ failure = { err: new LockCanNotBeReleasedError() };
1002
+ }
1003
+ } catch (err) {
1004
+ failure = { err };
1005
+ }
1006
+ if (failure) {
1007
+ try {
1008
+ this.onReleaseError?.(failure.err);
1009
+ } catch {
1010
+ }
1011
+ }
1012
+ return failure;
1013
+ }
938
1014
  resolveEvent(name) {
939
1015
  if (!this.currentState.hasEvent(name)) {
940
1016
  throw new WrongEventForStateError(this.currentState.getName(), name);
@@ -1408,15 +1484,31 @@ var LockAdapterMutex = class {
1408
1484
  lockAdapter;
1409
1485
  resourceName;
1410
1486
  acquired = false;
1487
+ pendingAcquire = null;
1411
1488
  constructor(lockAdapter, resourceName) {
1412
1489
  this.lockAdapter = lockAdapter;
1413
1490
  this.resourceName = resourceName;
1414
1491
  }
1492
+ /**
1493
+ * Overlapping calls share one underlying acquire: the `acquired` flag is
1494
+ * only set after the adapter resolves, so without this both callers would
1495
+ * pass the check and acquire twice on a non-idempotent adapter (database
1496
+ * advisory locks, redis SET NX). The pending promise is cleared once it
1497
+ * settles, so a failed acquire can still be retried.
1498
+ */
1415
1499
  async acquireLock() {
1416
- if (!this.acquired) {
1417
- this.acquired = await this.lockAdapter.acquireLock(this.resourceName);
1418
- }
1419
- return this.acquired;
1500
+ if (this.acquired) {
1501
+ return true;
1502
+ }
1503
+ this.pendingAcquire ??= (async () => {
1504
+ try {
1505
+ this.acquired = await this.lockAdapter.acquireLock(this.resourceName);
1506
+ return this.acquired;
1507
+ } finally {
1508
+ this.pendingAcquire = null;
1509
+ }
1510
+ })();
1511
+ return this.pendingAcquire;
1420
1512
  }
1421
1513
  async releaseLock() {
1422
1514
  if (this.acquired) {
@@ -1460,9 +1552,18 @@ var Factory = class {
1460
1552
  afterObservers = /* @__PURE__ */ new Set();
1461
1553
  transitionSelector = null;
1462
1554
  mutexFactory = null;
1463
- constructor(processDetector, stateNameDetector) {
1555
+ options;
1556
+ /**
1557
+ * @param options Engine options applied to every machine this factory
1558
+ * creates — back-pressure (maxQueueLength), the automatic-hop bound, lock
1559
+ * autorelease, and the onChainedOperationError / onReleaseError diagnostic
1560
+ * sinks. Without them, factory-created machines would silently run on
1561
+ * defaults, which is precisely where those sinks matter most.
1562
+ */
1563
+ constructor(processDetector, stateNameDetector, options = {}) {
1464
1564
  this.processDetector = processDetector;
1465
1565
  this.stateNameDetector = stateNameDetector ?? null;
1566
+ this.options = { ...options };
1466
1567
  }
1467
1568
  setMutexFactory(factory) {
1468
1569
  this.mutexFactory = factory;
@@ -1487,6 +1588,7 @@ var Factory = class {
1487
1588
  const stateName = this.stateNameDetector ? this.stateNameDetector.detectCurrentStateName(subject) : void 0;
1488
1589
  const mutex = this.mutexFactory ? await this.mutexFactory.createMutex(subject) : void 0;
1489
1590
  const sm = new Statemachine(subject, process, {
1591
+ ...this.options,
1490
1592
  initialStateName: stateName ?? void 0,
1491
1593
  transitionSelector: this.transitionSelector ?? void 0,
1492
1594
  mutex: mutex ?? void 0
@@ -1716,6 +1818,7 @@ export {
1716
1818
  InvalidSubjectError,
1717
1819
  LockAdapterMutex,
1718
1820
  LockCanNotBeAcquiredError,
1821
+ LockCanNotBeReleasedError,
1719
1822
  MutexFactory,
1720
1823
  Not,
1721
1824
  NullMutex,