@camcima/finita 4.1.0 → 4.3.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;
@@ -133,6 +135,8 @@ declare class State implements StateInterface {
133
135
  */
134
136
  _initTransitions(key: InternalConstructionKey, transitions: Iterable<TransitionInterface>): void;
135
137
  getName(): string;
138
+ /** Snapshot — the graph is shared by every machine built from the
139
+ * process, so callers must never receive the collection itself. */
136
140
  getTransitions(): Iterable<TransitionInterface>;
137
141
  getEventNames(): string[];
138
142
  hasEvent(name: string): boolean;
@@ -187,7 +191,9 @@ declare class ProcessBuilder<TSubject = unknown> {
187
191
  private findInitialStateName;
188
192
  private validateTransitionEndpoints;
189
193
  /** Transition identity: (fromState, eventName, toState). Used by both the
190
- * conflict check and the build-time dedup — keep them in lockstep. */
194
+ * conflict check and the build-time dedup — keep them in lockstep.
195
+ * Encoded as a JSON tuple, not a delimiter join: names may contain any
196
+ * character, so no delimiter can keep distinct tuples distinct. */
191
197
  private static transitionKey;
192
198
  private validateNoConflictingDuplicates;
193
199
  private collectEventNamesByState;
@@ -349,8 +355,11 @@ interface StatemachineOptions<TSubject = unknown> {
349
355
  * Called when an operation chained via EnqueueContext.enqueue() fails.
350
356
  * Chained operations are not awaited by the caller whose transition
351
357
  * enqueued them, so without this hook their errors are discarded.
352
- * Exceptions thrown by the hook itself are swallowed — it must not be
353
- * able to break the machine's drain loop.
358
+ * The hook may be async. It is not awaited, and both exceptions it throws
359
+ * and rejections of the promise it returns are swallowed — it must not be
360
+ * able to break the machine's drain loop or surface as an unhandled
361
+ * rejection. (The return type stays `void`, which already admits async
362
+ * functions, so callbacks returning incidental values keep compiling.)
354
363
  */
355
364
  onChainedOperationError?: (error: unknown, info: {
356
365
  eventName: string;
@@ -372,7 +381,8 @@ interface StatemachineOptions<TSubject = unknown> {
372
381
  * release throws — including when the operation itself also failed, in
373
382
  * which case the caller's rejection carries the operation error and the
374
383
  * release error would otherwise be discarded. Does not change rejection
375
- * behavior. Exceptions thrown by the hook itself are swallowed.
384
+ * behavior. The hook may be async; it is not awaited, and both exceptions
385
+ * it throws and rejections of the promise it returns are swallowed.
376
386
  */
377
387
  onReleaseError?: (error: unknown) => void;
378
388
  }
@@ -391,6 +401,8 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
391
401
  private running;
392
402
  private idleWaiters;
393
403
  private inSyncCallback;
404
+ /** Set when releasing a held lock fails; see LockOwnershipUncertainError. */
405
+ private ownershipUncertainty;
394
406
  private readonly beforeObservers;
395
407
  private readonly afterObservers;
396
408
  private readonly onChainedOperationError?;
@@ -402,11 +414,24 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
402
414
  getProcess(): ProcessInterface;
403
415
  attachBefore(observer: BeforeTransitionObserver<TSubject>): void;
404
416
  detachBefore(observer: BeforeTransitionObserver<TSubject>): void;
417
+ /** Snapshot — detaching later does not change an already-returned list,
418
+ * and mutating it does not change the machine's registrations. */
405
419
  getBeforeObservers(): Iterable<BeforeTransitionObserver<TSubject>>;
406
420
  attachAfter(observer: AfterTransitionObserver<TSubject>): void;
407
421
  detachAfter(observer: AfterTransitionObserver<TSubject>): void;
422
+ /** Snapshot — see getBeforeObservers. */
408
423
  getAfterObservers(): Iterable<AfterTransitionObserver<TSubject>>;
409
424
  acquireLock(): Promise<boolean>;
425
+ /**
426
+ * Releases the mutex. A failed release — whether the mutex throws or
427
+ * returns false — is reported to the onReleaseError hook; it is not thrown,
428
+ * so manual lock management keeps its existing control flow. Inspect
429
+ * isLockAcquired() (or the hook) to learn whether the lock was actually
430
+ * freed.
431
+ *
432
+ * A failed release of a held lock makes every later operation reject with
433
+ * LockOwnershipUncertainError; a successful call here is how to recover.
434
+ */
410
435
  releaseLock(): Promise<void>;
411
436
  isLockAcquired(): boolean;
412
437
  isAutoreleaseLock(): boolean;
@@ -419,6 +444,11 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
419
444
  * EnqueueContext.enqueue(), has completed. Resolves immediately if the
420
445
  * machine is already idle. Note this is a quiescence point, not a
421
446
  * receipt: work scheduled later (e.g. from a timer) starts a new drain.
447
+ *
448
+ * Like triggerEvent/checkTransitions, this may not be called from inside an
449
+ * observer or condition of the same machine: the machine cannot reach idle
450
+ * while the runner is blocked on that very callback, so awaiting it there
451
+ * always deadlocks.
422
452
  */
423
453
  whenIdle(): Promise<void>;
424
454
  /** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
@@ -433,6 +463,33 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
433
463
  private enqueueOperation;
434
464
  private runIfIdle;
435
465
  private runOperation;
466
+ /**
467
+ * Releases the mutex, normalizing its two failure modes into one result: a
468
+ * thrown error, and a false return — the failure signal MutexInterface /
469
+ * LockAdapterInterface define (a PostgreSQL advisory unlock that returns
470
+ * false, a Redis DEL that removed nothing). A false return means the lock
471
+ * may still be held, so it must never be mistaken for a successful release.
472
+ *
473
+ * Every failure is surfaced through the diagnostic hook — when the
474
+ * operation also failed, the rejection carries the operation error and this
475
+ * hook is the only place the release error appears.
476
+ *
477
+ * A failure while the mutex claimed to hold the lock leaves ownership
478
+ * uncertain and blocks later operations; a success clears that state. A
479
+ * failed release of a lock the mutex did not claim (a defensive manual
480
+ * release) is still reported but changes nothing.
481
+ *
482
+ * @returns null on success, or the failure wrapped for the caller to raise.
483
+ */
484
+ private releaseMutex;
485
+ /**
486
+ * Runs a user diagnostic hook in isolation. Neither a synchronous throw nor
487
+ * a rejection of a returned promise may reach the drain loop or the host:
488
+ * an unavailable telemetry backend must not fail an operation or, via an
489
+ * unhandled rejection, terminate the process. A returned promise is
490
+ * deliberately not awaited — a slow reporter must not stall the runner.
491
+ */
492
+ private callDiagnosticHook;
436
493
  private resolveEvent;
437
494
  /**
438
495
  * Drive transitions starting from the current state, following automatic
@@ -480,11 +537,11 @@ interface LastStateHasChangedDateInterface {
480
537
  getLastStateHasChangedDate(): Date;
481
538
  }
482
539
 
483
- /** @deprecated No longer used internally; will be removed in v4. */
540
+ /** @deprecated No longer used internally; will be removed in v5. */
484
541
  interface CallbackInterface {
485
542
  invoke(): MaybePromise<void>;
486
543
  }
487
- /** @deprecated No longer used internally; will be removed in v4. */
544
+ /** @deprecated No longer used internally; will be removed in v5. */
488
545
  interface DispatcherInterface extends CallbackInterface {
489
546
  dispatch(event: EventInterface, args?: unknown[]): void;
490
547
  invoke(): Promise<void>;
@@ -532,34 +589,45 @@ declare abstract class CompositeCondition<TSubject = unknown> implements Conditi
532
589
  constructor(joinWord: string, condition: ConditionInterface<TSubject>);
533
590
  protected addCondition(condition: ConditionInterface<TSubject>): this;
534
591
  getName(): string;
535
- abstract checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
592
+ abstract checkCondition(subject: TSubject, context: Map<string, unknown>): MaybePromise<boolean>;
593
+ /**
594
+ * Evaluates children in order, stopping at the first whose result equals
595
+ * `shortCircuitOn`. A child that returns a plain boolean is consumed
596
+ * synchronously; only a returned promise is awaited. Awaiting plain values
597
+ * would yield between children and end the machine's synchronous
598
+ * re-entrancy guard, so a re-entrant later child would deadlock instead of
599
+ * throwing ReentrancyError. For the same reason the composite itself
600
+ * returns a plain boolean when every child it evaluated did.
601
+ */
602
+ protected evaluate(subject: TSubject, context: Map<string, unknown>, shortCircuitOn: boolean): MaybePromise<boolean>;
536
603
  }
537
604
 
538
605
  declare class AndComposite<TSubject = unknown> extends CompositeCondition<TSubject> {
539
606
  constructor(condition: ConditionInterface<TSubject>);
540
607
  addAnd(condition: ConditionInterface<TSubject>): this;
541
- checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
608
+ checkCondition(subject: TSubject, context: Map<string, unknown>): MaybePromise<boolean>;
542
609
  }
543
610
 
544
611
  declare class OrComposite<TSubject = unknown> extends CompositeCondition<TSubject> {
545
612
  constructor(condition: ConditionInterface<TSubject>);
546
613
  addOr(condition: ConditionInterface<TSubject>): this;
547
- checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
614
+ checkCondition(subject: TSubject, context: Map<string, unknown>): MaybePromise<boolean>;
548
615
  }
549
616
 
550
617
  declare class Not<TSubject = unknown> implements ConditionInterface<TSubject> {
551
618
  private readonly condition;
552
619
  constructor(condition: ConditionInterface<TSubject>);
553
620
  getName(): string;
554
- checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
621
+ /** Stays synchronous for a synchronous child — see CompositeCondition. */
622
+ checkCondition(subject: TSubject, context: Map<string, unknown>): MaybePromise<boolean>;
555
623
  }
556
624
 
557
625
  /**
558
- * Legacy Observer for Event observers (commands attached to specific events).
626
+ * Observer for Event observers (commands attached to specific events).
559
627
  *
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.
628
+ * This is not a Statemachine observer. To run a callback after every
629
+ * transition, implement AfterTransitionObserver directly or compose a small
630
+ * wrapper.
563
631
  */
564
632
  declare class CallbackObserver implements Observer {
565
633
  private readonly callback;
@@ -666,7 +734,20 @@ declare class LockAdapterMutex implements MutexInterface {
666
734
  private readonly lockAdapter;
667
735
  private readonly resourceName;
668
736
  private acquired;
737
+ private pendingAcquire;
669
738
  constructor(lockAdapter: LockAdapterInterface, resourceName: string);
739
+ /**
740
+ * Overlapping calls share one underlying acquire: the `acquired` flag is
741
+ * only set after the adapter resolves, so without this both callers would
742
+ * pass the check and acquire twice on a non-idempotent adapter (database
743
+ * advisory locks, redis SET NX). The pending promise is cleared once it
744
+ * settles, so a failed acquire can still be retried.
745
+ *
746
+ * The clearing is attached to the attempt only after it is stored: an
747
+ * adapter that throws synchronously settles the attempt before the
748
+ * assignment would otherwise run, and clearing inside the attempt itself
749
+ * would then leave the rejected promise cached forever.
750
+ */
670
751
  acquireLock(): Promise<boolean>;
671
752
  releaseLock(): Promise<boolean>;
672
753
  isAcquired(): boolean;
@@ -681,6 +762,15 @@ declare class MutexFactory<TSubject = unknown> implements MutexFactoryInterface<
681
762
  createMutex(subject: TSubject): MutexInterface;
682
763
  }
683
764
 
765
+ /**
766
+ * Engine options applied to every machine the factory creates.
767
+ *
768
+ * `initialStateName`, `mutex` and `transitionSelector` are excluded: the
769
+ * factory derives them per subject from the state-name detector, the mutex
770
+ * factory and setTransitionSelector, so a template value could only
771
+ * contradict them.
772
+ */
773
+ type FactoryStatemachineOptions<TSubject = unknown> = Omit<StatemachineOptions<TSubject>, "initialStateName" | "mutex" | "transitionSelector">;
684
774
  declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject> {
685
775
  private readonly processDetector;
686
776
  private readonly stateNameDetector;
@@ -688,7 +778,15 @@ declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject>
688
778
  private readonly afterObservers;
689
779
  private transitionSelector;
690
780
  private mutexFactory;
691
- constructor(processDetector: ProcessDetectorInterface<TSubject>, stateNameDetector?: StateNameDetectorInterface<TSubject> | null);
781
+ private readonly options;
782
+ /**
783
+ * @param options Engine options applied to every machine this factory
784
+ * creates — back-pressure (maxQueueLength), the automatic-hop bound, lock
785
+ * autorelease, and the onChainedOperationError / onReleaseError diagnostic
786
+ * sinks. Without them, factory-created machines would silently run on
787
+ * defaults, which is precisely where those sinks matter most.
788
+ */
789
+ constructor(processDetector: ProcessDetectorInterface<TSubject>, stateNameDetector?: StateNameDetectorInterface<TSubject> | null, options?: FactoryStatemachineOptions<TSubject>);
692
790
  setMutexFactory(factory: MutexFactoryInterface<TSubject> | null): void;
693
791
  setTransitionSelector(selector: TransitionSelectorInterface<TSubject>): void;
694
792
  attachBeforeObserver(observer: BeforeTransitionObserver<TSubject>): void;
@@ -754,7 +852,7 @@ declare class GraphBuilder {
754
852
 
755
853
  declare abstract class FinitaError extends Error {
756
854
  abstract readonly code: string;
757
- constructor(message?: string);
855
+ constructor(message?: string, options?: ErrorOptions);
758
856
  }
759
857
 
760
858
  declare class WrongEventForStateError extends FinitaError {
@@ -769,6 +867,37 @@ declare class LockCanNotBeAcquiredError extends FinitaError {
769
867
  constructor(message?: string);
770
868
  }
771
869
 
870
+ /**
871
+ * The mutex reported a failed release by returning false, as
872
+ * LockAdapterInterface specifies (e.g. a PostgreSQL advisory unlock that
873
+ * returns false, or a Redis DEL that removed nothing).
874
+ *
875
+ * The lock must be assumed to still be held: the engine surfaces this so a
876
+ * failed release can never be mistaken for a successful one, which would let
877
+ * every later operation piggyback on — and never release — a stuck lock.
878
+ */
879
+ declare class LockCanNotBeReleasedError extends FinitaError {
880
+ readonly code = "lockCanNotBeReleased";
881
+ constructor(message?: string);
882
+ }
883
+
884
+ /**
885
+ * A lock release failed earlier, so the machine can no longer tell whether it
886
+ * still holds the lock: the unlock may have taken effect remotely with its
887
+ * reply lost, or may never have happened. Running further operations on the
888
+ * old ownership flag could violate mutual exclusion, so every operation is
889
+ * rejected with this error until a manual Statemachine.releaseLock()
890
+ * succeeds.
891
+ *
892
+ * `cause` carries the release failure. When the release cannot be confirmed
893
+ * — typically because the lock was in fact already freed — discard the
894
+ * machine and build a new one from persisted state.
895
+ */
896
+ declare class LockOwnershipUncertainError extends FinitaError {
897
+ readonly code = "lockOwnershipUncertain";
898
+ constructor(cause: unknown);
899
+ }
900
+
772
901
  declare class DuplicateStateError extends FinitaError {
773
902
  readonly code = "duplicateState";
774
903
  readonly stateName: string;
@@ -831,10 +960,19 @@ declare class InvalidSubjectError extends FinitaError {
831
960
  constructor(expectedInterface: string, missingMembers: Iterable<string>);
832
961
  }
833
962
 
963
+ /** One of the simultaneously-active transitions that caused the ambiguity. */
964
+ interface AmbiguousTransitionCandidate {
965
+ targetStateName: string;
966
+ eventName: string | null;
967
+ conditionName: string | null;
968
+ weight: number;
969
+ }
834
970
  declare class AmbiguousTransitionError extends FinitaError {
835
971
  readonly code = "ambiguousTransition";
836
972
  readonly activeCount: number;
837
- constructor(activeCount: number);
973
+ /** The competing transitions — what you need to resolve the ambiguity. */
974
+ readonly candidates: readonly Readonly<AmbiguousTransitionCandidate>[];
975
+ constructor(activeCount: number, candidates?: Iterable<AmbiguousTransitionCandidate>);
838
976
  }
839
977
 
840
978
  declare class AutomaticTransitionCycleError extends FinitaError {
@@ -854,4 +992,4 @@ declare class QueueLimitExceededError extends FinitaError {
854
992
  constructor(limit: number, eventName: string | null);
855
993
  }
856
994
 
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 };
995
+ 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, LockOwnershipUncertainError, 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;
@@ -133,6 +135,8 @@ declare class State implements StateInterface {
133
135
  */
134
136
  _initTransitions(key: InternalConstructionKey, transitions: Iterable<TransitionInterface>): void;
135
137
  getName(): string;
138
+ /** Snapshot — the graph is shared by every machine built from the
139
+ * process, so callers must never receive the collection itself. */
136
140
  getTransitions(): Iterable<TransitionInterface>;
137
141
  getEventNames(): string[];
138
142
  hasEvent(name: string): boolean;
@@ -187,7 +191,9 @@ declare class ProcessBuilder<TSubject = unknown> {
187
191
  private findInitialStateName;
188
192
  private validateTransitionEndpoints;
189
193
  /** Transition identity: (fromState, eventName, toState). Used by both the
190
- * conflict check and the build-time dedup — keep them in lockstep. */
194
+ * conflict check and the build-time dedup — keep them in lockstep.
195
+ * Encoded as a JSON tuple, not a delimiter join: names may contain any
196
+ * character, so no delimiter can keep distinct tuples distinct. */
191
197
  private static transitionKey;
192
198
  private validateNoConflictingDuplicates;
193
199
  private collectEventNamesByState;
@@ -349,8 +355,11 @@ interface StatemachineOptions<TSubject = unknown> {
349
355
  * Called when an operation chained via EnqueueContext.enqueue() fails.
350
356
  * Chained operations are not awaited by the caller whose transition
351
357
  * enqueued them, so without this hook their errors are discarded.
352
- * Exceptions thrown by the hook itself are swallowed — it must not be
353
- * able to break the machine's drain loop.
358
+ * The hook may be async. It is not awaited, and both exceptions it throws
359
+ * and rejections of the promise it returns are swallowed — it must not be
360
+ * able to break the machine's drain loop or surface as an unhandled
361
+ * rejection. (The return type stays `void`, which already admits async
362
+ * functions, so callbacks returning incidental values keep compiling.)
354
363
  */
355
364
  onChainedOperationError?: (error: unknown, info: {
356
365
  eventName: string;
@@ -372,7 +381,8 @@ interface StatemachineOptions<TSubject = unknown> {
372
381
  * release throws — including when the operation itself also failed, in
373
382
  * which case the caller's rejection carries the operation error and the
374
383
  * release error would otherwise be discarded. Does not change rejection
375
- * behavior. Exceptions thrown by the hook itself are swallowed.
384
+ * behavior. The hook may be async; it is not awaited, and both exceptions
385
+ * it throws and rejections of the promise it returns are swallowed.
376
386
  */
377
387
  onReleaseError?: (error: unknown) => void;
378
388
  }
@@ -391,6 +401,8 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
391
401
  private running;
392
402
  private idleWaiters;
393
403
  private inSyncCallback;
404
+ /** Set when releasing a held lock fails; see LockOwnershipUncertainError. */
405
+ private ownershipUncertainty;
394
406
  private readonly beforeObservers;
395
407
  private readonly afterObservers;
396
408
  private readonly onChainedOperationError?;
@@ -402,11 +414,24 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
402
414
  getProcess(): ProcessInterface;
403
415
  attachBefore(observer: BeforeTransitionObserver<TSubject>): void;
404
416
  detachBefore(observer: BeforeTransitionObserver<TSubject>): void;
417
+ /** Snapshot — detaching later does not change an already-returned list,
418
+ * and mutating it does not change the machine's registrations. */
405
419
  getBeforeObservers(): Iterable<BeforeTransitionObserver<TSubject>>;
406
420
  attachAfter(observer: AfterTransitionObserver<TSubject>): void;
407
421
  detachAfter(observer: AfterTransitionObserver<TSubject>): void;
422
+ /** Snapshot — see getBeforeObservers. */
408
423
  getAfterObservers(): Iterable<AfterTransitionObserver<TSubject>>;
409
424
  acquireLock(): Promise<boolean>;
425
+ /**
426
+ * Releases the mutex. A failed release — whether the mutex throws or
427
+ * returns false — is reported to the onReleaseError hook; it is not thrown,
428
+ * so manual lock management keeps its existing control flow. Inspect
429
+ * isLockAcquired() (or the hook) to learn whether the lock was actually
430
+ * freed.
431
+ *
432
+ * A failed release of a held lock makes every later operation reject with
433
+ * LockOwnershipUncertainError; a successful call here is how to recover.
434
+ */
410
435
  releaseLock(): Promise<void>;
411
436
  isLockAcquired(): boolean;
412
437
  isAutoreleaseLock(): boolean;
@@ -419,6 +444,11 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
419
444
  * EnqueueContext.enqueue(), has completed. Resolves immediately if the
420
445
  * machine is already idle. Note this is a quiescence point, not a
421
446
  * receipt: work scheduled later (e.g. from a timer) starts a new drain.
447
+ *
448
+ * Like triggerEvent/checkTransitions, this may not be called from inside an
449
+ * observer or condition of the same machine: the machine cannot reach idle
450
+ * while the runner is blocked on that very callback, so awaiting it there
451
+ * always deadlocks.
422
452
  */
423
453
  whenIdle(): Promise<void>;
424
454
  /** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
@@ -433,6 +463,33 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
433
463
  private enqueueOperation;
434
464
  private runIfIdle;
435
465
  private runOperation;
466
+ /**
467
+ * Releases the mutex, normalizing its two failure modes into one result: a
468
+ * thrown error, and a false return — the failure signal MutexInterface /
469
+ * LockAdapterInterface define (a PostgreSQL advisory unlock that returns
470
+ * false, a Redis DEL that removed nothing). A false return means the lock
471
+ * may still be held, so it must never be mistaken for a successful release.
472
+ *
473
+ * Every failure is surfaced through the diagnostic hook — when the
474
+ * operation also failed, the rejection carries the operation error and this
475
+ * hook is the only place the release error appears.
476
+ *
477
+ * A failure while the mutex claimed to hold the lock leaves ownership
478
+ * uncertain and blocks later operations; a success clears that state. A
479
+ * failed release of a lock the mutex did not claim (a defensive manual
480
+ * release) is still reported but changes nothing.
481
+ *
482
+ * @returns null on success, or the failure wrapped for the caller to raise.
483
+ */
484
+ private releaseMutex;
485
+ /**
486
+ * Runs a user diagnostic hook in isolation. Neither a synchronous throw nor
487
+ * a rejection of a returned promise may reach the drain loop or the host:
488
+ * an unavailable telemetry backend must not fail an operation or, via an
489
+ * unhandled rejection, terminate the process. A returned promise is
490
+ * deliberately not awaited — a slow reporter must not stall the runner.
491
+ */
492
+ private callDiagnosticHook;
436
493
  private resolveEvent;
437
494
  /**
438
495
  * Drive transitions starting from the current state, following automatic
@@ -480,11 +537,11 @@ interface LastStateHasChangedDateInterface {
480
537
  getLastStateHasChangedDate(): Date;
481
538
  }
482
539
 
483
- /** @deprecated No longer used internally; will be removed in v4. */
540
+ /** @deprecated No longer used internally; will be removed in v5. */
484
541
  interface CallbackInterface {
485
542
  invoke(): MaybePromise<void>;
486
543
  }
487
- /** @deprecated No longer used internally; will be removed in v4. */
544
+ /** @deprecated No longer used internally; will be removed in v5. */
488
545
  interface DispatcherInterface extends CallbackInterface {
489
546
  dispatch(event: EventInterface, args?: unknown[]): void;
490
547
  invoke(): Promise<void>;
@@ -532,34 +589,45 @@ declare abstract class CompositeCondition<TSubject = unknown> implements Conditi
532
589
  constructor(joinWord: string, condition: ConditionInterface<TSubject>);
533
590
  protected addCondition(condition: ConditionInterface<TSubject>): this;
534
591
  getName(): string;
535
- abstract checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
592
+ abstract checkCondition(subject: TSubject, context: Map<string, unknown>): MaybePromise<boolean>;
593
+ /**
594
+ * Evaluates children in order, stopping at the first whose result equals
595
+ * `shortCircuitOn`. A child that returns a plain boolean is consumed
596
+ * synchronously; only a returned promise is awaited. Awaiting plain values
597
+ * would yield between children and end the machine's synchronous
598
+ * re-entrancy guard, so a re-entrant later child would deadlock instead of
599
+ * throwing ReentrancyError. For the same reason the composite itself
600
+ * returns a plain boolean when every child it evaluated did.
601
+ */
602
+ protected evaluate(subject: TSubject, context: Map<string, unknown>, shortCircuitOn: boolean): MaybePromise<boolean>;
536
603
  }
537
604
 
538
605
  declare class AndComposite<TSubject = unknown> extends CompositeCondition<TSubject> {
539
606
  constructor(condition: ConditionInterface<TSubject>);
540
607
  addAnd(condition: ConditionInterface<TSubject>): this;
541
- checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
608
+ checkCondition(subject: TSubject, context: Map<string, unknown>): MaybePromise<boolean>;
542
609
  }
543
610
 
544
611
  declare class OrComposite<TSubject = unknown> extends CompositeCondition<TSubject> {
545
612
  constructor(condition: ConditionInterface<TSubject>);
546
613
  addOr(condition: ConditionInterface<TSubject>): this;
547
- checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
614
+ checkCondition(subject: TSubject, context: Map<string, unknown>): MaybePromise<boolean>;
548
615
  }
549
616
 
550
617
  declare class Not<TSubject = unknown> implements ConditionInterface<TSubject> {
551
618
  private readonly condition;
552
619
  constructor(condition: ConditionInterface<TSubject>);
553
620
  getName(): string;
554
- checkCondition(subject: TSubject, context: Map<string, unknown>): Promise<boolean>;
621
+ /** Stays synchronous for a synchronous child — see CompositeCondition. */
622
+ checkCondition(subject: TSubject, context: Map<string, unknown>): MaybePromise<boolean>;
555
623
  }
556
624
 
557
625
  /**
558
- * Legacy Observer for Event observers (commands attached to specific events).
626
+ * Observer for Event observers (commands attached to specific events).
559
627
  *
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.
628
+ * This is not a Statemachine observer. To run a callback after every
629
+ * transition, implement AfterTransitionObserver directly or compose a small
630
+ * wrapper.
563
631
  */
564
632
  declare class CallbackObserver implements Observer {
565
633
  private readonly callback;
@@ -666,7 +734,20 @@ declare class LockAdapterMutex implements MutexInterface {
666
734
  private readonly lockAdapter;
667
735
  private readonly resourceName;
668
736
  private acquired;
737
+ private pendingAcquire;
669
738
  constructor(lockAdapter: LockAdapterInterface, resourceName: string);
739
+ /**
740
+ * Overlapping calls share one underlying acquire: the `acquired` flag is
741
+ * only set after the adapter resolves, so without this both callers would
742
+ * pass the check and acquire twice on a non-idempotent adapter (database
743
+ * advisory locks, redis SET NX). The pending promise is cleared once it
744
+ * settles, so a failed acquire can still be retried.
745
+ *
746
+ * The clearing is attached to the attempt only after it is stored: an
747
+ * adapter that throws synchronously settles the attempt before the
748
+ * assignment would otherwise run, and clearing inside the attempt itself
749
+ * would then leave the rejected promise cached forever.
750
+ */
670
751
  acquireLock(): Promise<boolean>;
671
752
  releaseLock(): Promise<boolean>;
672
753
  isAcquired(): boolean;
@@ -681,6 +762,15 @@ declare class MutexFactory<TSubject = unknown> implements MutexFactoryInterface<
681
762
  createMutex(subject: TSubject): MutexInterface;
682
763
  }
683
764
 
765
+ /**
766
+ * Engine options applied to every machine the factory creates.
767
+ *
768
+ * `initialStateName`, `mutex` and `transitionSelector` are excluded: the
769
+ * factory derives them per subject from the state-name detector, the mutex
770
+ * factory and setTransitionSelector, so a template value could only
771
+ * contradict them.
772
+ */
773
+ type FactoryStatemachineOptions<TSubject = unknown> = Omit<StatemachineOptions<TSubject>, "initialStateName" | "mutex" | "transitionSelector">;
684
774
  declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject> {
685
775
  private readonly processDetector;
686
776
  private readonly stateNameDetector;
@@ -688,7 +778,15 @@ declare class Factory<TSubject = unknown> implements FactoryInterface<TSubject>
688
778
  private readonly afterObservers;
689
779
  private transitionSelector;
690
780
  private mutexFactory;
691
- constructor(processDetector: ProcessDetectorInterface<TSubject>, stateNameDetector?: StateNameDetectorInterface<TSubject> | null);
781
+ private readonly options;
782
+ /**
783
+ * @param options Engine options applied to every machine this factory
784
+ * creates — back-pressure (maxQueueLength), the automatic-hop bound, lock
785
+ * autorelease, and the onChainedOperationError / onReleaseError diagnostic
786
+ * sinks. Without them, factory-created machines would silently run on
787
+ * defaults, which is precisely where those sinks matter most.
788
+ */
789
+ constructor(processDetector: ProcessDetectorInterface<TSubject>, stateNameDetector?: StateNameDetectorInterface<TSubject> | null, options?: FactoryStatemachineOptions<TSubject>);
692
790
  setMutexFactory(factory: MutexFactoryInterface<TSubject> | null): void;
693
791
  setTransitionSelector(selector: TransitionSelectorInterface<TSubject>): void;
694
792
  attachBeforeObserver(observer: BeforeTransitionObserver<TSubject>): void;
@@ -754,7 +852,7 @@ declare class GraphBuilder {
754
852
 
755
853
  declare abstract class FinitaError extends Error {
756
854
  abstract readonly code: string;
757
- constructor(message?: string);
855
+ constructor(message?: string, options?: ErrorOptions);
758
856
  }
759
857
 
760
858
  declare class WrongEventForStateError extends FinitaError {
@@ -769,6 +867,37 @@ declare class LockCanNotBeAcquiredError extends FinitaError {
769
867
  constructor(message?: string);
770
868
  }
771
869
 
870
+ /**
871
+ * The mutex reported a failed release by returning false, as
872
+ * LockAdapterInterface specifies (e.g. a PostgreSQL advisory unlock that
873
+ * returns false, or a Redis DEL that removed nothing).
874
+ *
875
+ * The lock must be assumed to still be held: the engine surfaces this so a
876
+ * failed release can never be mistaken for a successful one, which would let
877
+ * every later operation piggyback on — and never release — a stuck lock.
878
+ */
879
+ declare class LockCanNotBeReleasedError extends FinitaError {
880
+ readonly code = "lockCanNotBeReleased";
881
+ constructor(message?: string);
882
+ }
883
+
884
+ /**
885
+ * A lock release failed earlier, so the machine can no longer tell whether it
886
+ * still holds the lock: the unlock may have taken effect remotely with its
887
+ * reply lost, or may never have happened. Running further operations on the
888
+ * old ownership flag could violate mutual exclusion, so every operation is
889
+ * rejected with this error until a manual Statemachine.releaseLock()
890
+ * succeeds.
891
+ *
892
+ * `cause` carries the release failure. When the release cannot be confirmed
893
+ * — typically because the lock was in fact already freed — discard the
894
+ * machine and build a new one from persisted state.
895
+ */
896
+ declare class LockOwnershipUncertainError extends FinitaError {
897
+ readonly code = "lockOwnershipUncertain";
898
+ constructor(cause: unknown);
899
+ }
900
+
772
901
  declare class DuplicateStateError extends FinitaError {
773
902
  readonly code = "duplicateState";
774
903
  readonly stateName: string;
@@ -831,10 +960,19 @@ declare class InvalidSubjectError extends FinitaError {
831
960
  constructor(expectedInterface: string, missingMembers: Iterable<string>);
832
961
  }
833
962
 
963
+ /** One of the simultaneously-active transitions that caused the ambiguity. */
964
+ interface AmbiguousTransitionCandidate {
965
+ targetStateName: string;
966
+ eventName: string | null;
967
+ conditionName: string | null;
968
+ weight: number;
969
+ }
834
970
  declare class AmbiguousTransitionError extends FinitaError {
835
971
  readonly code = "ambiguousTransition";
836
972
  readonly activeCount: number;
837
- constructor(activeCount: number);
973
+ /** The competing transitions — what you need to resolve the ambiguity. */
974
+ readonly candidates: readonly Readonly<AmbiguousTransitionCandidate>[];
975
+ constructor(activeCount: number, candidates?: Iterable<AmbiguousTransitionCandidate>);
838
976
  }
839
977
 
840
978
  declare class AutomaticTransitionCycleError extends FinitaError {
@@ -854,4 +992,4 @@ declare class QueueLimitExceededError extends FinitaError {
854
992
  constructor(limit: number, eventName: string | null);
855
993
  }
856
994
 
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 };
995
+ 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, LockOwnershipUncertainError, 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 };