@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.cjs CHANGED
@@ -52,6 +52,7 @@ __export(index_exports, {
52
52
  ProcessBuilder: () => ProcessBuilder,
53
53
  ProcessFinalizedError: () => ProcessFinalizedError,
54
54
  ProcessNotFoundError: () => ProcessNotFoundError,
55
+ ReentrancyError: () => ReentrancyError,
55
56
  ScoreTransition: () => ScoreTransition,
56
57
  SingleProcessDetector: () => SingleProcessDetector,
57
58
  State: () => State,
@@ -74,23 +75,22 @@ var Event = class {
74
75
  name;
75
76
  observers = /* @__PURE__ */ new Set();
76
77
  metadata = /* @__PURE__ */ new Map();
77
- invokeArgs = [];
78
78
  constructor(name) {
79
79
  this.name = name;
80
80
  }
81
81
  getName() {
82
82
  return this.name;
83
83
  }
84
+ /**
85
+ * @deprecated Always returns []. Invoke args are now passed directly to
86
+ * Observer.update — reading them from the event was racy when one
87
+ * Process served multiple Statemachines.
88
+ */
84
89
  getInvokeArgs() {
85
- return this.invokeArgs;
90
+ return [];
86
91
  }
87
92
  async invoke(...args) {
88
- this.invokeArgs = args;
89
- try {
90
- await this.notify();
91
- } finally {
92
- this.invokeArgs = [];
93
- }
93
+ await this.notify(args);
94
94
  }
95
95
  attach(observer) {
96
96
  this.observers.add(observer);
@@ -98,9 +98,9 @@ var Event = class {
98
98
  detach(observer) {
99
99
  this.observers.delete(observer);
100
100
  }
101
- async notify() {
102
- for (const observer of this.observers) {
103
- await observer.update(this);
101
+ async notify(args) {
102
+ for (const observer of [...this.observers]) {
103
+ await observer.update(this, args);
104
104
  }
105
105
  }
106
106
  getObservers() {
@@ -382,8 +382,9 @@ var DuplicateTransitionError = class extends FinitaError {
382
382
  const eventLabel = conflict.eventName ?? "<automatic>";
383
383
  const existing = conflict.existingConditionName ?? "<no condition>";
384
384
  const incoming = conflict.newConditionName ?? "<no condition>";
385
+ const weightInfo = conflict.existingWeight !== void 0 && conflict.newWeight !== void 0 && conflict.existingWeight !== conflict.newWeight ? `, existing weight ${conflict.existingWeight} vs new weight ${conflict.newWeight}` : "";
385
386
  super(
386
- `Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"`
387
+ `Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"${weightInfo}`
387
388
  );
388
389
  this.name = "DuplicateTransitionError";
389
390
  this.conflict = Object.freeze({ ...conflict });
@@ -391,7 +392,7 @@ var DuplicateTransitionError = class extends FinitaError {
391
392
  };
392
393
 
393
394
  // src/ProcessBuilder.ts
394
- var ProcessBuilder = class {
395
+ var ProcessBuilder = class _ProcessBuilder {
395
396
  processName;
396
397
  stateSpecs = /* @__PURE__ */ new Map();
397
398
  transitionSpecs = [];
@@ -406,6 +407,9 @@ var ProcessBuilder = class {
406
407
  if (this.stateSpecs.has(name)) {
407
408
  throw new DuplicateStateError(name);
408
409
  }
410
+ this.validateName("invalidStateName", name, `addState("${name}")`, {
411
+ stateName: name
412
+ });
409
413
  this.stateSpecs.set(name, {
410
414
  name,
411
415
  initial: options.initial === true,
@@ -419,25 +423,22 @@ var ProcessBuilder = class {
419
423
  }
420
424
  let eventName = null;
421
425
  if (options.event !== void 0) {
422
- const raw = options.event;
423
- if (raw.trim() === "" || raw !== raw.trim()) {
424
- throw new GraphValidationError(
425
- "invalidEventName",
426
- `addTransition called with an empty or whitespace-padded event name from "${fromState}" to "${toState}"`,
427
- { fromState, toState, eventName: raw }
428
- );
429
- }
430
- eventName = raw;
426
+ this.validateName(
427
+ "invalidEventName",
428
+ options.event,
429
+ `addTransition called with an invalid event name from "${fromState}" to "${toState}"`,
430
+ { fromState, toState, eventName: options.event }
431
+ );
432
+ eventName = options.event;
431
433
  }
432
434
  if (options.condition) {
433
435
  const conditionName = options.condition.getName();
434
- if (conditionName.trim() === "") {
435
- throw new GraphValidationError(
436
- "invalidConditionName",
437
- `addTransition called with an empty/whitespace condition name from "${fromState}" to "${toState}"`,
438
- { fromState, toState, conditionName }
439
- );
440
- }
436
+ this.validateName(
437
+ "invalidConditionName",
438
+ conditionName,
439
+ `addTransition called with an invalid condition name from "${fromState}" to "${toState}"`,
440
+ { fromState, toState, conditionName }
441
+ );
441
442
  }
442
443
  this.transitionSpecs.push({
443
444
  fromState,
@@ -471,6 +472,16 @@ var ProcessBuilder = class {
471
472
  );
472
473
  }
473
474
  // --- private helpers ---
475
+ /** One name rule for every named entity: non-empty, no leading/trailing whitespace. */
476
+ validateName(code, raw, description, details) {
477
+ if (raw.trim() === "" || raw !== raw.trim()) {
478
+ throw new GraphValidationError(
479
+ code,
480
+ `${description}: name ${JSON.stringify(raw)} is empty or whitespace-padded`,
481
+ details
482
+ );
483
+ }
484
+ }
474
485
  validateInitialState() {
475
486
  const initials = Array.from(this.stateSpecs.values()).filter(
476
487
  (s) => s.initial
@@ -522,22 +533,29 @@ var ProcessBuilder = class {
522
533
  }
523
534
  }
524
535
  }
536
+ /** Transition identity: (fromState, eventName, toState). Used by both the
537
+ * conflict check and the build-time dedup — keep them in lockstep. */
538
+ static transitionKey(t) {
539
+ return `${t.fromState}\0${t.eventName ?? ""}\0${t.toState}`;
540
+ }
525
541
  validateNoConflictingDuplicates() {
526
542
  const seen = /* @__PURE__ */ new Map();
527
543
  for (const t of this.transitionSpecs) {
528
- const key = `${t.fromState}\0${t.eventName ?? ""}\0${t.toState}`;
544
+ const key = _ProcessBuilder.transitionKey(t);
529
545
  const existing = seen.get(key);
530
546
  if (!existing) {
531
547
  seen.set(key, t);
532
548
  continue;
533
549
  }
534
- if (existing.condition !== t.condition) {
550
+ if (existing.condition !== t.condition || existing.weight !== t.weight) {
535
551
  throw new DuplicateTransitionError({
536
552
  fromState: t.fromState,
537
553
  toState: t.toState,
538
554
  eventName: t.eventName,
539
555
  existingConditionName: existing.condition ? existing.condition.getName() : null,
540
- newConditionName: t.condition ? t.condition.getName() : null
556
+ newConditionName: t.condition ? t.condition.getName() : null,
557
+ existingWeight: existing.weight,
558
+ newWeight: t.weight
541
559
  });
542
560
  }
543
561
  }
@@ -581,23 +599,12 @@ var ProcessBuilder = class {
581
599
  );
582
600
  }
583
601
  const dedupSeen = /* @__PURE__ */ new Set();
584
- const conditionId = /* @__PURE__ */ new Map();
585
- let nextConditionId = 0;
586
- const idForCondition = (cond) => {
587
- if (cond === null) return "";
588
- let id = conditionId.get(cond);
589
- if (id === void 0) {
590
- id = ++nextConditionId;
591
- conditionId.set(cond, id);
592
- }
593
- return String(id);
594
- };
595
602
  const transitionsByState = /* @__PURE__ */ new Map();
596
603
  for (const spec of this.stateSpecs.values()) {
597
604
  transitionsByState.set(spec.name, []);
598
605
  }
599
606
  for (const tSpec of this.transitionSpecs) {
600
- const dedupKey = `${tSpec.fromState}\0${tSpec.eventName ?? ""}\0${tSpec.toState}\0${idForCondition(tSpec.condition)}`;
607
+ const dedupKey = _ProcessBuilder.transitionKey(tSpec);
601
608
  if (dedupSeen.has(dedupKey)) continue;
602
609
  dedupSeen.add(dedupKey);
603
610
  const targetState = built.get(tSpec.toState);
@@ -689,27 +696,6 @@ var NullMutex = class {
689
696
  }
690
697
  };
691
698
 
692
- // src/internal/Dispatcher.ts
693
- var Dispatcher = class {
694
- commands = [];
695
- ready = false;
696
- dispatch(event, args = []) {
697
- if (this.ready) {
698
- throw new Error("Was already invoked!");
699
- }
700
- this.commands.push({ event, args });
701
- }
702
- async invoke() {
703
- if (this.ready) {
704
- throw new Error("Was already invoked!");
705
- }
706
- for (const { event, args } of this.commands) {
707
- await event.invoke(...args);
708
- }
709
- this.ready = true;
710
- }
711
- };
712
-
713
699
  // src/internal/OperationQueue.ts
714
700
  var OperationQueue = class {
715
701
  items = [];
@@ -726,10 +712,11 @@ var OperationQueue = class {
726
712
 
727
713
  // src/filter/ActiveTransitionFilter.ts
728
714
  var ActiveTransitionFilter = class {
729
- static async filter(transitions, subject, context, event) {
715
+ static async filter(transitions, subject, context, event, wrap) {
716
+ const run = wrap ?? ((fn) => fn());
730
717
  const active = [];
731
718
  for (const transition of transitions) {
732
- if (await transition.isActive(subject, context, event)) {
719
+ if (await run(() => transition.isActive(subject, context, event))) {
733
720
  active.push(transition);
734
721
  }
735
722
  }
@@ -762,16 +749,26 @@ var LockCanNotBeAcquiredError = class extends FinitaError {
762
749
  // src/error/AutomaticTransitionCycleError.ts
763
750
  var AutomaticTransitionCycleError = class extends FinitaError {
764
751
  code = "automaticTransitionCycle";
765
- targetStateName;
766
- visitedStateNames;
767
- constructor(targetStateName, visitedStateNames) {
768
- const visited = Array.from(visitedStateNames);
752
+ stateName;
753
+ hopLimit;
754
+ constructor(stateName, hopLimit) {
769
755
  super(
770
- `Automatic transition cycle detected: state "${targetStateName}" was already visited \u2014 this would cause infinite recursion`
756
+ `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.`
771
757
  );
772
758
  this.name = "AutomaticTransitionCycleError";
773
- this.targetStateName = targetStateName;
774
- this.visitedStateNames = Object.freeze([...visited]);
759
+ this.stateName = stateName;
760
+ this.hopLimit = hopLimit;
761
+ }
762
+ };
763
+
764
+ // src/error/ReentrancyError.ts
765
+ var ReentrancyError = class extends FinitaError {
766
+ code = "reentrancy";
767
+ constructor(operation) {
768
+ super(
769
+ `${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(...)).`
770
+ );
771
+ this.name = "ReentrancyError";
775
772
  }
776
773
  };
777
774
 
@@ -784,17 +781,26 @@ var Statemachine = class {
784
781
  currentState;
785
782
  lastState = null;
786
783
  autoreleaseLock;
784
+ maxAutomaticHops;
787
785
  queue = new OperationQueue();
788
786
  running = false;
787
+ inSyncCallback = false;
789
788
  beforeObservers = [];
790
789
  afterObservers = [];
791
790
  constructor(subject, process, options = {}) {
792
791
  this.subject = subject;
793
792
  this.process = process;
794
- this.currentState = options.initialStateName ? process.getState(options.initialStateName) : process.getInitialState();
793
+ this.currentState = options.initialStateName !== void 0 ? process.getState(options.initialStateName) : process.getInitialState();
795
794
  this.transitionSelector = options.transitionSelector ?? new OneOrNoneActiveTransition();
796
795
  this.mutex = options.mutex ?? new NullMutex();
797
796
  this.autoreleaseLock = options.autoreleaseLock ?? true;
797
+ const hops = options.maxAutomaticHops ?? 100;
798
+ if (!Number.isInteger(hops) || hops < 1) {
799
+ throw new RangeError(
800
+ `maxAutomaticHops must be a positive integer; got ${String(options.maxAutomaticHops)}`
801
+ );
802
+ }
803
+ this.maxAutomaticHops = hops;
798
804
  }
799
805
  // --- public getters ---
800
806
  getCurrentState() {
@@ -849,27 +855,45 @@ var Statemachine = class {
849
855
  // --- public top-level operations ---
850
856
  triggerEvent(name, context) {
851
857
  return new Promise((resolve, reject) => {
852
- this.queue.enqueue({
853
- kind: "triggerEvent",
854
- eventName: name,
855
- context: context ?? /* @__PURE__ */ new Map(),
856
- resolve,
857
- reject
858
- });
859
- void this.runIfIdle();
858
+ this.assertNotReentrant(`triggerEvent("${name}")`);
859
+ this.enqueueOperation(name, context, resolve, reject);
860
860
  });
861
861
  }
862
862
  checkTransitions(context) {
863
863
  return new Promise((resolve, reject) => {
864
- this.queue.enqueue({
865
- kind: "checkTransitions",
866
- eventName: null,
867
- context: context ?? /* @__PURE__ */ new Map(),
868
- resolve,
869
- reject
870
- });
871
- void this.runIfIdle();
864
+ this.assertNotReentrant("checkTransitions()");
865
+ this.enqueueOperation(null, context, resolve, reject);
866
+ });
867
+ }
868
+ /** Runs fn with the re-entrancy flag set for its SYNCHRONOUS portion only:
869
+ * the flag is cleared as soon as fn returns (before any promise it returned
870
+ * is awaited), so concurrent external callers are never affected. This
871
+ * catches triggerEvent/checkTransitions calls made before a callback's first
872
+ * await; calls made after a prior await are not detectable without
873
+ * AsyncLocalStorage (Node-only) and will still deadlock — a documented gap. */
874
+ guardSync(fn) {
875
+ this.inSyncCallback = true;
876
+ try {
877
+ return fn();
878
+ } finally {
879
+ this.inSyncCallback = false;
880
+ }
881
+ }
882
+ assertNotReentrant(operation) {
883
+ if (this.inSyncCallback) {
884
+ throw new ReentrancyError(operation);
885
+ }
886
+ }
887
+ /** Single entry point to the operation queue — every enqueue kicks the runner. */
888
+ enqueueOperation(eventName, context, resolve, reject, ifStateName) {
889
+ this.queue.enqueue({
890
+ eventName,
891
+ context: context ?? /* @__PURE__ */ new Map(),
892
+ ifStateName,
893
+ resolve,
894
+ reject
872
895
  });
896
+ void this.runIfIdle();
873
897
  }
874
898
  // --- internal runner ---
875
899
  async runIfIdle() {
@@ -885,7 +909,12 @@ var Statemachine = class {
885
909
  }
886
910
  }
887
911
  async runOperation(op) {
912
+ if (op.ifStateName !== void 0 && this.currentState.getName() !== op.ifStateName) {
913
+ op.resolve();
914
+ return;
915
+ }
888
916
  let acquiredHere = false;
917
+ let failure = null;
889
918
  try {
890
919
  if (!this.mutex.isAcquired()) {
891
920
  if (!await this.mutex.acquireLock()) {
@@ -893,19 +922,24 @@ var Statemachine = class {
893
922
  }
894
923
  acquiredHere = true;
895
924
  }
896
- const event = op.kind === "triggerEvent" ? this.resolveEvent(op.eventName) : null;
925
+ const event = op.eventName !== null ? this.resolveEvent(op.eventName) : null;
897
926
  await this.processOperation(event, op.context);
898
- op.resolve();
899
927
  } catch (err) {
900
- op.reject(err);
928
+ failure = { err };
901
929
  } finally {
902
930
  if (acquiredHere && this.autoreleaseLock) {
903
931
  try {
904
932
  await this.mutex.releaseLock();
905
- } catch {
933
+ } catch (err) {
934
+ if (!failure) failure = { err };
906
935
  }
907
936
  }
908
937
  }
938
+ if (failure) {
939
+ op.reject(failure.err);
940
+ } else {
941
+ op.resolve();
942
+ }
909
943
  }
910
944
  resolveEvent(name) {
911
945
  if (!this.currentState.hasEvent(name)) {
@@ -920,11 +954,13 @@ var Statemachine = class {
920
954
  */
921
955
  async processOperation(initialEvent, context) {
922
956
  let event = initialEvent;
923
- const automaticVisited = /* @__PURE__ */ new Set();
957
+ let automaticHops = 0;
924
958
  if (event) {
925
- const dispatcher = new Dispatcher();
926
- dispatcher.dispatch(event, [this.subject, context]);
927
- await dispatcher.invoke();
959
+ const userEvent = event;
960
+ const invokeArgs = [this.subject, context];
961
+ for (const observer of [...userEvent.getObservers()]) {
962
+ await this.guardSync(() => observer.update(userEvent, invokeArgs));
963
+ }
928
964
  }
929
965
  while (true) {
930
966
  const transitions = this.currentState.getTransitions();
@@ -932,26 +968,28 @@ var Statemachine = class {
932
968
  transitions,
933
969
  this.subject,
934
970
  context,
935
- event ?? void 0
971
+ event ?? void 0,
972
+ (fn) => this.guardSync(fn)
936
973
  );
937
- const selected = this.transitionSelector.selectTransition(
938
- active
974
+ const selected = this.guardSync(
975
+ () => this.transitionSelector.selectTransition(active)
939
976
  );
940
977
  if (!selected) {
941
978
  return;
942
979
  }
943
980
  const target = selected.getTargetState();
944
981
  if (selected.getEventName() === null) {
945
- automaticVisited.add(this.currentState);
946
- if (automaticVisited.has(target)) {
982
+ automaticHops += 1;
983
+ if (automaticHops > this.maxAutomaticHops) {
947
984
  throw new AutomaticTransitionCycleError(
948
985
  target.getName(),
949
- Array.from(automaticVisited).map((s) => s.getName())
986
+ this.maxAutomaticHops
950
987
  );
951
988
  }
952
989
  }
953
990
  if (this.currentState !== target) {
954
- const proposedFrame = Object.freeze({
991
+ const frame = Object.freeze({
992
+ subject: this.subject,
955
993
  fromState: this.currentState,
956
994
  toState: target,
957
995
  transition: selected,
@@ -961,39 +999,28 @@ var Statemachine = class {
961
999
  timestamp: Date.now(),
962
1000
  machineName: this.process.getName()
963
1001
  });
964
- for (const observer of this.beforeObservers) {
965
- await observer.notify(proposedFrame);
1002
+ for (const observer of [...this.beforeObservers]) {
1003
+ await this.guardSync(() => observer.notify(frame));
966
1004
  }
967
- const fromState = this.currentState;
968
- this.lastState = fromState;
1005
+ this.lastState = this.currentState;
969
1006
  this.currentState = target;
970
- const committedFrame = Object.freeze({
971
- fromState,
972
- toState: target,
973
- transition: selected,
974
- event,
975
- condition: selected.getCondition(),
976
- context: this.readonlyContext(context),
977
- timestamp: proposedFrame.timestamp,
978
- machineName: this.process.getName()
979
- });
980
1007
  const enqueueCtx = {
981
- enqueue: (chainedEventName, chainedCtx) => {
982
- this.queue.enqueue({
983
- kind: "triggerEvent",
984
- eventName: chainedEventName,
985
- context: chainedCtx ?? /* @__PURE__ */ new Map(),
986
- resolve: () => {
1008
+ enqueue: (chainedEventName, chainedCtx, ifStateName) => {
1009
+ this.enqueueOperation(
1010
+ chainedEventName,
1011
+ chainedCtx,
1012
+ () => {
987
1013
  },
988
- reject: () => {
989
- }
990
- });
1014
+ () => {
1015
+ },
1016
+ ifStateName
1017
+ );
991
1018
  }
992
1019
  };
993
1020
  const errors = [];
994
- for (const observer of this.afterObservers) {
1021
+ for (const observer of [...this.afterObservers]) {
995
1022
  try {
996
- await observer.notify(committedFrame, enqueueCtx);
1023
+ await this.guardSync(() => observer.notify(frame, enqueueCtx));
997
1024
  } catch (err) {
998
1025
  errors.push(err);
999
1026
  }
@@ -1060,6 +1087,23 @@ var CallbackCondition = class {
1060
1087
  }
1061
1088
  };
1062
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
+
1063
1107
  // src/condition/Timeout.ts
1064
1108
  function isLastStateHasChangedDate(obj) {
1065
1109
  return typeof obj === "object" && obj !== null && "getLastStateHasChangedDate" in obj && typeof obj.getLastStateHasChangedDate === "function";
@@ -1078,30 +1122,40 @@ var Timeout = class {
1078
1122
  if (isLastStateHasChangedDate(subject)) {
1079
1123
  return subject.getLastStateHasChangedDate();
1080
1124
  }
1081
- throw new Error("Subject must implement LastStateHasChangedDateInterface");
1125
+ throw new InvalidSubjectError("LastStateHasChangedDateInterface", [
1126
+ "getLastStateHasChangedDate"
1127
+ ]);
1082
1128
  }
1083
1129
  checkCondition(subject, context) {
1084
- const date = new Date(
1085
- this.getLastStateHasChangedDate(subject, context).getTime()
1086
- );
1087
- date.setTime(date.getTime() + this.timeoutMs);
1088
- return date <= /* @__PURE__ */ new Date();
1130
+ return this.getLastStateHasChangedDate(subject, context).getTime() + this.timeoutMs <= Date.now();
1089
1131
  }
1090
1132
  };
1091
1133
 
1092
- // src/condition/AndComposite.ts
1093
- var AndComposite = class {
1134
+ // src/condition/CompositeCondition.ts
1135
+ var CompositeCondition = class {
1094
1136
  conditions = [];
1095
- constructor(condition) {
1137
+ joinWord;
1138
+ constructor(joinWord, condition) {
1139
+ this.joinWord = joinWord;
1096
1140
  this.conditions.push(condition);
1097
1141
  }
1098
- addAnd(condition) {
1142
+ addCondition(condition) {
1099
1143
  this.conditions.push(condition);
1100
1144
  return this;
1101
1145
  }
1102
1146
  getName() {
1103
1147
  const names = this.conditions.map((c) => c.getName());
1104
- return `(${names.join(" and ")})`;
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);
1105
1159
  }
1106
1160
  async checkCondition(subject, context) {
1107
1161
  for (const condition of this.conditions) {
@@ -1114,18 +1168,12 @@ var AndComposite = class {
1114
1168
  };
1115
1169
 
1116
1170
  // src/condition/OrComposite.ts
1117
- var OrComposite = class {
1118
- conditions = [];
1171
+ var OrComposite = class extends CompositeCondition {
1119
1172
  constructor(condition) {
1120
- this.conditions.push(condition);
1173
+ super("or", condition);
1121
1174
  }
1122
1175
  addOr(condition) {
1123
- this.conditions.push(condition);
1124
- return this;
1125
- }
1126
- getName() {
1127
- const names = this.conditions.map((c) => c.getName());
1128
- return `(${names.join(" or ")})`;
1176
+ return this.addCondition(condition);
1129
1177
  }
1130
1178
  async checkCondition(subject, context) {
1131
1179
  for (const condition of this.conditions) {
@@ -1157,10 +1205,9 @@ var CallbackObserver = class {
1157
1205
  constructor(callback) {
1158
1206
  this.callback = callback;
1159
1207
  }
1160
- update(subject) {
1161
- const event = subject;
1162
- if (typeof event.getInvokeArgs === "function") {
1163
- return this.callback(...event.getInvokeArgs());
1208
+ update(subject, args) {
1209
+ if (args !== void 0) {
1210
+ return this.callback(...args);
1164
1211
  }
1165
1212
  return this.callback(subject);
1166
1213
  }
@@ -1169,11 +1216,19 @@ var CallbackObserver = class {
1169
1216
  // src/observer/StatefulStatusChanger.ts
1170
1217
  var StatefulStatusChanger = class {
1171
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
+ */
1172
1225
  constructor(subject) {
1173
- this.subject = subject;
1226
+ this.subject = subject ?? null;
1174
1227
  }
1175
1228
  notify(frame) {
1176
- this.subject.setCurrentStateName(frame.toState.getName());
1229
+ (this.subject ?? frame.subject).setCurrentStateName(
1230
+ frame.toState.getName()
1231
+ );
1177
1232
  }
1178
1233
  };
1179
1234
 
@@ -1186,19 +1241,25 @@ var OnEnterObserver = class _OnEnterObserver {
1186
1241
  }
1187
1242
  notify(frame, ctx) {
1188
1243
  if (frame.toState.hasEvent(this.eventName)) {
1189
- ctx.enqueue(this.eventName, new Map(frame.context));
1244
+ ctx.enqueue(
1245
+ this.eventName,
1246
+ new Map(frame.context),
1247
+ frame.toState.getName()
1248
+ );
1190
1249
  }
1191
1250
  }
1192
1251
  };
1193
1252
 
1194
- // src/observer/TransitionLogger.ts
1253
+ // src/util/index.ts
1195
1254
  function isNamed(obj) {
1196
1255
  return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
1197
1256
  }
1198
- function asString(obj) {
1257
+ function nameOrString(obj) {
1199
1258
  if (isNamed(obj)) return obj.getName();
1200
1259
  return String(obj);
1201
1260
  }
1261
+
1262
+ // src/observer/TransitionLogger.ts
1202
1263
  var TransitionLogger = class {
1203
1264
  logger;
1204
1265
  loggerLevel;
@@ -1208,7 +1269,7 @@ var TransitionLogger = class {
1208
1269
  }
1209
1270
  notify(frame) {
1210
1271
  let message = "Transition";
1211
- message += ` from "${asString(frame.fromState)}" to "${asString(frame.toState)}"`;
1272
+ message += ` from "${nameOrString(frame.fromState)}" to "${nameOrString(frame.toState)}"`;
1212
1273
  const eventName = frame.event ? frame.event.getName() : null;
1213
1274
  const conditionName = frame.condition ? frame.condition.getName() : null;
1214
1275
  if (eventName || conditionName) {
@@ -1319,19 +1380,16 @@ var WeightTransition = class {
1319
1380
  this.epsilon = epsilon;
1320
1381
  }
1321
1382
  selectTransition(transitions) {
1322
- let bestTransitions = [];
1323
- let bestWeight = null;
1324
- for (const transition of transitions) {
1383
+ const all = Array.from(transitions);
1384
+ let maxWeight = Number.NEGATIVE_INFINITY;
1385
+ for (const transition of all) {
1325
1386
  const weight = transition.getWeight();
1326
- const diff = weight - (bestWeight ?? 0);
1327
- if (bestWeight === null || diff >= this.epsilon) {
1328
- bestWeight = weight;
1329
- bestTransitions = [transition];
1330
- } else if (Math.abs(diff) < this.epsilon) {
1331
- bestTransitions.push(transition);
1332
- }
1387
+ if (weight > maxWeight) maxWeight = weight;
1333
1388
  }
1334
- return this.innerSelector.selectTransition(bestTransitions);
1389
+ const best = all.filter(
1390
+ (transition) => maxWeight - transition.getWeight() < this.epsilon
1391
+ );
1392
+ return this.innerSelector.selectTransition(best);
1335
1393
  }
1336
1394
  };
1337
1395
 
@@ -1474,23 +1532,6 @@ var AbstractNamedProcessDetector = class {
1474
1532
  }
1475
1533
  };
1476
1534
 
1477
- // src/error/InvalidSubjectError.ts
1478
- var InvalidSubjectError = class extends FinitaError {
1479
- code = "invalidSubject";
1480
- expectedInterface;
1481
- missingMembers;
1482
- constructor(expectedInterface, missingMembers) {
1483
- const members = Array.from(missingMembers);
1484
- const memberList = members.map((m) => `"${m}"`).join(", ");
1485
- super(
1486
- `Subject does not satisfy ${expectedInterface}; missing member(s): ${memberList || "(unknown)"}`
1487
- );
1488
- this.name = "InvalidSubjectError";
1489
- this.expectedInterface = expectedInterface;
1490
- this.missingMembers = Object.freeze([...members]);
1491
- }
1492
- };
1493
-
1494
1535
  // src/factory/StatefulStateNameDetector.ts
1495
1536
  function isStateful(obj) {
1496
1537
  return typeof obj === "object" && obj !== null && "getCurrentStateName" in obj && typeof obj.getCurrentStateName === "function";
@@ -1505,11 +1546,8 @@ var StatefulStateNameDetector = class {
1505
1546
  };
1506
1547
 
1507
1548
  // src/graph/GraphBuilder.ts
1508
- function isNamed2(obj) {
1509
- return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
1510
- }
1511
- function escapeDoubleQuotes(str) {
1512
- return str.replace(/"/g, '\\"');
1549
+ function escapeDotString(str) {
1550
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1513
1551
  }
1514
1552
  function toMermaidId(name) {
1515
1553
  return "s_" + name.replace(
@@ -1518,13 +1556,7 @@ function toMermaidId(name) {
1518
1556
  );
1519
1557
  }
1520
1558
  function escapeMermaidLabel(str) {
1521
- return str.replace(/"/g, "#quot;");
1522
- }
1523
- function convertToString(obj) {
1524
- if (isNamed2(obj)) {
1525
- return obj.getName();
1526
- }
1527
- return String(obj);
1559
+ return str.replace(/\\/g, "#92;").replace(/"/g, "#quot;");
1528
1560
  }
1529
1561
  var GraphBuilder = class {
1530
1562
  nodes = /* @__PURE__ */ new Map();
@@ -1544,13 +1576,15 @@ var GraphBuilder = class {
1544
1576
  const eventName = transition.getEventName();
1545
1577
  if (eventName) {
1546
1578
  parts.push(`E: ${eventName}`);
1547
- const event = state.getEvent(eventName);
1548
- const observerNames = [];
1549
- for (const observer of event.getObservers()) {
1550
- observerNames.push(convertToString(observer));
1551
- }
1552
- if (observerNames.length > 0) {
1553
- parts.push(`C: ${observerNames.join(", ")}`);
1579
+ if (state.hasEvent(eventName)) {
1580
+ const event = state.getEvent(eventName);
1581
+ const observerNames = [];
1582
+ for (const observer of event.getObservers()) {
1583
+ observerNames.push(nameOrString(observer));
1584
+ }
1585
+ if (observerNames.length > 0) {
1586
+ parts.push(`C: ${observerNames.join(", ")}`);
1587
+ }
1554
1588
  }
1555
1589
  }
1556
1590
  const conditionName = transition.getConditionName();
@@ -1603,13 +1637,13 @@ var GraphBuilder = class {
1603
1637
  lines.push("digraph {");
1604
1638
  lines.push(` rankdir=${rankdir};`);
1605
1639
  for (const node of graph.nodes) {
1606
- const label = escapeDoubleQuotes(node.label);
1640
+ const label = escapeDotString(node.label);
1607
1641
  lines.push(` "${label}" [label="${label}"];`);
1608
1642
  }
1609
1643
  for (const edge of graph.edges) {
1610
- const source = escapeDoubleQuotes(edge.source);
1611
- const target = escapeDoubleQuotes(edge.target);
1612
- const label = escapeDoubleQuotes(edge.label);
1644
+ const source = escapeDotString(edge.source);
1645
+ const target = escapeDotString(edge.target);
1646
+ const label = escapeDotString(edge.label);
1613
1647
  lines.push(` "${source}" -> "${target}" [label="${label}"];`);
1614
1648
  }
1615
1649
  lines.push("}");
@@ -1673,6 +1707,7 @@ var GraphBuilder = class {
1673
1707
  ProcessBuilder,
1674
1708
  ProcessFinalizedError,
1675
1709
  ProcessNotFoundError,
1710
+ ReentrancyError,
1676
1711
  ScoreTransition,
1677
1712
  SingleProcessDetector,
1678
1713
  State,