@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.cjs
CHANGED
|
@@ -52,6 +52,7 @@ __export(index_exports, {
|
|
|
52
52
|
ProcessBuilder: () => ProcessBuilder,
|
|
53
53
|
ProcessFinalizedError: () => ProcessFinalizedError,
|
|
54
54
|
ProcessNotFoundError: () => ProcessNotFoundError,
|
|
55
|
+
QueueLimitExceededError: () => QueueLimitExceededError,
|
|
55
56
|
ReentrancyError: () => ReentrancyError,
|
|
56
57
|
ScoreTransition: () => ScoreTransition,
|
|
57
58
|
SingleProcessDetector: () => SingleProcessDetector,
|
|
@@ -256,6 +257,7 @@ var State = class {
|
|
|
256
257
|
throw new Error(`State "${this.name}" transitions already set`);
|
|
257
258
|
}
|
|
258
259
|
this._transitions = new Set(transitions);
|
|
260
|
+
Object.freeze(this);
|
|
259
261
|
}
|
|
260
262
|
getName() {
|
|
261
263
|
return this.name;
|
|
@@ -306,6 +308,7 @@ var Transition = class {
|
|
|
306
308
|
this.eventName = eventName;
|
|
307
309
|
this.condition = condition;
|
|
308
310
|
this.weight = weight;
|
|
311
|
+
Object.freeze(this);
|
|
309
312
|
}
|
|
310
313
|
getTargetState() {
|
|
311
314
|
return this.targetState;
|
|
@@ -440,12 +443,20 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
440
443
|
{ fromState, toState, conditionName }
|
|
441
444
|
);
|
|
442
445
|
}
|
|
446
|
+
const weight = options.weight ?? 1;
|
|
447
|
+
if (!Number.isFinite(weight)) {
|
|
448
|
+
throw new GraphValidationError(
|
|
449
|
+
"invalidTransitionWeight",
|
|
450
|
+
`addTransition from "${fromState}" to "${toState}": weight must be a finite number; got ${String(weight)}`,
|
|
451
|
+
{ fromState, toState, eventName, weight }
|
|
452
|
+
);
|
|
453
|
+
}
|
|
443
454
|
this.transitionSpecs.push({
|
|
444
455
|
fromState,
|
|
445
456
|
toState,
|
|
446
457
|
eventName,
|
|
447
458
|
condition: options.condition ?? null,
|
|
448
|
-
weight
|
|
459
|
+
weight
|
|
449
460
|
});
|
|
450
461
|
return this;
|
|
451
462
|
}
|
|
@@ -453,7 +464,6 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
453
464
|
if (this.built) {
|
|
454
465
|
throw new ProcessFinalizedError(this.processName);
|
|
455
466
|
}
|
|
456
|
-
this.built = true;
|
|
457
467
|
this.validateInitialState();
|
|
458
468
|
this.validateTransitionEndpoints();
|
|
459
469
|
this.validateNoConflictingDuplicates();
|
|
@@ -464,6 +474,7 @@ var ProcessBuilder = class _ProcessBuilder {
|
|
|
464
474
|
this.validateOrphans(finalStates, initialName);
|
|
465
475
|
}
|
|
466
476
|
const initialState = finalStates.get(initialName);
|
|
477
|
+
this.built = true;
|
|
467
478
|
return new Process(
|
|
468
479
|
INTERNAL_CONSTRUCTION_KEY,
|
|
469
480
|
this.processName,
|
|
@@ -708,6 +719,9 @@ var OperationQueue = class {
|
|
|
708
719
|
isEmpty() {
|
|
709
720
|
return this.items.length === 0;
|
|
710
721
|
}
|
|
722
|
+
size() {
|
|
723
|
+
return this.items.length;
|
|
724
|
+
}
|
|
711
725
|
};
|
|
712
726
|
|
|
713
727
|
// src/filter/ActiveTransitionFilter.ts
|
|
@@ -772,6 +786,17 @@ var ReentrancyError = class extends FinitaError {
|
|
|
772
786
|
}
|
|
773
787
|
};
|
|
774
788
|
|
|
789
|
+
// src/error/QueueLimitExceededError.ts
|
|
790
|
+
var QueueLimitExceededError = class extends FinitaError {
|
|
791
|
+
code = "queueLimitExceeded";
|
|
792
|
+
constructor(limit, eventName) {
|
|
793
|
+
super(
|
|
794
|
+
`${eventName === null ? "checkTransitions()" : `triggerEvent("${eventName}")`} rejected: the operation queue already holds ${limit} pending operation(s) (maxQueueLength = ${limit}).`
|
|
795
|
+
);
|
|
796
|
+
this.name = "QueueLimitExceededError";
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
|
|
775
800
|
// src/Statemachine.ts
|
|
776
801
|
var Statemachine = class {
|
|
777
802
|
subject;
|
|
@@ -782,11 +807,15 @@ var Statemachine = class {
|
|
|
782
807
|
lastState = null;
|
|
783
808
|
autoreleaseLock;
|
|
784
809
|
maxAutomaticHops;
|
|
810
|
+
maxQueueLength;
|
|
785
811
|
queue = new OperationQueue();
|
|
786
812
|
running = false;
|
|
813
|
+
idleWaiters = [];
|
|
787
814
|
inSyncCallback = false;
|
|
788
815
|
beforeObservers = [];
|
|
789
816
|
afterObservers = [];
|
|
817
|
+
onChainedOperationError;
|
|
818
|
+
onReleaseError;
|
|
790
819
|
constructor(subject, process, options = {}) {
|
|
791
820
|
this.subject = subject;
|
|
792
821
|
this.process = process;
|
|
@@ -801,6 +830,15 @@ var Statemachine = class {
|
|
|
801
830
|
);
|
|
802
831
|
}
|
|
803
832
|
this.maxAutomaticHops = hops;
|
|
833
|
+
const maxQueue = options.maxQueueLength ?? Infinity;
|
|
834
|
+
if (maxQueue !== Infinity && (!Number.isInteger(maxQueue) || maxQueue < 1)) {
|
|
835
|
+
throw new RangeError(
|
|
836
|
+
`maxQueueLength must be a positive integer; got ${String(options.maxQueueLength)}`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
this.maxQueueLength = maxQueue;
|
|
840
|
+
this.onChainedOperationError = options.onChainedOperationError;
|
|
841
|
+
this.onReleaseError = options.onReleaseError;
|
|
804
842
|
}
|
|
805
843
|
// --- public getters ---
|
|
806
844
|
getCurrentState() {
|
|
@@ -817,6 +855,7 @@ var Statemachine = class {
|
|
|
817
855
|
}
|
|
818
856
|
// --- public observer attach/detach ---
|
|
819
857
|
attachBefore(observer) {
|
|
858
|
+
if (this.beforeObservers.includes(observer)) return;
|
|
820
859
|
this.beforeObservers.push(observer);
|
|
821
860
|
}
|
|
822
861
|
detachBefore(observer) {
|
|
@@ -827,6 +866,7 @@ var Statemachine = class {
|
|
|
827
866
|
return this.beforeObservers;
|
|
828
867
|
}
|
|
829
868
|
attachAfter(observer) {
|
|
869
|
+
if (this.afterObservers.includes(observer)) return;
|
|
830
870
|
this.afterObservers.push(observer);
|
|
831
871
|
}
|
|
832
872
|
detachAfter(observer) {
|
|
@@ -865,6 +905,21 @@ var Statemachine = class {
|
|
|
865
905
|
this.enqueueOperation(null, context, resolve, reject);
|
|
866
906
|
});
|
|
867
907
|
}
|
|
908
|
+
/**
|
|
909
|
+
* Resolves once the operation queue is empty and the runner is idle —
|
|
910
|
+
* i.e. every operation enqueued so far, including operations chained via
|
|
911
|
+
* EnqueueContext.enqueue(), has completed. Resolves immediately if the
|
|
912
|
+
* machine is already idle. Note this is a quiescence point, not a
|
|
913
|
+
* receipt: work scheduled later (e.g. from a timer) starts a new drain.
|
|
914
|
+
*/
|
|
915
|
+
whenIdle() {
|
|
916
|
+
if (!this.running && this.queue.isEmpty()) {
|
|
917
|
+
return Promise.resolve();
|
|
918
|
+
}
|
|
919
|
+
return new Promise((resolve) => {
|
|
920
|
+
this.idleWaiters.push(resolve);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
868
923
|
/** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
|
|
869
924
|
* the flag is cleared as soon as fn returns (before any promise it returned
|
|
870
925
|
* is awaited), so concurrent external callers are never affected. This
|
|
@@ -886,6 +941,9 @@ var Statemachine = class {
|
|
|
886
941
|
}
|
|
887
942
|
/** Single entry point to the operation queue — every enqueue kicks the runner. */
|
|
888
943
|
enqueueOperation(eventName, context, resolve, reject, ifStateName) {
|
|
944
|
+
if (this.queue.size() >= this.maxQueueLength) {
|
|
945
|
+
throw new QueueLimitExceededError(this.maxQueueLength, eventName);
|
|
946
|
+
}
|
|
889
947
|
this.queue.enqueue({
|
|
890
948
|
eventName,
|
|
891
949
|
context: context ?? /* @__PURE__ */ new Map(),
|
|
@@ -906,6 +964,11 @@ var Statemachine = class {
|
|
|
906
964
|
}
|
|
907
965
|
} finally {
|
|
908
966
|
this.running = false;
|
|
967
|
+
if (this.queue.isEmpty() && this.idleWaiters.length > 0) {
|
|
968
|
+
const waiters = this.idleWaiters;
|
|
969
|
+
this.idleWaiters = [];
|
|
970
|
+
for (const waiter of waiters) waiter();
|
|
971
|
+
}
|
|
909
972
|
}
|
|
910
973
|
}
|
|
911
974
|
async runOperation(op) {
|
|
@@ -931,6 +994,10 @@ var Statemachine = class {
|
|
|
931
994
|
try {
|
|
932
995
|
await this.mutex.releaseLock();
|
|
933
996
|
} catch (err) {
|
|
997
|
+
try {
|
|
998
|
+
this.onReleaseError?.(err);
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
934
1001
|
if (!failure) failure = { err };
|
|
935
1002
|
}
|
|
936
1003
|
}
|
|
@@ -1011,7 +1078,13 @@ var Statemachine = class {
|
|
|
1011
1078
|
chainedCtx,
|
|
1012
1079
|
() => {
|
|
1013
1080
|
},
|
|
1014
|
-
() => {
|
|
1081
|
+
(err) => {
|
|
1082
|
+
try {
|
|
1083
|
+
this.onChainedOperationError?.(err, {
|
|
1084
|
+
eventName: chainedEventName
|
|
1085
|
+
});
|
|
1086
|
+
} catch {
|
|
1087
|
+
}
|
|
1015
1088
|
},
|
|
1016
1089
|
ifStateName
|
|
1017
1090
|
);
|
|
@@ -1376,6 +1449,11 @@ var WeightTransition = class {
|
|
|
1376
1449
|
innerSelector;
|
|
1377
1450
|
epsilon;
|
|
1378
1451
|
constructor(innerSelector, epsilon = 1e-3) {
|
|
1452
|
+
if (!Number.isFinite(epsilon) || epsilon <= 0) {
|
|
1453
|
+
throw new RangeError(
|
|
1454
|
+
`WeightTransition epsilon must be a finite number greater than 0; got ${String(epsilon)}`
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1379
1457
|
this.innerSelector = innerSelector ?? new OneOrNoneActiveTransition();
|
|
1380
1458
|
this.epsilon = epsilon;
|
|
1381
1459
|
}
|
|
@@ -1384,6 +1462,11 @@ var WeightTransition = class {
|
|
|
1384
1462
|
let maxWeight = Number.NEGATIVE_INFINITY;
|
|
1385
1463
|
for (const transition of all) {
|
|
1386
1464
|
const weight = transition.getWeight();
|
|
1465
|
+
if (!Number.isFinite(weight)) {
|
|
1466
|
+
throw new RangeError(
|
|
1467
|
+
`WeightTransition: transition weights must be finite numbers; got ${String(weight)}`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1387
1470
|
if (weight > maxWeight) maxWeight = weight;
|
|
1388
1471
|
}
|
|
1389
1472
|
const best = all.filter(
|
|
@@ -1558,6 +1641,14 @@ function toMermaidId(name) {
|
|
|
1558
1641
|
function escapeMermaidLabel(str) {
|
|
1559
1642
|
return str.replace(/\\/g, "#92;").replace(/"/g, "#quot;");
|
|
1560
1643
|
}
|
|
1644
|
+
var VALID_DIRECTIONS = /* @__PURE__ */ new Set(["TB", "BT", "LR", "RL"]);
|
|
1645
|
+
function assertDirection(value, optionName) {
|
|
1646
|
+
if (!VALID_DIRECTIONS.has(value)) {
|
|
1647
|
+
throw new RangeError(
|
|
1648
|
+
`${optionName} must be one of "TB", "BT", "LR", "RL"; got ${JSON.stringify(value)}`
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1561
1652
|
var GraphBuilder = class {
|
|
1562
1653
|
nodes = /* @__PURE__ */ new Map();
|
|
1563
1654
|
edges = [];
|
|
@@ -1633,6 +1724,7 @@ var GraphBuilder = class {
|
|
|
1633
1724
|
toDot(options) {
|
|
1634
1725
|
const graph = this.getGraph();
|
|
1635
1726
|
const rankdir = options?.rankdir ?? "LR";
|
|
1727
|
+
assertDirection(rankdir, "rankdir");
|
|
1636
1728
|
const lines = [];
|
|
1637
1729
|
lines.push("digraph {");
|
|
1638
1730
|
lines.push(` rankdir=${rankdir};`);
|
|
@@ -1652,6 +1744,7 @@ var GraphBuilder = class {
|
|
|
1652
1744
|
toMermaid(options) {
|
|
1653
1745
|
const graph = this.getGraph();
|
|
1654
1746
|
const direction = options?.direction ?? "LR";
|
|
1747
|
+
assertDirection(direction, "direction");
|
|
1655
1748
|
const lines = [];
|
|
1656
1749
|
lines.push(`stateDiagram-v2`);
|
|
1657
1750
|
lines.push(` direction ${direction}`);
|
|
@@ -1707,6 +1800,7 @@ var GraphBuilder = class {
|
|
|
1707
1800
|
ProcessBuilder,
|
|
1708
1801
|
ProcessFinalizedError,
|
|
1709
1802
|
ProcessNotFoundError,
|
|
1803
|
+
QueueLimitExceededError,
|
|
1710
1804
|
ReentrancyError,
|
|
1711
1805
|
ScoreTransition,
|
|
1712
1806
|
SingleProcessDetector,
|