@camcima/finita 3.0.1 → 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/README.md +1 -1
- package/dist/index.cjs +340 -211
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +179 -34
- package/dist/index.d.ts +179 -34
- package/dist/index.js +338 -211
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3,23 +3,22 @@ var Event = class {
|
|
|
3
3
|
name;
|
|
4
4
|
observers = /* @__PURE__ */ new Set();
|
|
5
5
|
metadata = /* @__PURE__ */ new Map();
|
|
6
|
-
invokeArgs = [];
|
|
7
6
|
constructor(name) {
|
|
8
7
|
this.name = name;
|
|
9
8
|
}
|
|
10
9
|
getName() {
|
|
11
10
|
return this.name;
|
|
12
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated Always returns []. Invoke args are now passed directly to
|
|
14
|
+
* Observer.update — reading them from the event was racy when one
|
|
15
|
+
* Process served multiple Statemachines.
|
|
16
|
+
*/
|
|
13
17
|
getInvokeArgs() {
|
|
14
|
-
return
|
|
18
|
+
return [];
|
|
15
19
|
}
|
|
16
20
|
async invoke(...args) {
|
|
17
|
-
this.
|
|
18
|
-
try {
|
|
19
|
-
await this.notify();
|
|
20
|
-
} finally {
|
|
21
|
-
this.invokeArgs = [];
|
|
22
|
-
}
|
|
21
|
+
await this.notify(args);
|
|
23
22
|
}
|
|
24
23
|
attach(observer) {
|
|
25
24
|
this.observers.add(observer);
|
|
@@ -27,9 +26,9 @@ var Event = class {
|
|
|
27
26
|
detach(observer) {
|
|
28
27
|
this.observers.delete(observer);
|
|
29
28
|
}
|
|
30
|
-
async notify() {
|
|
31
|
-
for (const observer of this.observers) {
|
|
32
|
-
await observer.update(this);
|
|
29
|
+
async notify(args) {
|
|
30
|
+
for (const observer of [...this.observers]) {
|
|
31
|
+
await observer.update(this, args);
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
34
|
getObservers() {
|
|
@@ -185,6 +184,7 @@ var State = class {
|
|
|
185
184
|
throw new Error(`State "${this.name}" transitions already set`);
|
|
186
185
|
}
|
|
187
186
|
this._transitions = new Set(transitions);
|
|
187
|
+
Object.freeze(this);
|
|
188
188
|
}
|
|
189
189
|
getName() {
|
|
190
190
|
return this.name;
|
|
@@ -235,6 +235,7 @@ var Transition = class {
|
|
|
235
235
|
this.eventName = eventName;
|
|
236
236
|
this.condition = condition;
|
|
237
237
|
this.weight = weight;
|
|
238
|
+
Object.freeze(this);
|
|
238
239
|
}
|
|
239
240
|
getTargetState() {
|
|
240
241
|
return this.targetState;
|
|
@@ -311,8 +312,9 @@ var DuplicateTransitionError = class extends FinitaError {
|
|
|
311
312
|
const eventLabel = conflict.eventName ?? "<automatic>";
|
|
312
313
|
const existing = conflict.existingConditionName ?? "<no condition>";
|
|
313
314
|
const incoming = conflict.newConditionName ?? "<no condition>";
|
|
315
|
+
const weightInfo = conflict.existingWeight !== void 0 && conflict.newWeight !== void 0 && conflict.existingWeight !== conflict.newWeight ? `, existing weight ${conflict.existingWeight} vs new weight ${conflict.newWeight}` : "";
|
|
314
316
|
super(
|
|
315
|
-
`Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"`
|
|
317
|
+
`Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"${weightInfo}`
|
|
316
318
|
);
|
|
317
319
|
this.name = "DuplicateTransitionError";
|
|
318
320
|
this.conflict = Object.freeze({ ...conflict });
|
|
@@ -320,7 +322,7 @@ var DuplicateTransitionError = class extends FinitaError {
|
|
|
320
322
|
};
|
|
321
323
|
|
|
322
324
|
// src/ProcessBuilder.ts
|
|
323
|
-
var ProcessBuilder = class {
|
|
325
|
+
var ProcessBuilder = class _ProcessBuilder {
|
|
324
326
|
processName;
|
|
325
327
|
stateSpecs = /* @__PURE__ */ new Map();
|
|
326
328
|
transitionSpecs = [];
|
|
@@ -335,6 +337,9 @@ var ProcessBuilder = class {
|
|
|
335
337
|
if (this.stateSpecs.has(name)) {
|
|
336
338
|
throw new DuplicateStateError(name);
|
|
337
339
|
}
|
|
340
|
+
this.validateName("invalidStateName", name, `addState("${name}")`, {
|
|
341
|
+
stateName: name
|
|
342
|
+
});
|
|
338
343
|
this.stateSpecs.set(name, {
|
|
339
344
|
name,
|
|
340
345
|
initial: options.initial === true,
|
|
@@ -348,32 +353,37 @@ var ProcessBuilder = class {
|
|
|
348
353
|
}
|
|
349
354
|
let eventName = null;
|
|
350
355
|
if (options.event !== void 0) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
eventName = raw;
|
|
356
|
+
this.validateName(
|
|
357
|
+
"invalidEventName",
|
|
358
|
+
options.event,
|
|
359
|
+
`addTransition called with an invalid event name from "${fromState}" to "${toState}"`,
|
|
360
|
+
{ fromState, toState, eventName: options.event }
|
|
361
|
+
);
|
|
362
|
+
eventName = options.event;
|
|
360
363
|
}
|
|
361
364
|
if (options.condition) {
|
|
362
365
|
const conditionName = options.condition.getName();
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
366
|
+
this.validateName(
|
|
367
|
+
"invalidConditionName",
|
|
368
|
+
conditionName,
|
|
369
|
+
`addTransition called with an invalid condition name from "${fromState}" to "${toState}"`,
|
|
370
|
+
{ fromState, toState, conditionName }
|
|
371
|
+
);
|
|
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
|
+
);
|
|
370
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 {
|
|
|
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 {
|
|
|
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,
|
|
@@ -400,6 +410,16 @@ var ProcessBuilder = class {
|
|
|
400
410
|
);
|
|
401
411
|
}
|
|
402
412
|
// --- private helpers ---
|
|
413
|
+
/** One name rule for every named entity: non-empty, no leading/trailing whitespace. */
|
|
414
|
+
validateName(code, raw, description, details) {
|
|
415
|
+
if (raw.trim() === "" || raw !== raw.trim()) {
|
|
416
|
+
throw new GraphValidationError(
|
|
417
|
+
code,
|
|
418
|
+
`${description}: name ${JSON.stringify(raw)} is empty or whitespace-padded`,
|
|
419
|
+
details
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
403
423
|
validateInitialState() {
|
|
404
424
|
const initials = Array.from(this.stateSpecs.values()).filter(
|
|
405
425
|
(s) => s.initial
|
|
@@ -451,22 +471,29 @@ var ProcessBuilder = class {
|
|
|
451
471
|
}
|
|
452
472
|
}
|
|
453
473
|
}
|
|
474
|
+
/** Transition identity: (fromState, eventName, toState). Used by both the
|
|
475
|
+
* conflict check and the build-time dedup — keep them in lockstep. */
|
|
476
|
+
static transitionKey(t) {
|
|
477
|
+
return `${t.fromState}\0${t.eventName ?? ""}\0${t.toState}`;
|
|
478
|
+
}
|
|
454
479
|
validateNoConflictingDuplicates() {
|
|
455
480
|
const seen = /* @__PURE__ */ new Map();
|
|
456
481
|
for (const t of this.transitionSpecs) {
|
|
457
|
-
const key =
|
|
482
|
+
const key = _ProcessBuilder.transitionKey(t);
|
|
458
483
|
const existing = seen.get(key);
|
|
459
484
|
if (!existing) {
|
|
460
485
|
seen.set(key, t);
|
|
461
486
|
continue;
|
|
462
487
|
}
|
|
463
|
-
if (existing.condition !== t.condition) {
|
|
488
|
+
if (existing.condition !== t.condition || existing.weight !== t.weight) {
|
|
464
489
|
throw new DuplicateTransitionError({
|
|
465
490
|
fromState: t.fromState,
|
|
466
491
|
toState: t.toState,
|
|
467
492
|
eventName: t.eventName,
|
|
468
493
|
existingConditionName: existing.condition ? existing.condition.getName() : null,
|
|
469
|
-
newConditionName: t.condition ? t.condition.getName() : null
|
|
494
|
+
newConditionName: t.condition ? t.condition.getName() : null,
|
|
495
|
+
existingWeight: existing.weight,
|
|
496
|
+
newWeight: t.weight
|
|
470
497
|
});
|
|
471
498
|
}
|
|
472
499
|
}
|
|
@@ -510,23 +537,12 @@ var ProcessBuilder = class {
|
|
|
510
537
|
);
|
|
511
538
|
}
|
|
512
539
|
const dedupSeen = /* @__PURE__ */ new Set();
|
|
513
|
-
const conditionId = /* @__PURE__ */ new Map();
|
|
514
|
-
let nextConditionId = 0;
|
|
515
|
-
const idForCondition = (cond) => {
|
|
516
|
-
if (cond === null) return "";
|
|
517
|
-
let id = conditionId.get(cond);
|
|
518
|
-
if (id === void 0) {
|
|
519
|
-
id = ++nextConditionId;
|
|
520
|
-
conditionId.set(cond, id);
|
|
521
|
-
}
|
|
522
|
-
return String(id);
|
|
523
|
-
};
|
|
524
540
|
const transitionsByState = /* @__PURE__ */ new Map();
|
|
525
541
|
for (const spec of this.stateSpecs.values()) {
|
|
526
542
|
transitionsByState.set(spec.name, []);
|
|
527
543
|
}
|
|
528
544
|
for (const tSpec of this.transitionSpecs) {
|
|
529
|
-
const dedupKey =
|
|
545
|
+
const dedupKey = _ProcessBuilder.transitionKey(tSpec);
|
|
530
546
|
if (dedupSeen.has(dedupKey)) continue;
|
|
531
547
|
dedupSeen.add(dedupKey);
|
|
532
548
|
const targetState = built.get(tSpec.toState);
|
|
@@ -618,27 +634,6 @@ var NullMutex = class {
|
|
|
618
634
|
}
|
|
619
635
|
};
|
|
620
636
|
|
|
621
|
-
// src/internal/Dispatcher.ts
|
|
622
|
-
var Dispatcher = class {
|
|
623
|
-
commands = [];
|
|
624
|
-
ready = false;
|
|
625
|
-
dispatch(event, args = []) {
|
|
626
|
-
if (this.ready) {
|
|
627
|
-
throw new Error("Was already invoked!");
|
|
628
|
-
}
|
|
629
|
-
this.commands.push({ event, args });
|
|
630
|
-
}
|
|
631
|
-
async invoke() {
|
|
632
|
-
if (this.ready) {
|
|
633
|
-
throw new Error("Was already invoked!");
|
|
634
|
-
}
|
|
635
|
-
for (const { event, args } of this.commands) {
|
|
636
|
-
await event.invoke(...args);
|
|
637
|
-
}
|
|
638
|
-
this.ready = true;
|
|
639
|
-
}
|
|
640
|
-
};
|
|
641
|
-
|
|
642
637
|
// src/internal/OperationQueue.ts
|
|
643
638
|
var OperationQueue = class {
|
|
644
639
|
items = [];
|
|
@@ -651,14 +646,18 @@ var OperationQueue = class {
|
|
|
651
646
|
isEmpty() {
|
|
652
647
|
return this.items.length === 0;
|
|
653
648
|
}
|
|
649
|
+
size() {
|
|
650
|
+
return this.items.length;
|
|
651
|
+
}
|
|
654
652
|
};
|
|
655
653
|
|
|
656
654
|
// src/filter/ActiveTransitionFilter.ts
|
|
657
655
|
var ActiveTransitionFilter = class {
|
|
658
|
-
static async filter(transitions, subject, context, event) {
|
|
656
|
+
static async filter(transitions, subject, context, event, wrap) {
|
|
657
|
+
const run = wrap ?? ((fn) => fn());
|
|
659
658
|
const active = [];
|
|
660
659
|
for (const transition of transitions) {
|
|
661
|
-
if (await transition.isActive(subject, context, event)) {
|
|
660
|
+
if (await run(() => transition.isActive(subject, context, event))) {
|
|
662
661
|
active.push(transition);
|
|
663
662
|
}
|
|
664
663
|
}
|
|
@@ -691,16 +690,37 @@ var LockCanNotBeAcquiredError = class extends FinitaError {
|
|
|
691
690
|
// src/error/AutomaticTransitionCycleError.ts
|
|
692
691
|
var AutomaticTransitionCycleError = class extends FinitaError {
|
|
693
692
|
code = "automaticTransitionCycle";
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
constructor(
|
|
697
|
-
const visited = Array.from(visitedStateNames);
|
|
693
|
+
stateName;
|
|
694
|
+
hopLimit;
|
|
695
|
+
constructor(stateName, hopLimit) {
|
|
698
696
|
super(
|
|
699
|
-
`Automatic
|
|
697
|
+
`Automatic transitions exceeded ${hopLimit} hops without reaching a quiescent state (last target: "${stateName}") \u2014 the graph is likely looping forever. Raise maxAutomaticHops if the loop is legitimate and bounded. Transitions committed before this error are NOT rolled back.`
|
|
700
698
|
);
|
|
701
699
|
this.name = "AutomaticTransitionCycleError";
|
|
702
|
-
this.
|
|
703
|
-
this.
|
|
700
|
+
this.stateName = stateName;
|
|
701
|
+
this.hopLimit = hopLimit;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// src/error/ReentrancyError.ts
|
|
706
|
+
var ReentrancyError = class extends FinitaError {
|
|
707
|
+
code = "reentrancy";
|
|
708
|
+
constructor(operation) {
|
|
709
|
+
super(
|
|
710
|
+
`${operation} was called from inside an observer or condition of the same Statemachine. Awaiting it would deadlock: the machine runs one operation at a time and the runner is blocked on your callback. Where applicable, use the EnqueueContext passed to after-observers to chain events instead; from other callbacks, defer the call out of the synchronous path, e.g. queueMicrotask(() => sm.triggerEvent(...)).`
|
|
711
|
+
);
|
|
712
|
+
this.name = "ReentrancyError";
|
|
713
|
+
}
|
|
714
|
+
};
|
|
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";
|
|
704
724
|
}
|
|
705
725
|
};
|
|
706
726
|
|
|
@@ -713,17 +733,39 @@ var Statemachine = class {
|
|
|
713
733
|
currentState;
|
|
714
734
|
lastState = null;
|
|
715
735
|
autoreleaseLock;
|
|
736
|
+
maxAutomaticHops;
|
|
737
|
+
maxQueueLength;
|
|
716
738
|
queue = new OperationQueue();
|
|
717
739
|
running = false;
|
|
740
|
+
idleWaiters = [];
|
|
741
|
+
inSyncCallback = false;
|
|
718
742
|
beforeObservers = [];
|
|
719
743
|
afterObservers = [];
|
|
744
|
+
onChainedOperationError;
|
|
745
|
+
onReleaseError;
|
|
720
746
|
constructor(subject, process, options = {}) {
|
|
721
747
|
this.subject = subject;
|
|
722
748
|
this.process = process;
|
|
723
|
-
this.currentState = options.initialStateName ? process.getState(options.initialStateName) : process.getInitialState();
|
|
749
|
+
this.currentState = options.initialStateName !== void 0 ? process.getState(options.initialStateName) : process.getInitialState();
|
|
724
750
|
this.transitionSelector = options.transitionSelector ?? new OneOrNoneActiveTransition();
|
|
725
751
|
this.mutex = options.mutex ?? new NullMutex();
|
|
726
752
|
this.autoreleaseLock = options.autoreleaseLock ?? true;
|
|
753
|
+
const hops = options.maxAutomaticHops ?? 100;
|
|
754
|
+
if (!Number.isInteger(hops) || hops < 1) {
|
|
755
|
+
throw new RangeError(
|
|
756
|
+
`maxAutomaticHops must be a positive integer; got ${String(options.maxAutomaticHops)}`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
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;
|
|
727
769
|
}
|
|
728
770
|
// --- public getters ---
|
|
729
771
|
getCurrentState() {
|
|
@@ -740,6 +782,7 @@ var Statemachine = class {
|
|
|
740
782
|
}
|
|
741
783
|
// --- public observer attach/detach ---
|
|
742
784
|
attachBefore(observer) {
|
|
785
|
+
if (this.beforeObservers.includes(observer)) return;
|
|
743
786
|
this.beforeObservers.push(observer);
|
|
744
787
|
}
|
|
745
788
|
detachBefore(observer) {
|
|
@@ -750,6 +793,7 @@ var Statemachine = class {
|
|
|
750
793
|
return this.beforeObservers;
|
|
751
794
|
}
|
|
752
795
|
attachAfter(observer) {
|
|
796
|
+
if (this.afterObservers.includes(observer)) return;
|
|
753
797
|
this.afterObservers.push(observer);
|
|
754
798
|
}
|
|
755
799
|
detachAfter(observer) {
|
|
@@ -778,28 +822,64 @@ var Statemachine = class {
|
|
|
778
822
|
// --- public top-level operations ---
|
|
779
823
|
triggerEvent(name, context) {
|
|
780
824
|
return new Promise((resolve, reject) => {
|
|
781
|
-
this.
|
|
782
|
-
|
|
783
|
-
eventName: name,
|
|
784
|
-
context: context ?? /* @__PURE__ */ new Map(),
|
|
785
|
-
resolve,
|
|
786
|
-
reject
|
|
787
|
-
});
|
|
788
|
-
void this.runIfIdle();
|
|
825
|
+
this.assertNotReentrant(`triggerEvent("${name}")`);
|
|
826
|
+
this.enqueueOperation(name, context, resolve, reject);
|
|
789
827
|
});
|
|
790
828
|
}
|
|
791
829
|
checkTransitions(context) {
|
|
792
830
|
return new Promise((resolve, reject) => {
|
|
793
|
-
this.
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
831
|
+
this.assertNotReentrant("checkTransitions()");
|
|
832
|
+
this.enqueueOperation(null, context, resolve, reject);
|
|
833
|
+
});
|
|
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);
|
|
801
848
|
});
|
|
802
849
|
}
|
|
850
|
+
/** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
|
|
851
|
+
* the flag is cleared as soon as fn returns (before any promise it returned
|
|
852
|
+
* is awaited), so concurrent external callers are never affected. This
|
|
853
|
+
* catches triggerEvent/checkTransitions calls made before a callback's first
|
|
854
|
+
* await; calls made after a prior await are not detectable without
|
|
855
|
+
* AsyncLocalStorage (Node-only) and will still deadlock — a documented gap. */
|
|
856
|
+
guardSync(fn) {
|
|
857
|
+
this.inSyncCallback = true;
|
|
858
|
+
try {
|
|
859
|
+
return fn();
|
|
860
|
+
} finally {
|
|
861
|
+
this.inSyncCallback = false;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
assertNotReentrant(operation) {
|
|
865
|
+
if (this.inSyncCallback) {
|
|
866
|
+
throw new ReentrancyError(operation);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/** Single entry point to the operation queue — every enqueue kicks the runner. */
|
|
870
|
+
enqueueOperation(eventName, context, resolve, reject, ifStateName) {
|
|
871
|
+
if (this.queue.size() >= this.maxQueueLength) {
|
|
872
|
+
throw new QueueLimitExceededError(this.maxQueueLength, eventName);
|
|
873
|
+
}
|
|
874
|
+
this.queue.enqueue({
|
|
875
|
+
eventName,
|
|
876
|
+
context: context ?? /* @__PURE__ */ new Map(),
|
|
877
|
+
ifStateName,
|
|
878
|
+
resolve,
|
|
879
|
+
reject
|
|
880
|
+
});
|
|
881
|
+
void this.runIfIdle();
|
|
882
|
+
}
|
|
803
883
|
// --- internal runner ---
|
|
804
884
|
async runIfIdle() {
|
|
805
885
|
if (this.running) return;
|
|
@@ -811,10 +891,20 @@ var Statemachine = class {
|
|
|
811
891
|
}
|
|
812
892
|
} finally {
|
|
813
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
|
+
}
|
|
814
899
|
}
|
|
815
900
|
}
|
|
816
901
|
async runOperation(op) {
|
|
902
|
+
if (op.ifStateName !== void 0 && this.currentState.getName() !== op.ifStateName) {
|
|
903
|
+
op.resolve();
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
817
906
|
let acquiredHere = false;
|
|
907
|
+
let failure = null;
|
|
818
908
|
try {
|
|
819
909
|
if (!this.mutex.isAcquired()) {
|
|
820
910
|
if (!await this.mutex.acquireLock()) {
|
|
@@ -822,19 +912,28 @@ var Statemachine = class {
|
|
|
822
912
|
}
|
|
823
913
|
acquiredHere = true;
|
|
824
914
|
}
|
|
825
|
-
const event = op.
|
|
915
|
+
const event = op.eventName !== null ? this.resolveEvent(op.eventName) : null;
|
|
826
916
|
await this.processOperation(event, op.context);
|
|
827
|
-
op.resolve();
|
|
828
917
|
} catch (err) {
|
|
829
|
-
|
|
918
|
+
failure = { err };
|
|
830
919
|
} finally {
|
|
831
920
|
if (acquiredHere && this.autoreleaseLock) {
|
|
832
921
|
try {
|
|
833
922
|
await this.mutex.releaseLock();
|
|
834
|
-
} catch {
|
|
923
|
+
} catch (err) {
|
|
924
|
+
try {
|
|
925
|
+
this.onReleaseError?.(err);
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
if (!failure) failure = { err };
|
|
835
929
|
}
|
|
836
930
|
}
|
|
837
931
|
}
|
|
932
|
+
if (failure) {
|
|
933
|
+
op.reject(failure.err);
|
|
934
|
+
} else {
|
|
935
|
+
op.resolve();
|
|
936
|
+
}
|
|
838
937
|
}
|
|
839
938
|
resolveEvent(name) {
|
|
840
939
|
if (!this.currentState.hasEvent(name)) {
|
|
@@ -849,11 +948,13 @@ var Statemachine = class {
|
|
|
849
948
|
*/
|
|
850
949
|
async processOperation(initialEvent, context) {
|
|
851
950
|
let event = initialEvent;
|
|
852
|
-
|
|
951
|
+
let automaticHops = 0;
|
|
853
952
|
if (event) {
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
953
|
+
const userEvent = event;
|
|
954
|
+
const invokeArgs = [this.subject, context];
|
|
955
|
+
for (const observer of [...userEvent.getObservers()]) {
|
|
956
|
+
await this.guardSync(() => observer.update(userEvent, invokeArgs));
|
|
957
|
+
}
|
|
857
958
|
}
|
|
858
959
|
while (true) {
|
|
859
960
|
const transitions = this.currentState.getTransitions();
|
|
@@ -861,26 +962,28 @@ var Statemachine = class {
|
|
|
861
962
|
transitions,
|
|
862
963
|
this.subject,
|
|
863
964
|
context,
|
|
864
|
-
event ?? void 0
|
|
965
|
+
event ?? void 0,
|
|
966
|
+
(fn) => this.guardSync(fn)
|
|
865
967
|
);
|
|
866
|
-
const selected = this.
|
|
867
|
-
active
|
|
968
|
+
const selected = this.guardSync(
|
|
969
|
+
() => this.transitionSelector.selectTransition(active)
|
|
868
970
|
);
|
|
869
971
|
if (!selected) {
|
|
870
972
|
return;
|
|
871
973
|
}
|
|
872
974
|
const target = selected.getTargetState();
|
|
873
975
|
if (selected.getEventName() === null) {
|
|
874
|
-
|
|
875
|
-
if (
|
|
976
|
+
automaticHops += 1;
|
|
977
|
+
if (automaticHops > this.maxAutomaticHops) {
|
|
876
978
|
throw new AutomaticTransitionCycleError(
|
|
877
979
|
target.getName(),
|
|
878
|
-
|
|
980
|
+
this.maxAutomaticHops
|
|
879
981
|
);
|
|
880
982
|
}
|
|
881
983
|
}
|
|
882
984
|
if (this.currentState !== target) {
|
|
883
|
-
const
|
|
985
|
+
const frame = Object.freeze({
|
|
986
|
+
subject: this.subject,
|
|
884
987
|
fromState: this.currentState,
|
|
885
988
|
toState: target,
|
|
886
989
|
transition: selected,
|
|
@@ -890,39 +993,34 @@ var Statemachine = class {
|
|
|
890
993
|
timestamp: Date.now(),
|
|
891
994
|
machineName: this.process.getName()
|
|
892
995
|
});
|
|
893
|
-
for (const observer of this.beforeObservers) {
|
|
894
|
-
await observer.notify(
|
|
996
|
+
for (const observer of [...this.beforeObservers]) {
|
|
997
|
+
await this.guardSync(() => observer.notify(frame));
|
|
895
998
|
}
|
|
896
|
-
|
|
897
|
-
this.lastState = fromState;
|
|
999
|
+
this.lastState = this.currentState;
|
|
898
1000
|
this.currentState = target;
|
|
899
|
-
const committedFrame = Object.freeze({
|
|
900
|
-
fromState,
|
|
901
|
-
toState: target,
|
|
902
|
-
transition: selected,
|
|
903
|
-
event,
|
|
904
|
-
condition: selected.getCondition(),
|
|
905
|
-
context: this.readonlyContext(context),
|
|
906
|
-
timestamp: proposedFrame.timestamp,
|
|
907
|
-
machineName: this.process.getName()
|
|
908
|
-
});
|
|
909
1001
|
const enqueueCtx = {
|
|
910
|
-
enqueue: (chainedEventName, chainedCtx) => {
|
|
911
|
-
this.
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1002
|
+
enqueue: (chainedEventName, chainedCtx, ifStateName) => {
|
|
1003
|
+
this.enqueueOperation(
|
|
1004
|
+
chainedEventName,
|
|
1005
|
+
chainedCtx,
|
|
1006
|
+
() => {
|
|
1007
|
+
},
|
|
1008
|
+
(err) => {
|
|
1009
|
+
try {
|
|
1010
|
+
this.onChainedOperationError?.(err, {
|
|
1011
|
+
eventName: chainedEventName
|
|
1012
|
+
});
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
916
1015
|
},
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
});
|
|
1016
|
+
ifStateName
|
|
1017
|
+
);
|
|
920
1018
|
}
|
|
921
1019
|
};
|
|
922
1020
|
const errors = [];
|
|
923
|
-
for (const observer of this.afterObservers) {
|
|
1021
|
+
for (const observer of [...this.afterObservers]) {
|
|
924
1022
|
try {
|
|
925
|
-
await observer.notify(
|
|
1023
|
+
await this.guardSync(() => observer.notify(frame, enqueueCtx));
|
|
926
1024
|
} catch (err) {
|
|
927
1025
|
errors.push(err);
|
|
928
1026
|
}
|
|
@@ -989,6 +1087,23 @@ var CallbackCondition = class {
|
|
|
989
1087
|
}
|
|
990
1088
|
};
|
|
991
1089
|
|
|
1090
|
+
// src/error/InvalidSubjectError.ts
|
|
1091
|
+
var InvalidSubjectError = class extends FinitaError {
|
|
1092
|
+
code = "invalidSubject";
|
|
1093
|
+
expectedInterface;
|
|
1094
|
+
missingMembers;
|
|
1095
|
+
constructor(expectedInterface, missingMembers) {
|
|
1096
|
+
const members = Array.from(missingMembers);
|
|
1097
|
+
const memberList = members.map((m) => `"${m}"`).join(", ");
|
|
1098
|
+
super(
|
|
1099
|
+
`Subject does not satisfy ${expectedInterface}; missing member(s): ${memberList || "(unknown)"}`
|
|
1100
|
+
);
|
|
1101
|
+
this.name = "InvalidSubjectError";
|
|
1102
|
+
this.expectedInterface = expectedInterface;
|
|
1103
|
+
this.missingMembers = Object.freeze([...members]);
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
|
|
992
1107
|
// src/condition/Timeout.ts
|
|
993
1108
|
function isLastStateHasChangedDate(obj) {
|
|
994
1109
|
return typeof obj === "object" && obj !== null && "getLastStateHasChangedDate" in obj && typeof obj.getLastStateHasChangedDate === "function";
|
|
@@ -1007,30 +1122,40 @@ var Timeout = class {
|
|
|
1007
1122
|
if (isLastStateHasChangedDate(subject)) {
|
|
1008
1123
|
return subject.getLastStateHasChangedDate();
|
|
1009
1124
|
}
|
|
1010
|
-
throw new
|
|
1125
|
+
throw new InvalidSubjectError("LastStateHasChangedDateInterface", [
|
|
1126
|
+
"getLastStateHasChangedDate"
|
|
1127
|
+
]);
|
|
1011
1128
|
}
|
|
1012
1129
|
checkCondition(subject, context) {
|
|
1013
|
-
|
|
1014
|
-
this.getLastStateHasChangedDate(subject, context).getTime()
|
|
1015
|
-
);
|
|
1016
|
-
date.setTime(date.getTime() + this.timeoutMs);
|
|
1017
|
-
return date <= /* @__PURE__ */ new Date();
|
|
1130
|
+
return this.getLastStateHasChangedDate(subject, context).getTime() + this.timeoutMs <= Date.now();
|
|
1018
1131
|
}
|
|
1019
1132
|
};
|
|
1020
1133
|
|
|
1021
|
-
// src/condition/
|
|
1022
|
-
var
|
|
1134
|
+
// src/condition/CompositeCondition.ts
|
|
1135
|
+
var CompositeCondition = class {
|
|
1023
1136
|
conditions = [];
|
|
1024
|
-
|
|
1137
|
+
joinWord;
|
|
1138
|
+
constructor(joinWord, condition) {
|
|
1139
|
+
this.joinWord = joinWord;
|
|
1025
1140
|
this.conditions.push(condition);
|
|
1026
1141
|
}
|
|
1027
|
-
|
|
1142
|
+
addCondition(condition) {
|
|
1028
1143
|
this.conditions.push(condition);
|
|
1029
1144
|
return this;
|
|
1030
1145
|
}
|
|
1031
1146
|
getName() {
|
|
1032
1147
|
const names = this.conditions.map((c) => c.getName());
|
|
1033
|
-
return `(${names.join(
|
|
1148
|
+
return `(${names.join(` ${this.joinWord} `)})`;
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// src/condition/AndComposite.ts
|
|
1153
|
+
var AndComposite = class extends CompositeCondition {
|
|
1154
|
+
constructor(condition) {
|
|
1155
|
+
super("and", condition);
|
|
1156
|
+
}
|
|
1157
|
+
addAnd(condition) {
|
|
1158
|
+
return this.addCondition(condition);
|
|
1034
1159
|
}
|
|
1035
1160
|
async checkCondition(subject, context) {
|
|
1036
1161
|
for (const condition of this.conditions) {
|
|
@@ -1043,18 +1168,12 @@ var AndComposite = class {
|
|
|
1043
1168
|
};
|
|
1044
1169
|
|
|
1045
1170
|
// src/condition/OrComposite.ts
|
|
1046
|
-
var OrComposite = class {
|
|
1047
|
-
conditions = [];
|
|
1171
|
+
var OrComposite = class extends CompositeCondition {
|
|
1048
1172
|
constructor(condition) {
|
|
1049
|
-
|
|
1173
|
+
super("or", condition);
|
|
1050
1174
|
}
|
|
1051
1175
|
addOr(condition) {
|
|
1052
|
-
this.
|
|
1053
|
-
return this;
|
|
1054
|
-
}
|
|
1055
|
-
getName() {
|
|
1056
|
-
const names = this.conditions.map((c) => c.getName());
|
|
1057
|
-
return `(${names.join(" or ")})`;
|
|
1176
|
+
return this.addCondition(condition);
|
|
1058
1177
|
}
|
|
1059
1178
|
async checkCondition(subject, context) {
|
|
1060
1179
|
for (const condition of this.conditions) {
|
|
@@ -1086,10 +1205,9 @@ var CallbackObserver = class {
|
|
|
1086
1205
|
constructor(callback) {
|
|
1087
1206
|
this.callback = callback;
|
|
1088
1207
|
}
|
|
1089
|
-
update(subject) {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
return this.callback(...event.getInvokeArgs());
|
|
1208
|
+
update(subject, args) {
|
|
1209
|
+
if (args !== void 0) {
|
|
1210
|
+
return this.callback(...args);
|
|
1093
1211
|
}
|
|
1094
1212
|
return this.callback(subject);
|
|
1095
1213
|
}
|
|
@@ -1098,11 +1216,19 @@ var CallbackObserver = class {
|
|
|
1098
1216
|
// src/observer/StatefulStatusChanger.ts
|
|
1099
1217
|
var StatefulStatusChanger = class {
|
|
1100
1218
|
subject;
|
|
1219
|
+
/**
|
|
1220
|
+
* @param subject Optional explicit subject to write to. When omitted
|
|
1221
|
+
* (recommended), the observer writes to frame.subject — the subject of
|
|
1222
|
+
* whichever machine fired the transition — so a single instance can be
|
|
1223
|
+
* shared safely across every machine a Factory creates.
|
|
1224
|
+
*/
|
|
1101
1225
|
constructor(subject) {
|
|
1102
|
-
this.subject = subject;
|
|
1226
|
+
this.subject = subject ?? null;
|
|
1103
1227
|
}
|
|
1104
1228
|
notify(frame) {
|
|
1105
|
-
this.subject
|
|
1229
|
+
(this.subject ?? frame.subject).setCurrentStateName(
|
|
1230
|
+
frame.toState.getName()
|
|
1231
|
+
);
|
|
1106
1232
|
}
|
|
1107
1233
|
};
|
|
1108
1234
|
|
|
@@ -1115,19 +1241,25 @@ var OnEnterObserver = class _OnEnterObserver {
|
|
|
1115
1241
|
}
|
|
1116
1242
|
notify(frame, ctx) {
|
|
1117
1243
|
if (frame.toState.hasEvent(this.eventName)) {
|
|
1118
|
-
ctx.enqueue(
|
|
1244
|
+
ctx.enqueue(
|
|
1245
|
+
this.eventName,
|
|
1246
|
+
new Map(frame.context),
|
|
1247
|
+
frame.toState.getName()
|
|
1248
|
+
);
|
|
1119
1249
|
}
|
|
1120
1250
|
}
|
|
1121
1251
|
};
|
|
1122
1252
|
|
|
1123
|
-
// src/
|
|
1253
|
+
// src/util/index.ts
|
|
1124
1254
|
function isNamed(obj) {
|
|
1125
1255
|
return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
|
|
1126
1256
|
}
|
|
1127
|
-
function
|
|
1257
|
+
function nameOrString(obj) {
|
|
1128
1258
|
if (isNamed(obj)) return obj.getName();
|
|
1129
1259
|
return String(obj);
|
|
1130
1260
|
}
|
|
1261
|
+
|
|
1262
|
+
// src/observer/TransitionLogger.ts
|
|
1131
1263
|
var TransitionLogger = class {
|
|
1132
1264
|
logger;
|
|
1133
1265
|
loggerLevel;
|
|
@@ -1137,7 +1269,7 @@ var TransitionLogger = class {
|
|
|
1137
1269
|
}
|
|
1138
1270
|
notify(frame) {
|
|
1139
1271
|
let message = "Transition";
|
|
1140
|
-
message += ` from "${
|
|
1272
|
+
message += ` from "${nameOrString(frame.fromState)}" to "${nameOrString(frame.toState)}"`;
|
|
1141
1273
|
const eventName = frame.event ? frame.event.getName() : null;
|
|
1142
1274
|
const conditionName = frame.condition ? frame.condition.getName() : null;
|
|
1143
1275
|
if (eventName || conditionName) {
|
|
@@ -1244,23 +1376,30 @@ var WeightTransition = class {
|
|
|
1244
1376
|
innerSelector;
|
|
1245
1377
|
epsilon;
|
|
1246
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
|
+
}
|
|
1247
1384
|
this.innerSelector = innerSelector ?? new OneOrNoneActiveTransition();
|
|
1248
1385
|
this.epsilon = epsilon;
|
|
1249
1386
|
}
|
|
1250
1387
|
selectTransition(transitions) {
|
|
1251
|
-
|
|
1252
|
-
let
|
|
1253
|
-
for (const transition of
|
|
1388
|
+
const all = Array.from(transitions);
|
|
1389
|
+
let maxWeight = Number.NEGATIVE_INFINITY;
|
|
1390
|
+
for (const transition of all) {
|
|
1254
1391
|
const weight = transition.getWeight();
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
} else if (Math.abs(diff) < this.epsilon) {
|
|
1260
|
-
bestTransitions.push(transition);
|
|
1392
|
+
if (!Number.isFinite(weight)) {
|
|
1393
|
+
throw new RangeError(
|
|
1394
|
+
`WeightTransition: transition weights must be finite numbers; got ${String(weight)}`
|
|
1395
|
+
);
|
|
1261
1396
|
}
|
|
1397
|
+
if (weight > maxWeight) maxWeight = weight;
|
|
1262
1398
|
}
|
|
1263
|
-
|
|
1399
|
+
const best = all.filter(
|
|
1400
|
+
(transition) => maxWeight - transition.getWeight() < this.epsilon
|
|
1401
|
+
);
|
|
1402
|
+
return this.innerSelector.selectTransition(best);
|
|
1264
1403
|
}
|
|
1265
1404
|
};
|
|
1266
1405
|
|
|
@@ -1403,23 +1542,6 @@ var AbstractNamedProcessDetector = class {
|
|
|
1403
1542
|
}
|
|
1404
1543
|
};
|
|
1405
1544
|
|
|
1406
|
-
// src/error/InvalidSubjectError.ts
|
|
1407
|
-
var InvalidSubjectError = class extends FinitaError {
|
|
1408
|
-
code = "invalidSubject";
|
|
1409
|
-
expectedInterface;
|
|
1410
|
-
missingMembers;
|
|
1411
|
-
constructor(expectedInterface, missingMembers) {
|
|
1412
|
-
const members = Array.from(missingMembers);
|
|
1413
|
-
const memberList = members.map((m) => `"${m}"`).join(", ");
|
|
1414
|
-
super(
|
|
1415
|
-
`Subject does not satisfy ${expectedInterface}; missing member(s): ${memberList || "(unknown)"}`
|
|
1416
|
-
);
|
|
1417
|
-
this.name = "InvalidSubjectError";
|
|
1418
|
-
this.expectedInterface = expectedInterface;
|
|
1419
|
-
this.missingMembers = Object.freeze([...members]);
|
|
1420
|
-
}
|
|
1421
|
-
};
|
|
1422
|
-
|
|
1423
1545
|
// src/factory/StatefulStateNameDetector.ts
|
|
1424
1546
|
function isStateful(obj) {
|
|
1425
1547
|
return typeof obj === "object" && obj !== null && "getCurrentStateName" in obj && typeof obj.getCurrentStateName === "function";
|
|
@@ -1434,11 +1556,8 @@ var StatefulStateNameDetector = class {
|
|
|
1434
1556
|
};
|
|
1435
1557
|
|
|
1436
1558
|
// src/graph/GraphBuilder.ts
|
|
1437
|
-
function
|
|
1438
|
-
return
|
|
1439
|
-
}
|
|
1440
|
-
function escapeDoubleQuotes(str) {
|
|
1441
|
-
return str.replace(/"/g, '\\"');
|
|
1559
|
+
function escapeDotString(str) {
|
|
1560
|
+
return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1442
1561
|
}
|
|
1443
1562
|
function toMermaidId(name) {
|
|
1444
1563
|
return "s_" + name.replace(
|
|
@@ -1447,13 +1566,15 @@ function toMermaidId(name) {
|
|
|
1447
1566
|
);
|
|
1448
1567
|
}
|
|
1449
1568
|
function escapeMermaidLabel(str) {
|
|
1450
|
-
return str.replace(/"/g, "#quot;");
|
|
1569
|
+
return str.replace(/\\/g, "#92;").replace(/"/g, "#quot;");
|
|
1451
1570
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
+
);
|
|
1455
1577
|
}
|
|
1456
|
-
return String(obj);
|
|
1457
1578
|
}
|
|
1458
1579
|
var GraphBuilder = class {
|
|
1459
1580
|
nodes = /* @__PURE__ */ new Map();
|
|
@@ -1473,13 +1594,15 @@ var GraphBuilder = class {
|
|
|
1473
1594
|
const eventName = transition.getEventName();
|
|
1474
1595
|
if (eventName) {
|
|
1475
1596
|
parts.push(`E: ${eventName}`);
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1597
|
+
if (state.hasEvent(eventName)) {
|
|
1598
|
+
const event = state.getEvent(eventName);
|
|
1599
|
+
const observerNames = [];
|
|
1600
|
+
for (const observer of event.getObservers()) {
|
|
1601
|
+
observerNames.push(nameOrString(observer));
|
|
1602
|
+
}
|
|
1603
|
+
if (observerNames.length > 0) {
|
|
1604
|
+
parts.push(`C: ${observerNames.join(", ")}`);
|
|
1605
|
+
}
|
|
1483
1606
|
}
|
|
1484
1607
|
}
|
|
1485
1608
|
const conditionName = transition.getConditionName();
|
|
@@ -1528,17 +1651,18 @@ var GraphBuilder = class {
|
|
|
1528
1651
|
toDot(options) {
|
|
1529
1652
|
const graph = this.getGraph();
|
|
1530
1653
|
const rankdir = options?.rankdir ?? "LR";
|
|
1654
|
+
assertDirection(rankdir, "rankdir");
|
|
1531
1655
|
const lines = [];
|
|
1532
1656
|
lines.push("digraph {");
|
|
1533
1657
|
lines.push(` rankdir=${rankdir};`);
|
|
1534
1658
|
for (const node of graph.nodes) {
|
|
1535
|
-
const label =
|
|
1659
|
+
const label = escapeDotString(node.label);
|
|
1536
1660
|
lines.push(` "${label}" [label="${label}"];`);
|
|
1537
1661
|
}
|
|
1538
1662
|
for (const edge of graph.edges) {
|
|
1539
|
-
const source =
|
|
1540
|
-
const target =
|
|
1541
|
-
const label =
|
|
1663
|
+
const source = escapeDotString(edge.source);
|
|
1664
|
+
const target = escapeDotString(edge.target);
|
|
1665
|
+
const label = escapeDotString(edge.label);
|
|
1542
1666
|
lines.push(` "${source}" -> "${target}" [label="${label}"];`);
|
|
1543
1667
|
}
|
|
1544
1668
|
lines.push("}");
|
|
@@ -1547,6 +1671,7 @@ var GraphBuilder = class {
|
|
|
1547
1671
|
toMermaid(options) {
|
|
1548
1672
|
const graph = this.getGraph();
|
|
1549
1673
|
const direction = options?.direction ?? "LR";
|
|
1674
|
+
assertDirection(direction, "direction");
|
|
1550
1675
|
const lines = [];
|
|
1551
1676
|
lines.push(`stateDiagram-v2`);
|
|
1552
1677
|
lines.push(` direction ${direction}`);
|
|
@@ -1601,6 +1726,8 @@ export {
|
|
|
1601
1726
|
ProcessBuilder,
|
|
1602
1727
|
ProcessFinalizedError,
|
|
1603
1728
|
ProcessNotFoundError,
|
|
1729
|
+
QueueLimitExceededError,
|
|
1730
|
+
ReentrancyError,
|
|
1604
1731
|
ScoreTransition,
|
|
1605
1732
|
SingleProcessDetector,
|
|
1606
1733
|
State,
|