@camcima/finita 4.0.0 → 4.1.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.cjs +97 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -5
- package/dist/index.d.ts +68 -5
- package/dist/index.js +96 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -287,6 +287,12 @@ interface StatemachineInterface<TSubject = unknown> {
|
|
|
287
287
|
getProcess(): ProcessInterface;
|
|
288
288
|
triggerEvent(name: string, context?: Map<string, unknown>): Promise<void>;
|
|
289
289
|
checkTransitions(context?: Map<string, unknown>): Promise<void>;
|
|
290
|
+
/**
|
|
291
|
+
* Resolves once the operation queue is empty and the runner is idle,
|
|
292
|
+
* including operations chained via EnqueueContext.enqueue(). Resolves
|
|
293
|
+
* immediately if the machine is already idle.
|
|
294
|
+
*/
|
|
295
|
+
whenIdle(): Promise<void>;
|
|
290
296
|
attachBefore(observer: BeforeTransitionObserver<TSubject>): void;
|
|
291
297
|
detachBefore(observer: BeforeTransitionObserver<TSubject>): void;
|
|
292
298
|
getBeforeObservers(): Iterable<BeforeTransitionObserver<TSubject>>;
|
|
@@ -316,7 +322,16 @@ interface StatemachineOptions<TSubject = unknown> {
|
|
|
316
322
|
initialStateName?: string;
|
|
317
323
|
/** Defaults to OneOrNoneActiveTransition. */
|
|
318
324
|
transitionSelector?: TransitionSelectorInterface<TSubject>;
|
|
319
|
-
/**
|
|
325
|
+
/**
|
|
326
|
+
* Defaults to NullMutex (no cross-process serialization).
|
|
327
|
+
*
|
|
328
|
+
* Must be exclusive to this machine — never share one MutexInterface
|
|
329
|
+
* instance between machines: the engine reads isAcquired() as "this
|
|
330
|
+
* machine holds the lock", so a shared instance silently disables mutual
|
|
331
|
+
* exclusion. To coordinate machines, share the underlying
|
|
332
|
+
* LockAdapterInterface (same resource name) and construct one mutex per
|
|
333
|
+
* machine, as MutexFactory does.
|
|
334
|
+
*/
|
|
320
335
|
mutex?: MutexInterface;
|
|
321
336
|
/** When true, the engine releases the mutex at the end of each top-level operation. Defaults to true. */
|
|
322
337
|
autoreleaseLock?: boolean;
|
|
@@ -330,6 +345,36 @@ interface StatemachineOptions<TSubject = unknown> {
|
|
|
330
345
|
* Defaults to 100.
|
|
331
346
|
*/
|
|
332
347
|
maxAutomaticHops?: number;
|
|
348
|
+
/**
|
|
349
|
+
* Called when an operation chained via EnqueueContext.enqueue() fails.
|
|
350
|
+
* Chained operations are not awaited by the caller whose transition
|
|
351
|
+
* 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.
|
|
354
|
+
*/
|
|
355
|
+
onChainedOperationError?: (error: unknown, info: {
|
|
356
|
+
eventName: string;
|
|
357
|
+
}) => void;
|
|
358
|
+
/**
|
|
359
|
+
* Maximum number of operations that may wait in the queue (the running
|
|
360
|
+
* operation does not count). When the limit is reached, further
|
|
361
|
+
* triggerEvent/checkTransitions calls reject with
|
|
362
|
+
* QueueLimitExceededError, and EnqueueContext.enqueue() throws it into
|
|
363
|
+
* the enqueuing after-observer's error path. In that case the original
|
|
364
|
+
* caller's promise rejects even though its transition already
|
|
365
|
+
* committed — check the machine's state, not just the rejection, before
|
|
366
|
+
* retrying. Must be a positive integer when set. Defaults to Infinity
|
|
367
|
+
* (unbounded, the previous behavior).
|
|
368
|
+
*/
|
|
369
|
+
maxQueueLength?: number;
|
|
370
|
+
/**
|
|
371
|
+
* Diagnostic hook called whenever the automatic post-operation lock
|
|
372
|
+
* release throws — including when the operation itself also failed, in
|
|
373
|
+
* which case the caller's rejection carries the operation error and the
|
|
374
|
+
* release error would otherwise be discarded. Does not change rejection
|
|
375
|
+
* behavior. Exceptions thrown by the hook itself are swallowed.
|
|
376
|
+
*/
|
|
377
|
+
onReleaseError?: (error: unknown) => void;
|
|
333
378
|
}
|
|
334
379
|
|
|
335
380
|
declare class Statemachine<TSubject = unknown> implements StatemachineInterface<TSubject> {
|
|
@@ -341,11 +386,15 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
|
|
|
341
386
|
private lastState;
|
|
342
387
|
private autoreleaseLock;
|
|
343
388
|
private readonly maxAutomaticHops;
|
|
389
|
+
private readonly maxQueueLength;
|
|
344
390
|
private readonly queue;
|
|
345
391
|
private running;
|
|
392
|
+
private idleWaiters;
|
|
346
393
|
private inSyncCallback;
|
|
347
394
|
private readonly beforeObservers;
|
|
348
395
|
private readonly afterObservers;
|
|
396
|
+
private readonly onChainedOperationError?;
|
|
397
|
+
private readonly onReleaseError?;
|
|
349
398
|
constructor(subject: TSubject, process: ProcessInterface, options?: StatemachineOptions<TSubject>);
|
|
350
399
|
getCurrentState(): StateInterface;
|
|
351
400
|
getLastState(): StateInterface | null;
|
|
@@ -364,6 +413,14 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
|
|
|
364
413
|
setAutoreleaseLock(autorelease: boolean): void;
|
|
365
414
|
triggerEvent(name: string, context?: Map<string, unknown>): Promise<void>;
|
|
366
415
|
checkTransitions(context?: Map<string, unknown>): Promise<void>;
|
|
416
|
+
/**
|
|
417
|
+
* Resolves once the operation queue is empty and the runner is idle —
|
|
418
|
+
* i.e. every operation enqueued so far, including operations chained via
|
|
419
|
+
* EnqueueContext.enqueue(), has completed. Resolves immediately if the
|
|
420
|
+
* machine is already idle. Note this is a quiescence point, not a
|
|
421
|
+
* receipt: work scheduled later (e.g. from a timer) starts a new drain.
|
|
422
|
+
*/
|
|
423
|
+
whenIdle(): Promise<void>;
|
|
367
424
|
/** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
|
|
368
425
|
* the flag is cleared as soon as fn returns (before any promise it returned
|
|
369
426
|
* is awaited), so concurrent external callers are never affected. This
|
|
@@ -674,11 +731,12 @@ interface Graph {
|
|
|
674
731
|
nodes: GraphNode[];
|
|
675
732
|
edges: GraphEdge[];
|
|
676
733
|
}
|
|
734
|
+
type GraphDirection = "TB" | "BT" | "LR" | "RL";
|
|
677
735
|
interface DotOptions {
|
|
678
|
-
rankdir?:
|
|
736
|
+
rankdir?: GraphDirection;
|
|
679
737
|
}
|
|
680
738
|
interface MermaidOptions {
|
|
681
|
-
direction?:
|
|
739
|
+
direction?: GraphDirection;
|
|
682
740
|
}
|
|
683
741
|
declare class GraphBuilder {
|
|
684
742
|
private readonly nodes;
|
|
@@ -723,7 +781,7 @@ declare class ProcessFinalizedError extends FinitaError {
|
|
|
723
781
|
constructor(processName: string);
|
|
724
782
|
}
|
|
725
783
|
|
|
726
|
-
type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidStateName" | "invalidEventName" | "invalidConditionName" | "orphanState";
|
|
784
|
+
type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidStateName" | "invalidEventName" | "invalidConditionName" | "invalidTransitionWeight" | "orphanState";
|
|
727
785
|
declare class GraphValidationError extends FinitaError {
|
|
728
786
|
readonly code: GraphValidationCode;
|
|
729
787
|
readonly details: Readonly<Record<string, unknown>>;
|
|
@@ -791,4 +849,9 @@ declare class ReentrancyError extends FinitaError {
|
|
|
791
849
|
constructor(operation: string);
|
|
792
850
|
}
|
|
793
851
|
|
|
794
|
-
|
|
852
|
+
declare class QueueLimitExceededError extends FinitaError {
|
|
853
|
+
readonly code = "queueLimitExceeded";
|
|
854
|
+
constructor(limit: number, eventName: string | null);
|
|
855
|
+
}
|
|
856
|
+
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -287,6 +287,12 @@ interface StatemachineInterface<TSubject = unknown> {
|
|
|
287
287
|
getProcess(): ProcessInterface;
|
|
288
288
|
triggerEvent(name: string, context?: Map<string, unknown>): Promise<void>;
|
|
289
289
|
checkTransitions(context?: Map<string, unknown>): Promise<void>;
|
|
290
|
+
/**
|
|
291
|
+
* Resolves once the operation queue is empty and the runner is idle,
|
|
292
|
+
* including operations chained via EnqueueContext.enqueue(). Resolves
|
|
293
|
+
* immediately if the machine is already idle.
|
|
294
|
+
*/
|
|
295
|
+
whenIdle(): Promise<void>;
|
|
290
296
|
attachBefore(observer: BeforeTransitionObserver<TSubject>): void;
|
|
291
297
|
detachBefore(observer: BeforeTransitionObserver<TSubject>): void;
|
|
292
298
|
getBeforeObservers(): Iterable<BeforeTransitionObserver<TSubject>>;
|
|
@@ -316,7 +322,16 @@ interface StatemachineOptions<TSubject = unknown> {
|
|
|
316
322
|
initialStateName?: string;
|
|
317
323
|
/** Defaults to OneOrNoneActiveTransition. */
|
|
318
324
|
transitionSelector?: TransitionSelectorInterface<TSubject>;
|
|
319
|
-
/**
|
|
325
|
+
/**
|
|
326
|
+
* Defaults to NullMutex (no cross-process serialization).
|
|
327
|
+
*
|
|
328
|
+
* Must be exclusive to this machine — never share one MutexInterface
|
|
329
|
+
* instance between machines: the engine reads isAcquired() as "this
|
|
330
|
+
* machine holds the lock", so a shared instance silently disables mutual
|
|
331
|
+
* exclusion. To coordinate machines, share the underlying
|
|
332
|
+
* LockAdapterInterface (same resource name) and construct one mutex per
|
|
333
|
+
* machine, as MutexFactory does.
|
|
334
|
+
*/
|
|
320
335
|
mutex?: MutexInterface;
|
|
321
336
|
/** When true, the engine releases the mutex at the end of each top-level operation. Defaults to true. */
|
|
322
337
|
autoreleaseLock?: boolean;
|
|
@@ -330,6 +345,36 @@ interface StatemachineOptions<TSubject = unknown> {
|
|
|
330
345
|
* Defaults to 100.
|
|
331
346
|
*/
|
|
332
347
|
maxAutomaticHops?: number;
|
|
348
|
+
/**
|
|
349
|
+
* Called when an operation chained via EnqueueContext.enqueue() fails.
|
|
350
|
+
* Chained operations are not awaited by the caller whose transition
|
|
351
|
+
* 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.
|
|
354
|
+
*/
|
|
355
|
+
onChainedOperationError?: (error: unknown, info: {
|
|
356
|
+
eventName: string;
|
|
357
|
+
}) => void;
|
|
358
|
+
/**
|
|
359
|
+
* Maximum number of operations that may wait in the queue (the running
|
|
360
|
+
* operation does not count). When the limit is reached, further
|
|
361
|
+
* triggerEvent/checkTransitions calls reject with
|
|
362
|
+
* QueueLimitExceededError, and EnqueueContext.enqueue() throws it into
|
|
363
|
+
* the enqueuing after-observer's error path. In that case the original
|
|
364
|
+
* caller's promise rejects even though its transition already
|
|
365
|
+
* committed — check the machine's state, not just the rejection, before
|
|
366
|
+
* retrying. Must be a positive integer when set. Defaults to Infinity
|
|
367
|
+
* (unbounded, the previous behavior).
|
|
368
|
+
*/
|
|
369
|
+
maxQueueLength?: number;
|
|
370
|
+
/**
|
|
371
|
+
* Diagnostic hook called whenever the automatic post-operation lock
|
|
372
|
+
* release throws — including when the operation itself also failed, in
|
|
373
|
+
* which case the caller's rejection carries the operation error and the
|
|
374
|
+
* release error would otherwise be discarded. Does not change rejection
|
|
375
|
+
* behavior. Exceptions thrown by the hook itself are swallowed.
|
|
376
|
+
*/
|
|
377
|
+
onReleaseError?: (error: unknown) => void;
|
|
333
378
|
}
|
|
334
379
|
|
|
335
380
|
declare class Statemachine<TSubject = unknown> implements StatemachineInterface<TSubject> {
|
|
@@ -341,11 +386,15 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
|
|
|
341
386
|
private lastState;
|
|
342
387
|
private autoreleaseLock;
|
|
343
388
|
private readonly maxAutomaticHops;
|
|
389
|
+
private readonly maxQueueLength;
|
|
344
390
|
private readonly queue;
|
|
345
391
|
private running;
|
|
392
|
+
private idleWaiters;
|
|
346
393
|
private inSyncCallback;
|
|
347
394
|
private readonly beforeObservers;
|
|
348
395
|
private readonly afterObservers;
|
|
396
|
+
private readonly onChainedOperationError?;
|
|
397
|
+
private readonly onReleaseError?;
|
|
349
398
|
constructor(subject: TSubject, process: ProcessInterface, options?: StatemachineOptions<TSubject>);
|
|
350
399
|
getCurrentState(): StateInterface;
|
|
351
400
|
getLastState(): StateInterface | null;
|
|
@@ -364,6 +413,14 @@ declare class Statemachine<TSubject = unknown> implements StatemachineInterface<
|
|
|
364
413
|
setAutoreleaseLock(autorelease: boolean): void;
|
|
365
414
|
triggerEvent(name: string, context?: Map<string, unknown>): Promise<void>;
|
|
366
415
|
checkTransitions(context?: Map<string, unknown>): Promise<void>;
|
|
416
|
+
/**
|
|
417
|
+
* Resolves once the operation queue is empty and the runner is idle —
|
|
418
|
+
* i.e. every operation enqueued so far, including operations chained via
|
|
419
|
+
* EnqueueContext.enqueue(), has completed. Resolves immediately if the
|
|
420
|
+
* machine is already idle. Note this is a quiescence point, not a
|
|
421
|
+
* receipt: work scheduled later (e.g. from a timer) starts a new drain.
|
|
422
|
+
*/
|
|
423
|
+
whenIdle(): Promise<void>;
|
|
367
424
|
/** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
|
|
368
425
|
* the flag is cleared as soon as fn returns (before any promise it returned
|
|
369
426
|
* is awaited), so concurrent external callers are never affected. This
|
|
@@ -674,11 +731,12 @@ interface Graph {
|
|
|
674
731
|
nodes: GraphNode[];
|
|
675
732
|
edges: GraphEdge[];
|
|
676
733
|
}
|
|
734
|
+
type GraphDirection = "TB" | "BT" | "LR" | "RL";
|
|
677
735
|
interface DotOptions {
|
|
678
|
-
rankdir?:
|
|
736
|
+
rankdir?: GraphDirection;
|
|
679
737
|
}
|
|
680
738
|
interface MermaidOptions {
|
|
681
|
-
direction?:
|
|
739
|
+
direction?: GraphDirection;
|
|
682
740
|
}
|
|
683
741
|
declare class GraphBuilder {
|
|
684
742
|
private readonly nodes;
|
|
@@ -723,7 +781,7 @@ declare class ProcessFinalizedError extends FinitaError {
|
|
|
723
781
|
constructor(processName: string);
|
|
724
782
|
}
|
|
725
783
|
|
|
726
|
-
type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidStateName" | "invalidEventName" | "invalidConditionName" | "orphanState";
|
|
784
|
+
type GraphValidationCode = "unknownTarget" | "unknownSource" | "missingInitialState" | "multipleInitialStates" | "invalidStateName" | "invalidEventName" | "invalidConditionName" | "invalidTransitionWeight" | "orphanState";
|
|
727
785
|
declare class GraphValidationError extends FinitaError {
|
|
728
786
|
readonly code: GraphValidationCode;
|
|
729
787
|
readonly details: Readonly<Record<string, unknown>>;
|
|
@@ -791,4 +849,9 @@ declare class ReentrancyError extends FinitaError {
|
|
|
791
849
|
constructor(operation: string);
|
|
792
850
|
}
|
|
793
851
|
|
|
794
|
-
|
|
852
|
+
declare class QueueLimitExceededError extends FinitaError {
|
|
853
|
+
readonly code = "queueLimitExceeded";
|
|
854
|
+
constructor(limit: number, eventName: string | null);
|
|
855
|
+
}
|
|
856
|
+
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -184,6 +184,7 @@ var State = class {
|
|
|
184
184
|
throw new Error(`State "${this.name}" transitions already set`);
|
|
185
185
|
}
|
|
186
186
|
this._transitions = new Set(transitions);
|
|
187
|
+
Object.freeze(this);
|
|
187
188
|
}
|
|
188
189
|
getName() {
|
|
189
190
|
return this.name;
|
|
@@ -234,6 +235,7 @@ var Transition = class {
|
|
|
234
235
|
this.eventName = eventName;
|
|
235
236
|
this.condition = condition;
|
|
236
237
|
this.weight = weight;
|
|
238
|
+
Object.freeze(this);
|
|
237
239
|
}
|
|
238
240
|
getTargetState() {
|
|
239
241
|
return this.targetState;
|
|
@@ -368,12 +370,20 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
368
370
|
{ fromState, toState, conditionName }
|
|
369
371
|
);
|
|
370
372
|
}
|
|
373
|
+
const weight = options.weight ?? 1;
|
|
374
|
+
if (!Number.isFinite(weight)) {
|
|
375
|
+
throw new GraphValidationError(
|
|
376
|
+
"invalidTransitionWeight",
|
|
377
|
+
`addTransition from "${fromState}" to "${toState}": weight must be a finite number; got ${String(weight)}`,
|
|
378
|
+
{ fromState, toState, eventName, weight }
|
|
379
|
+
);
|
|
380
|
+
}
|
|
371
381
|
this.transitionSpecs.push({
|
|
372
382
|
fromState,
|
|
373
383
|
toState,
|
|
374
384
|
eventName,
|
|
375
385
|
condition: options.condition ?? null,
|
|
376
|
-
weight
|
|
386
|
+
weight
|
|
377
387
|
});
|
|
378
388
|
return this;
|
|
379
389
|
}
|
|
@@ -381,7 +391,6 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
381
391
|
if (this.built) {
|
|
382
392
|
throw new ProcessFinalizedError(this.processName);
|
|
383
393
|
}
|
|
384
|
-
this.built = true;
|
|
385
394
|
this.validateInitialState();
|
|
386
395
|
this.validateTransitionEndpoints();
|
|
387
396
|
this.validateNoConflictingDuplicates();
|
|
@@ -392,6 +401,7 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
392
401
|
this.validateOrphans(finalStates, initialName);
|
|
393
402
|
}
|
|
394
403
|
const initialState = finalStates.get(initialName);
|
|
404
|
+
this.built = true;
|
|
395
405
|
return new Process(
|
|
396
406
|
INTERNAL_CONSTRUCTION_KEY,
|
|
397
407
|
this.processName,
|
|
@@ -636,6 +646,9 @@ var OperationQueue = class {
|
|
|
636
646
|
isEmpty() {
|
|
637
647
|
return this.items.length === 0;
|
|
638
648
|
}
|
|
649
|
+
size() {
|
|
650
|
+
return this.items.length;
|
|
651
|
+
}
|
|
639
652
|
};
|
|
640
653
|
|
|
641
654
|
// src/filter/ActiveTransitionFilter.ts
|
|
@@ -700,6 +713,17 @@ var ReentrancyError = class extends FinitaError {
|
|
|
700
713
|
}
|
|
701
714
|
};
|
|
702
715
|
|
|
716
|
+
// src/error/QueueLimitExceededError.ts
|
|
717
|
+
var QueueLimitExceededError = class extends FinitaError {
|
|
718
|
+
code = "queueLimitExceeded";
|
|
719
|
+
constructor(limit, eventName) {
|
|
720
|
+
super(
|
|
721
|
+
`${eventName === null ? "checkTransitions()" : `triggerEvent("${eventName}")`} rejected: the operation queue already holds ${limit} pending operation(s) (maxQueueLength = ${limit}).`
|
|
722
|
+
);
|
|
723
|
+
this.name = "QueueLimitExceededError";
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
703
727
|
// src/Statemachine.ts
|
|
704
728
|
var Statemachine = class {
|
|
705
729
|
subject;
|
|
@@ -710,11 +734,15 @@ var Statemachine = class {
|
|
|
710
734
|
lastState = null;
|
|
711
735
|
autoreleaseLock;
|
|
712
736
|
maxAutomaticHops;
|
|
737
|
+
maxQueueLength;
|
|
713
738
|
queue = new OperationQueue();
|
|
714
739
|
running = false;
|
|
740
|
+
idleWaiters = [];
|
|
715
741
|
inSyncCallback = false;
|
|
716
742
|
beforeObservers = [];
|
|
717
743
|
afterObservers = [];
|
|
744
|
+
onChainedOperationError;
|
|
745
|
+
onReleaseError;
|
|
718
746
|
constructor(subject, process, options = {}) {
|
|
719
747
|
this.subject = subject;
|
|
720
748
|
this.process = process;
|
|
@@ -729,6 +757,15 @@ var Statemachine = class {
|
|
|
729
757
|
);
|
|
730
758
|
}
|
|
731
759
|
this.maxAutomaticHops = hops;
|
|
760
|
+
const maxQueue = options.maxQueueLength ?? Infinity;
|
|
761
|
+
if (maxQueue !== Infinity && (!Number.isInteger(maxQueue) || maxQueue < 1)) {
|
|
762
|
+
throw new RangeError(
|
|
763
|
+
`maxQueueLength must be a positive integer; got ${String(options.maxQueueLength)}`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
this.maxQueueLength = maxQueue;
|
|
767
|
+
this.onChainedOperationError = options.onChainedOperationError;
|
|
768
|
+
this.onReleaseError = options.onReleaseError;
|
|
732
769
|
}
|
|
733
770
|
// --- public getters ---
|
|
734
771
|
getCurrentState() {
|
|
@@ -745,6 +782,7 @@ var Statemachine = class {
|
|
|
745
782
|
}
|
|
746
783
|
// --- public observer attach/detach ---
|
|
747
784
|
attachBefore(observer) {
|
|
785
|
+
if (this.beforeObservers.includes(observer)) return;
|
|
748
786
|
this.beforeObservers.push(observer);
|
|
749
787
|
}
|
|
750
788
|
detachBefore(observer) {
|
|
@@ -755,6 +793,7 @@ var Statemachine = class {
|
|
|
755
793
|
return this.beforeObservers;
|
|
756
794
|
}
|
|
757
795
|
attachAfter(observer) {
|
|
796
|
+
if (this.afterObservers.includes(observer)) return;
|
|
758
797
|
this.afterObservers.push(observer);
|
|
759
798
|
}
|
|
760
799
|
detachAfter(observer) {
|
|
@@ -793,6 +832,21 @@ var Statemachine = class {
|
|
|
793
832
|
this.enqueueOperation(null, context, resolve, reject);
|
|
794
833
|
});
|
|
795
834
|
}
|
|
835
|
+
/**
|
|
836
|
+
* Resolves once the operation queue is empty and the runner is idle —
|
|
837
|
+
* i.e. every operation enqueued so far, including operations chained via
|
|
838
|
+
* EnqueueContext.enqueue(), has completed. Resolves immediately if the
|
|
839
|
+
* machine is already idle. Note this is a quiescence point, not a
|
|
840
|
+
* receipt: work scheduled later (e.g. from a timer) starts a new drain.
|
|
841
|
+
*/
|
|
842
|
+
whenIdle() {
|
|
843
|
+
if (!this.running && this.queue.isEmpty()) {
|
|
844
|
+
return Promise.resolve();
|
|
845
|
+
}
|
|
846
|
+
return new Promise((resolve) => {
|
|
847
|
+
this.idleWaiters.push(resolve);
|
|
848
|
+
});
|
|
849
|
+
}
|
|
796
850
|
/** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
|
|
797
851
|
* the flag is cleared as soon as fn returns (before any promise it returned
|
|
798
852
|
* is awaited), so concurrent external callers are never affected. This
|
|
@@ -814,6 +868,9 @@ var Statemachine = class {
|
|
|
814
868
|
}
|
|
815
869
|
/** Single entry point to the operation queue — every enqueue kicks the runner. */
|
|
816
870
|
enqueueOperation(eventName, context, resolve, reject, ifStateName) {
|
|
871
|
+
if (this.queue.size() >= this.maxQueueLength) {
|
|
872
|
+
throw new QueueLimitExceededError(this.maxQueueLength, eventName);
|
|
873
|
+
}
|
|
817
874
|
this.queue.enqueue({
|
|
818
875
|
eventName,
|
|
819
876
|
context: context ?? /* @__PURE__ */ new Map(),
|
|
@@ -834,6 +891,11 @@ var Statemachine = class {
|
|
|
834
891
|
}
|
|
835
892
|
} finally {
|
|
836
893
|
this.running = false;
|
|
894
|
+
if (this.queue.isEmpty() && this.idleWaiters.length > 0) {
|
|
895
|
+
const waiters = this.idleWaiters;
|
|
896
|
+
this.idleWaiters = [];
|
|
897
|
+
for (const waiter of waiters) waiter();
|
|
898
|
+
}
|
|
837
899
|
}
|
|
838
900
|
}
|
|
839
901
|
async runOperation(op) {
|
|
@@ -859,6 +921,10 @@ var Statemachine = class {
|
|
|
859
921
|
try {
|
|
860
922
|
await this.mutex.releaseLock();
|
|
861
923
|
} catch (err) {
|
|
924
|
+
try {
|
|
925
|
+
this.onReleaseError?.(err);
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
862
928
|
if (!failure) failure = { err };
|
|
863
929
|
}
|
|
864
930
|
}
|
|
@@ -939,7 +1005,13 @@ var Statemachine = class {
|
|
|
939
1005
|
chainedCtx,
|
|
940
1006
|
() => {
|
|
941
1007
|
},
|
|
942
|
-
() => {
|
|
1008
|
+
(err) => {
|
|
1009
|
+
try {
|
|
1010
|
+
this.onChainedOperationError?.(err, {
|
|
1011
|
+
eventName: chainedEventName
|
|
1012
|
+
});
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
943
1015
|
},
|
|
944
1016
|
ifStateName
|
|
945
1017
|
);
|
|
@@ -1304,6 +1376,11 @@ var WeightTransition = class {
|
|
|
1304
1376
|
innerSelector;
|
|
1305
1377
|
epsilon;
|
|
1306
1378
|
constructor(innerSelector, epsilon = 1e-3) {
|
|
1379
|
+
if (!Number.isFinite(epsilon) || epsilon <= 0) {
|
|
1380
|
+
throw new RangeError(
|
|
1381
|
+
`WeightTransition epsilon must be a finite number greater than 0; got ${String(epsilon)}`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1307
1384
|
this.innerSelector = innerSelector ?? new OneOrNoneActiveTransition();
|
|
1308
1385
|
this.epsilon = epsilon;
|
|
1309
1386
|
}
|
|
@@ -1312,6 +1389,11 @@ var WeightTransition = class {
|
|
|
1312
1389
|
let maxWeight = Number.NEGATIVE_INFINITY;
|
|
1313
1390
|
for (const transition of all) {
|
|
1314
1391
|
const weight = transition.getWeight();
|
|
1392
|
+
if (!Number.isFinite(weight)) {
|
|
1393
|
+
throw new RangeError(
|
|
1394
|
+
`WeightTransition: transition weights must be finite numbers; got ${String(weight)}`
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1315
1397
|
if (weight > maxWeight) maxWeight = weight;
|
|
1316
1398
|
}
|
|
1317
1399
|
const best = all.filter(
|
|
@@ -1486,6 +1568,14 @@ function toMermaidId(name) {
|
|
|
1486
1568
|
function escapeMermaidLabel(str) {
|
|
1487
1569
|
return str.replace(/\\/g, "#92;").replace(/"/g, "#quot;");
|
|
1488
1570
|
}
|
|
1571
|
+
var VALID_DIRECTIONS = /* @__PURE__ */ new Set(["TB", "BT", "LR", "RL"]);
|
|
1572
|
+
function assertDirection(value, optionName) {
|
|
1573
|
+
if (!VALID_DIRECTIONS.has(value)) {
|
|
1574
|
+
throw new RangeError(
|
|
1575
|
+
`${optionName} must be one of "TB", "BT", "LR", "RL"; got ${JSON.stringify(value)}`
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1489
1579
|
var GraphBuilder = class {
|
|
1490
1580
|
nodes = /* @__PURE__ */ new Map();
|
|
1491
1581
|
edges = [];
|
|
@@ -1561,6 +1651,7 @@ var GraphBuilder = class {
|
|
|
1561
1651
|
toDot(options) {
|
|
1562
1652
|
const graph = this.getGraph();
|
|
1563
1653
|
const rankdir = options?.rankdir ?? "LR";
|
|
1654
|
+
assertDirection(rankdir, "rankdir");
|
|
1564
1655
|
const lines = [];
|
|
1565
1656
|
lines.push("digraph {");
|
|
1566
1657
|
lines.push(` rankdir=${rankdir};`);
|
|
@@ -1580,6 +1671,7 @@ var GraphBuilder = class {
|
|
|
1580
1671
|
toMermaid(options) {
|
|
1581
1672
|
const graph = this.getGraph();
|
|
1582
1673
|
const direction = options?.direction ?? "LR";
|
|
1674
|
+
assertDirection(direction, "direction");
|
|
1583
1675
|
const lines = [];
|
|
1584
1676
|
lines.push(`stateDiagram-v2`);
|
|
1585
1677
|
lines.push(` direction ${direction}`);
|
|
@@ -1634,6 +1726,7 @@ export {
|
|
|
1634
1726
|
ProcessBuilder,
|
|
1635
1727
|
ProcessFinalizedError,
|
|
1636
1728
|
ProcessNotFoundError,
|
|
1729
|
+
QueueLimitExceededError,
|
|
1637
1730
|
ReentrancyError,
|
|
1638
1731
|
ScoreTransition,
|
|
1639
1732
|
SingleProcessDetector,
|