@camcima/finita 3.0.1 → 4.0.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.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 this.invokeArgs;
18
+ return [];
15
19
  }
16
20
  async invoke(...args) {
17
- this.invokeArgs = args;
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() {
@@ -311,8 +310,9 @@ var DuplicateTransitionError = class extends FinitaError {
311
310
  const eventLabel = conflict.eventName ?? "<automatic>";
312
311
  const existing = conflict.existingConditionName ?? "<no condition>";
313
312
  const incoming = conflict.newConditionName ?? "<no condition>";
313
+ const weightInfo = conflict.existingWeight !== void 0 && conflict.newWeight !== void 0 && conflict.existingWeight !== conflict.newWeight ? `, existing weight ${conflict.existingWeight} vs new weight ${conflict.newWeight}` : "";
314
314
  super(
315
- `Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"`
315
+ `Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"${weightInfo}`
316
316
  );
317
317
  this.name = "DuplicateTransitionError";
318
318
  this.conflict = Object.freeze({ ...conflict });
@@ -320,7 +320,7 @@ var DuplicateTransitionError = class extends FinitaError {
320
320
  };
321
321
 
322
322
  // src/ProcessBuilder.ts
323
- var ProcessBuilder = class {
323
+ var ProcessBuilder = class _ProcessBuilder {
324
324
  processName;
325
325
  stateSpecs = /* @__PURE__ */ new Map();
326
326
  transitionSpecs = [];
@@ -335,6 +335,9 @@ var ProcessBuilder = class {
335
335
  if (this.stateSpecs.has(name)) {
336
336
  throw new DuplicateStateError(name);
337
337
  }
338
+ this.validateName("invalidStateName", name, `addState("${name}")`, {
339
+ stateName: name
340
+ });
338
341
  this.stateSpecs.set(name, {
339
342
  name,
340
343
  initial: options.initial === true,
@@ -348,25 +351,22 @@ var ProcessBuilder = class {
348
351
  }
349
352
  let eventName = null;
350
353
  if (options.event !== void 0) {
351
- const raw = options.event;
352
- if (raw.trim() === "" || raw !== raw.trim()) {
353
- throw new GraphValidationError(
354
- "invalidEventName",
355
- `addTransition called with an empty or whitespace-padded event name from "${fromState}" to "${toState}"`,
356
- { fromState, toState, eventName: raw }
357
- );
358
- }
359
- eventName = raw;
354
+ this.validateName(
355
+ "invalidEventName",
356
+ options.event,
357
+ `addTransition called with an invalid event name from "${fromState}" to "${toState}"`,
358
+ { fromState, toState, eventName: options.event }
359
+ );
360
+ eventName = options.event;
360
361
  }
361
362
  if (options.condition) {
362
363
  const conditionName = options.condition.getName();
363
- if (conditionName.trim() === "") {
364
- throw new GraphValidationError(
365
- "invalidConditionName",
366
- `addTransition called with an empty/whitespace condition name from "${fromState}" to "${toState}"`,
367
- { fromState, toState, conditionName }
368
- );
369
- }
364
+ this.validateName(
365
+ "invalidConditionName",
366
+ conditionName,
367
+ `addTransition called with an invalid condition name from "${fromState}" to "${toState}"`,
368
+ { fromState, toState, conditionName }
369
+ );
370
370
  }
371
371
  this.transitionSpecs.push({
372
372
  fromState,
@@ -400,6 +400,16 @@ var ProcessBuilder = class {
400
400
  );
401
401
  }
402
402
  // --- private helpers ---
403
+ /** One name rule for every named entity: non-empty, no leading/trailing whitespace. */
404
+ validateName(code, raw, description, details) {
405
+ if (raw.trim() === "" || raw !== raw.trim()) {
406
+ throw new GraphValidationError(
407
+ code,
408
+ `${description}: name ${JSON.stringify(raw)} is empty or whitespace-padded`,
409
+ details
410
+ );
411
+ }
412
+ }
403
413
  validateInitialState() {
404
414
  const initials = Array.from(this.stateSpecs.values()).filter(
405
415
  (s) => s.initial
@@ -451,22 +461,29 @@ var ProcessBuilder = class {
451
461
  }
452
462
  }
453
463
  }
464
+ /** Transition identity: (fromState, eventName, toState). Used by both the
465
+ * conflict check and the build-time dedup — keep them in lockstep. */
466
+ static transitionKey(t) {
467
+ return `${t.fromState}\0${t.eventName ?? ""}\0${t.toState}`;
468
+ }
454
469
  validateNoConflictingDuplicates() {
455
470
  const seen = /* @__PURE__ */ new Map();
456
471
  for (const t of this.transitionSpecs) {
457
- const key = `${t.fromState}\0${t.eventName ?? ""}\0${t.toState}`;
472
+ const key = _ProcessBuilder.transitionKey(t);
458
473
  const existing = seen.get(key);
459
474
  if (!existing) {
460
475
  seen.set(key, t);
461
476
  continue;
462
477
  }
463
- if (existing.condition !== t.condition) {
478
+ if (existing.condition !== t.condition || existing.weight !== t.weight) {
464
479
  throw new DuplicateTransitionError({
465
480
  fromState: t.fromState,
466
481
  toState: t.toState,
467
482
  eventName: t.eventName,
468
483
  existingConditionName: existing.condition ? existing.condition.getName() : null,
469
- newConditionName: t.condition ? t.condition.getName() : null
484
+ newConditionName: t.condition ? t.condition.getName() : null,
485
+ existingWeight: existing.weight,
486
+ newWeight: t.weight
470
487
  });
471
488
  }
472
489
  }
@@ -510,23 +527,12 @@ var ProcessBuilder = class {
510
527
  );
511
528
  }
512
529
  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
530
  const transitionsByState = /* @__PURE__ */ new Map();
525
531
  for (const spec of this.stateSpecs.values()) {
526
532
  transitionsByState.set(spec.name, []);
527
533
  }
528
534
  for (const tSpec of this.transitionSpecs) {
529
- const dedupKey = `${tSpec.fromState}\0${tSpec.eventName ?? ""}\0${tSpec.toState}\0${idForCondition(tSpec.condition)}`;
535
+ const dedupKey = _ProcessBuilder.transitionKey(tSpec);
530
536
  if (dedupSeen.has(dedupKey)) continue;
531
537
  dedupSeen.add(dedupKey);
532
538
  const targetState = built.get(tSpec.toState);
@@ -618,27 +624,6 @@ var NullMutex = class {
618
624
  }
619
625
  };
620
626
 
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
627
  // src/internal/OperationQueue.ts
643
628
  var OperationQueue = class {
644
629
  items = [];
@@ -655,10 +640,11 @@ var OperationQueue = class {
655
640
 
656
641
  // src/filter/ActiveTransitionFilter.ts
657
642
  var ActiveTransitionFilter = class {
658
- static async filter(transitions, subject, context, event) {
643
+ static async filter(transitions, subject, context, event, wrap) {
644
+ const run = wrap ?? ((fn) => fn());
659
645
  const active = [];
660
646
  for (const transition of transitions) {
661
- if (await transition.isActive(subject, context, event)) {
647
+ if (await run(() => transition.isActive(subject, context, event))) {
662
648
  active.push(transition);
663
649
  }
664
650
  }
@@ -691,16 +677,26 @@ var LockCanNotBeAcquiredError = class extends FinitaError {
691
677
  // src/error/AutomaticTransitionCycleError.ts
692
678
  var AutomaticTransitionCycleError = class extends FinitaError {
693
679
  code = "automaticTransitionCycle";
694
- targetStateName;
695
- visitedStateNames;
696
- constructor(targetStateName, visitedStateNames) {
697
- const visited = Array.from(visitedStateNames);
680
+ stateName;
681
+ hopLimit;
682
+ constructor(stateName, hopLimit) {
698
683
  super(
699
- `Automatic transition cycle detected: state "${targetStateName}" was already visited \u2014 this would cause infinite recursion`
684
+ `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
685
  );
701
686
  this.name = "AutomaticTransitionCycleError";
702
- this.targetStateName = targetStateName;
703
- this.visitedStateNames = Object.freeze([...visited]);
687
+ this.stateName = stateName;
688
+ this.hopLimit = hopLimit;
689
+ }
690
+ };
691
+
692
+ // src/error/ReentrancyError.ts
693
+ var ReentrancyError = class extends FinitaError {
694
+ code = "reentrancy";
695
+ constructor(operation) {
696
+ super(
697
+ `${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(...)).`
698
+ );
699
+ this.name = "ReentrancyError";
704
700
  }
705
701
  };
706
702
 
@@ -713,17 +709,26 @@ var Statemachine = class {
713
709
  currentState;
714
710
  lastState = null;
715
711
  autoreleaseLock;
712
+ maxAutomaticHops;
716
713
  queue = new OperationQueue();
717
714
  running = false;
715
+ inSyncCallback = false;
718
716
  beforeObservers = [];
719
717
  afterObservers = [];
720
718
  constructor(subject, process, options = {}) {
721
719
  this.subject = subject;
722
720
  this.process = process;
723
- this.currentState = options.initialStateName ? process.getState(options.initialStateName) : process.getInitialState();
721
+ this.currentState = options.initialStateName !== void 0 ? process.getState(options.initialStateName) : process.getInitialState();
724
722
  this.transitionSelector = options.transitionSelector ?? new OneOrNoneActiveTransition();
725
723
  this.mutex = options.mutex ?? new NullMutex();
726
724
  this.autoreleaseLock = options.autoreleaseLock ?? true;
725
+ const hops = options.maxAutomaticHops ?? 100;
726
+ if (!Number.isInteger(hops) || hops < 1) {
727
+ throw new RangeError(
728
+ `maxAutomaticHops must be a positive integer; got ${String(options.maxAutomaticHops)}`
729
+ );
730
+ }
731
+ this.maxAutomaticHops = hops;
727
732
  }
728
733
  // --- public getters ---
729
734
  getCurrentState() {
@@ -778,27 +783,45 @@ var Statemachine = class {
778
783
  // --- public top-level operations ---
779
784
  triggerEvent(name, context) {
780
785
  return new Promise((resolve, reject) => {
781
- this.queue.enqueue({
782
- kind: "triggerEvent",
783
- eventName: name,
784
- context: context ?? /* @__PURE__ */ new Map(),
785
- resolve,
786
- reject
787
- });
788
- void this.runIfIdle();
786
+ this.assertNotReentrant(`triggerEvent("${name}")`);
787
+ this.enqueueOperation(name, context, resolve, reject);
789
788
  });
790
789
  }
791
790
  checkTransitions(context) {
792
791
  return new Promise((resolve, reject) => {
793
- this.queue.enqueue({
794
- kind: "checkTransitions",
795
- eventName: null,
796
- context: context ?? /* @__PURE__ */ new Map(),
797
- resolve,
798
- reject
799
- });
800
- void this.runIfIdle();
792
+ this.assertNotReentrant("checkTransitions()");
793
+ this.enqueueOperation(null, context, resolve, reject);
794
+ });
795
+ }
796
+ /** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
797
+ * the flag is cleared as soon as fn returns (before any promise it returned
798
+ * is awaited), so concurrent external callers are never affected. This
799
+ * catches triggerEvent/checkTransitions calls made before a callback's first
800
+ * await; calls made after a prior await are not detectable without
801
+ * AsyncLocalStorage (Node-only) and will still deadlock — a documented gap. */
802
+ guardSync(fn) {
803
+ this.inSyncCallback = true;
804
+ try {
805
+ return fn();
806
+ } finally {
807
+ this.inSyncCallback = false;
808
+ }
809
+ }
810
+ assertNotReentrant(operation) {
811
+ if (this.inSyncCallback) {
812
+ throw new ReentrancyError(operation);
813
+ }
814
+ }
815
+ /** Single entry point to the operation queue — every enqueue kicks the runner. */
816
+ enqueueOperation(eventName, context, resolve, reject, ifStateName) {
817
+ this.queue.enqueue({
818
+ eventName,
819
+ context: context ?? /* @__PURE__ */ new Map(),
820
+ ifStateName,
821
+ resolve,
822
+ reject
801
823
  });
824
+ void this.runIfIdle();
802
825
  }
803
826
  // --- internal runner ---
804
827
  async runIfIdle() {
@@ -814,7 +837,12 @@ var Statemachine = class {
814
837
  }
815
838
  }
816
839
  async runOperation(op) {
840
+ if (op.ifStateName !== void 0 && this.currentState.getName() !== op.ifStateName) {
841
+ op.resolve();
842
+ return;
843
+ }
817
844
  let acquiredHere = false;
845
+ let failure = null;
818
846
  try {
819
847
  if (!this.mutex.isAcquired()) {
820
848
  if (!await this.mutex.acquireLock()) {
@@ -822,19 +850,24 @@ var Statemachine = class {
822
850
  }
823
851
  acquiredHere = true;
824
852
  }
825
- const event = op.kind === "triggerEvent" ? this.resolveEvent(op.eventName) : null;
853
+ const event = op.eventName !== null ? this.resolveEvent(op.eventName) : null;
826
854
  await this.processOperation(event, op.context);
827
- op.resolve();
828
855
  } catch (err) {
829
- op.reject(err);
856
+ failure = { err };
830
857
  } finally {
831
858
  if (acquiredHere && this.autoreleaseLock) {
832
859
  try {
833
860
  await this.mutex.releaseLock();
834
- } catch {
861
+ } catch (err) {
862
+ if (!failure) failure = { err };
835
863
  }
836
864
  }
837
865
  }
866
+ if (failure) {
867
+ op.reject(failure.err);
868
+ } else {
869
+ op.resolve();
870
+ }
838
871
  }
839
872
  resolveEvent(name) {
840
873
  if (!this.currentState.hasEvent(name)) {
@@ -849,11 +882,13 @@ var Statemachine = class {
849
882
  */
850
883
  async processOperation(initialEvent, context) {
851
884
  let event = initialEvent;
852
- const automaticVisited = /* @__PURE__ */ new Set();
885
+ let automaticHops = 0;
853
886
  if (event) {
854
- const dispatcher = new Dispatcher();
855
- dispatcher.dispatch(event, [this.subject, context]);
856
- await dispatcher.invoke();
887
+ const userEvent = event;
888
+ const invokeArgs = [this.subject, context];
889
+ for (const observer of [...userEvent.getObservers()]) {
890
+ await this.guardSync(() => observer.update(userEvent, invokeArgs));
891
+ }
857
892
  }
858
893
  while (true) {
859
894
  const transitions = this.currentState.getTransitions();
@@ -861,26 +896,28 @@ var Statemachine = class {
861
896
  transitions,
862
897
  this.subject,
863
898
  context,
864
- event ?? void 0
899
+ event ?? void 0,
900
+ (fn) => this.guardSync(fn)
865
901
  );
866
- const selected = this.transitionSelector.selectTransition(
867
- active
902
+ const selected = this.guardSync(
903
+ () => this.transitionSelector.selectTransition(active)
868
904
  );
869
905
  if (!selected) {
870
906
  return;
871
907
  }
872
908
  const target = selected.getTargetState();
873
909
  if (selected.getEventName() === null) {
874
- automaticVisited.add(this.currentState);
875
- if (automaticVisited.has(target)) {
910
+ automaticHops += 1;
911
+ if (automaticHops > this.maxAutomaticHops) {
876
912
  throw new AutomaticTransitionCycleError(
877
913
  target.getName(),
878
- Array.from(automaticVisited).map((s) => s.getName())
914
+ this.maxAutomaticHops
879
915
  );
880
916
  }
881
917
  }
882
918
  if (this.currentState !== target) {
883
- const proposedFrame = Object.freeze({
919
+ const frame = Object.freeze({
920
+ subject: this.subject,
884
921
  fromState: this.currentState,
885
922
  toState: target,
886
923
  transition: selected,
@@ -890,39 +927,28 @@ var Statemachine = class {
890
927
  timestamp: Date.now(),
891
928
  machineName: this.process.getName()
892
929
  });
893
- for (const observer of this.beforeObservers) {
894
- await observer.notify(proposedFrame);
930
+ for (const observer of [...this.beforeObservers]) {
931
+ await this.guardSync(() => observer.notify(frame));
895
932
  }
896
- const fromState = this.currentState;
897
- this.lastState = fromState;
933
+ this.lastState = this.currentState;
898
934
  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
935
  const enqueueCtx = {
910
- enqueue: (chainedEventName, chainedCtx) => {
911
- this.queue.enqueue({
912
- kind: "triggerEvent",
913
- eventName: chainedEventName,
914
- context: chainedCtx ?? /* @__PURE__ */ new Map(),
915
- resolve: () => {
936
+ enqueue: (chainedEventName, chainedCtx, ifStateName) => {
937
+ this.enqueueOperation(
938
+ chainedEventName,
939
+ chainedCtx,
940
+ () => {
916
941
  },
917
- reject: () => {
918
- }
919
- });
942
+ () => {
943
+ },
944
+ ifStateName
945
+ );
920
946
  }
921
947
  };
922
948
  const errors = [];
923
- for (const observer of this.afterObservers) {
949
+ for (const observer of [...this.afterObservers]) {
924
950
  try {
925
- await observer.notify(committedFrame, enqueueCtx);
951
+ await this.guardSync(() => observer.notify(frame, enqueueCtx));
926
952
  } catch (err) {
927
953
  errors.push(err);
928
954
  }
@@ -989,6 +1015,23 @@ var CallbackCondition = class {
989
1015
  }
990
1016
  };
991
1017
 
1018
+ // src/error/InvalidSubjectError.ts
1019
+ var InvalidSubjectError = class extends FinitaError {
1020
+ code = "invalidSubject";
1021
+ expectedInterface;
1022
+ missingMembers;
1023
+ constructor(expectedInterface, missingMembers) {
1024
+ const members = Array.from(missingMembers);
1025
+ const memberList = members.map((m) => `"${m}"`).join(", ");
1026
+ super(
1027
+ `Subject does not satisfy ${expectedInterface}; missing member(s): ${memberList || "(unknown)"}`
1028
+ );
1029
+ this.name = "InvalidSubjectError";
1030
+ this.expectedInterface = expectedInterface;
1031
+ this.missingMembers = Object.freeze([...members]);
1032
+ }
1033
+ };
1034
+
992
1035
  // src/condition/Timeout.ts
993
1036
  function isLastStateHasChangedDate(obj) {
994
1037
  return typeof obj === "object" && obj !== null && "getLastStateHasChangedDate" in obj && typeof obj.getLastStateHasChangedDate === "function";
@@ -1007,30 +1050,40 @@ var Timeout = class {
1007
1050
  if (isLastStateHasChangedDate(subject)) {
1008
1051
  return subject.getLastStateHasChangedDate();
1009
1052
  }
1010
- throw new Error("Subject must implement LastStateHasChangedDateInterface");
1053
+ throw new InvalidSubjectError("LastStateHasChangedDateInterface", [
1054
+ "getLastStateHasChangedDate"
1055
+ ]);
1011
1056
  }
1012
1057
  checkCondition(subject, context) {
1013
- const date = new Date(
1014
- this.getLastStateHasChangedDate(subject, context).getTime()
1015
- );
1016
- date.setTime(date.getTime() + this.timeoutMs);
1017
- return date <= /* @__PURE__ */ new Date();
1058
+ return this.getLastStateHasChangedDate(subject, context).getTime() + this.timeoutMs <= Date.now();
1018
1059
  }
1019
1060
  };
1020
1061
 
1021
- // src/condition/AndComposite.ts
1022
- var AndComposite = class {
1062
+ // src/condition/CompositeCondition.ts
1063
+ var CompositeCondition = class {
1023
1064
  conditions = [];
1024
- constructor(condition) {
1065
+ joinWord;
1066
+ constructor(joinWord, condition) {
1067
+ this.joinWord = joinWord;
1025
1068
  this.conditions.push(condition);
1026
1069
  }
1027
- addAnd(condition) {
1070
+ addCondition(condition) {
1028
1071
  this.conditions.push(condition);
1029
1072
  return this;
1030
1073
  }
1031
1074
  getName() {
1032
1075
  const names = this.conditions.map((c) => c.getName());
1033
- return `(${names.join(" and ")})`;
1076
+ return `(${names.join(` ${this.joinWord} `)})`;
1077
+ }
1078
+ };
1079
+
1080
+ // src/condition/AndComposite.ts
1081
+ var AndComposite = class extends CompositeCondition {
1082
+ constructor(condition) {
1083
+ super("and", condition);
1084
+ }
1085
+ addAnd(condition) {
1086
+ return this.addCondition(condition);
1034
1087
  }
1035
1088
  async checkCondition(subject, context) {
1036
1089
  for (const condition of this.conditions) {
@@ -1043,18 +1096,12 @@ var AndComposite = class {
1043
1096
  };
1044
1097
 
1045
1098
  // src/condition/OrComposite.ts
1046
- var OrComposite = class {
1047
- conditions = [];
1099
+ var OrComposite = class extends CompositeCondition {
1048
1100
  constructor(condition) {
1049
- this.conditions.push(condition);
1101
+ super("or", condition);
1050
1102
  }
1051
1103
  addOr(condition) {
1052
- this.conditions.push(condition);
1053
- return this;
1054
- }
1055
- getName() {
1056
- const names = this.conditions.map((c) => c.getName());
1057
- return `(${names.join(" or ")})`;
1104
+ return this.addCondition(condition);
1058
1105
  }
1059
1106
  async checkCondition(subject, context) {
1060
1107
  for (const condition of this.conditions) {
@@ -1086,10 +1133,9 @@ var CallbackObserver = class {
1086
1133
  constructor(callback) {
1087
1134
  this.callback = callback;
1088
1135
  }
1089
- update(subject) {
1090
- const event = subject;
1091
- if (typeof event.getInvokeArgs === "function") {
1092
- return this.callback(...event.getInvokeArgs());
1136
+ update(subject, args) {
1137
+ if (args !== void 0) {
1138
+ return this.callback(...args);
1093
1139
  }
1094
1140
  return this.callback(subject);
1095
1141
  }
@@ -1098,11 +1144,19 @@ var CallbackObserver = class {
1098
1144
  // src/observer/StatefulStatusChanger.ts
1099
1145
  var StatefulStatusChanger = class {
1100
1146
  subject;
1147
+ /**
1148
+ * @param subject Optional explicit subject to write to. When omitted
1149
+ * (recommended), the observer writes to frame.subject — the subject of
1150
+ * whichever machine fired the transition — so a single instance can be
1151
+ * shared safely across every machine a Factory creates.
1152
+ */
1101
1153
  constructor(subject) {
1102
- this.subject = subject;
1154
+ this.subject = subject ?? null;
1103
1155
  }
1104
1156
  notify(frame) {
1105
- this.subject.setCurrentStateName(frame.toState.getName());
1157
+ (this.subject ?? frame.subject).setCurrentStateName(
1158
+ frame.toState.getName()
1159
+ );
1106
1160
  }
1107
1161
  };
1108
1162
 
@@ -1115,19 +1169,25 @@ var OnEnterObserver = class _OnEnterObserver {
1115
1169
  }
1116
1170
  notify(frame, ctx) {
1117
1171
  if (frame.toState.hasEvent(this.eventName)) {
1118
- ctx.enqueue(this.eventName, new Map(frame.context));
1172
+ ctx.enqueue(
1173
+ this.eventName,
1174
+ new Map(frame.context),
1175
+ frame.toState.getName()
1176
+ );
1119
1177
  }
1120
1178
  }
1121
1179
  };
1122
1180
 
1123
- // src/observer/TransitionLogger.ts
1181
+ // src/util/index.ts
1124
1182
  function isNamed(obj) {
1125
1183
  return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
1126
1184
  }
1127
- function asString(obj) {
1185
+ function nameOrString(obj) {
1128
1186
  if (isNamed(obj)) return obj.getName();
1129
1187
  return String(obj);
1130
1188
  }
1189
+
1190
+ // src/observer/TransitionLogger.ts
1131
1191
  var TransitionLogger = class {
1132
1192
  logger;
1133
1193
  loggerLevel;
@@ -1137,7 +1197,7 @@ var TransitionLogger = class {
1137
1197
  }
1138
1198
  notify(frame) {
1139
1199
  let message = "Transition";
1140
- message += ` from "${asString(frame.fromState)}" to "${asString(frame.toState)}"`;
1200
+ message += ` from "${nameOrString(frame.fromState)}" to "${nameOrString(frame.toState)}"`;
1141
1201
  const eventName = frame.event ? frame.event.getName() : null;
1142
1202
  const conditionName = frame.condition ? frame.condition.getName() : null;
1143
1203
  if (eventName || conditionName) {
@@ -1248,19 +1308,16 @@ var WeightTransition = class {
1248
1308
  this.epsilon = epsilon;
1249
1309
  }
1250
1310
  selectTransition(transitions) {
1251
- let bestTransitions = [];
1252
- let bestWeight = null;
1253
- for (const transition of transitions) {
1311
+ const all = Array.from(transitions);
1312
+ let maxWeight = Number.NEGATIVE_INFINITY;
1313
+ for (const transition of all) {
1254
1314
  const weight = transition.getWeight();
1255
- const diff = weight - (bestWeight ?? 0);
1256
- if (bestWeight === null || diff >= this.epsilon) {
1257
- bestWeight = weight;
1258
- bestTransitions = [transition];
1259
- } else if (Math.abs(diff) < this.epsilon) {
1260
- bestTransitions.push(transition);
1261
- }
1315
+ if (weight > maxWeight) maxWeight = weight;
1262
1316
  }
1263
- return this.innerSelector.selectTransition(bestTransitions);
1317
+ const best = all.filter(
1318
+ (transition) => maxWeight - transition.getWeight() < this.epsilon
1319
+ );
1320
+ return this.innerSelector.selectTransition(best);
1264
1321
  }
1265
1322
  };
1266
1323
 
@@ -1403,23 +1460,6 @@ var AbstractNamedProcessDetector = class {
1403
1460
  }
1404
1461
  };
1405
1462
 
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
1463
  // src/factory/StatefulStateNameDetector.ts
1424
1464
  function isStateful(obj) {
1425
1465
  return typeof obj === "object" && obj !== null && "getCurrentStateName" in obj && typeof obj.getCurrentStateName === "function";
@@ -1434,11 +1474,8 @@ var StatefulStateNameDetector = class {
1434
1474
  };
1435
1475
 
1436
1476
  // src/graph/GraphBuilder.ts
1437
- function isNamed2(obj) {
1438
- return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
1439
- }
1440
- function escapeDoubleQuotes(str) {
1441
- return str.replace(/"/g, '\\"');
1477
+ function escapeDotString(str) {
1478
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1442
1479
  }
1443
1480
  function toMermaidId(name) {
1444
1481
  return "s_" + name.replace(
@@ -1447,13 +1484,7 @@ function toMermaidId(name) {
1447
1484
  );
1448
1485
  }
1449
1486
  function escapeMermaidLabel(str) {
1450
- return str.replace(/"/g, "#quot;");
1451
- }
1452
- function convertToString(obj) {
1453
- if (isNamed2(obj)) {
1454
- return obj.getName();
1455
- }
1456
- return String(obj);
1487
+ return str.replace(/\\/g, "#92;").replace(/"/g, "#quot;");
1457
1488
  }
1458
1489
  var GraphBuilder = class {
1459
1490
  nodes = /* @__PURE__ */ new Map();
@@ -1473,13 +1504,15 @@ var GraphBuilder = class {
1473
1504
  const eventName = transition.getEventName();
1474
1505
  if (eventName) {
1475
1506
  parts.push(`E: ${eventName}`);
1476
- const event = state.getEvent(eventName);
1477
- const observerNames = [];
1478
- for (const observer of event.getObservers()) {
1479
- observerNames.push(convertToString(observer));
1480
- }
1481
- if (observerNames.length > 0) {
1482
- parts.push(`C: ${observerNames.join(", ")}`);
1507
+ if (state.hasEvent(eventName)) {
1508
+ const event = state.getEvent(eventName);
1509
+ const observerNames = [];
1510
+ for (const observer of event.getObservers()) {
1511
+ observerNames.push(nameOrString(observer));
1512
+ }
1513
+ if (observerNames.length > 0) {
1514
+ parts.push(`C: ${observerNames.join(", ")}`);
1515
+ }
1483
1516
  }
1484
1517
  }
1485
1518
  const conditionName = transition.getConditionName();
@@ -1532,13 +1565,13 @@ var GraphBuilder = class {
1532
1565
  lines.push("digraph {");
1533
1566
  lines.push(` rankdir=${rankdir};`);
1534
1567
  for (const node of graph.nodes) {
1535
- const label = escapeDoubleQuotes(node.label);
1568
+ const label = escapeDotString(node.label);
1536
1569
  lines.push(` "${label}" [label="${label}"];`);
1537
1570
  }
1538
1571
  for (const edge of graph.edges) {
1539
- const source = escapeDoubleQuotes(edge.source);
1540
- const target = escapeDoubleQuotes(edge.target);
1541
- const label = escapeDoubleQuotes(edge.label);
1572
+ const source = escapeDotString(edge.source);
1573
+ const target = escapeDotString(edge.target);
1574
+ const label = escapeDotString(edge.label);
1542
1575
  lines.push(` "${source}" -> "${target}" [label="${label}"];`);
1543
1576
  }
1544
1577
  lines.push("}");
@@ -1601,6 +1634,7 @@ export {
1601
1634
  ProcessBuilder,
1602
1635
  ProcessFinalizedError,
1603
1636
  ProcessNotFoundError,
1637
+ ReentrancyError,
1604
1638
  ScoreTransition,
1605
1639
  SingleProcessDetector,
1606
1640
  State,