@camcima/finita 2.1.0 → 3.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
@@ -52,37 +52,148 @@ var Event = class {
52
52
  }
53
53
  };
54
54
 
55
- // src/State.ts
56
- var State = class {
55
+ // src/internal/InternalConstruction.ts
56
+ var INTERNAL_CONSTRUCTION_KEY = /* @__PURE__ */ Symbol(
57
+ "@camcima/finita/InternalConstruction"
58
+ );
59
+
60
+ // src/error/FinitaError.ts
61
+ var FinitaError = class _FinitaError extends Error {
62
+ constructor(message) {
63
+ super(message);
64
+ if (new.target === _FinitaError) {
65
+ throw new TypeError(
66
+ "FinitaError is abstract and cannot be instantiated directly"
67
+ );
68
+ }
69
+ }
70
+ };
71
+
72
+ // src/error/StateNotFoundError.ts
73
+ var StateNotFoundError = class extends FinitaError {
74
+ code = "stateNotFound";
75
+ stateName;
76
+ availableStates;
77
+ constructor(stateName, availableStates) {
78
+ const list = Array.from(availableStates);
79
+ const display = list.length > 0 ? list.map((n) => `"${n}"`).join(", ") : "(none)";
80
+ super(`State "${stateName}" not found. Available: ${display}`);
81
+ this.name = "StateNotFoundError";
82
+ this.stateName = stateName;
83
+ this.availableStates = Object.freeze([...list]);
84
+ }
85
+ };
86
+
87
+ // src/StateCollection.ts
88
+ var StateCollection = class {
89
+ states;
90
+ constructor(states) {
91
+ const map = /* @__PURE__ */ new Map();
92
+ for (const s of states) {
93
+ map.set(s.getName(), s);
94
+ }
95
+ this.states = map;
96
+ }
97
+ getStates() {
98
+ return this.states.values();
99
+ }
100
+ getState(name) {
101
+ const s = this.states.get(name);
102
+ if (!s) {
103
+ throw new StateNotFoundError(name, this.states.keys());
104
+ }
105
+ return s;
106
+ }
107
+ hasState(name) {
108
+ return this.states.has(name);
109
+ }
110
+ };
111
+
112
+ // src/Process.ts
113
+ var Process = class {
57
114
  name;
58
- transitions = /* @__PURE__ */ new Set();
59
- events = /* @__PURE__ */ new Map();
60
- metadata = /* @__PURE__ */ new Map();
61
- constructor(name) {
115
+ initialState;
116
+ states;
117
+ constructor(key, name, initialState, states) {
118
+ if (key !== INTERNAL_CONSTRUCTION_KEY) {
119
+ throw new Error("Process is not user-constructible; use ProcessBuilder.");
120
+ }
62
121
  this.name = name;
122
+ this.initialState = initialState;
123
+ this.states = new StateCollection(states);
124
+ Object.freeze(this);
63
125
  }
64
126
  getName() {
65
127
  return this.name;
66
128
  }
67
- getTransitions() {
68
- return this.transitions;
129
+ getInitialState() {
130
+ return this.initialState;
131
+ }
132
+ getStates() {
133
+ return this.states.getStates();
69
134
  }
70
- addTransition(transition) {
71
- if (this.transitions.has(transition)) {
72
- return;
135
+ getState(name) {
136
+ return this.states.getState(name);
137
+ }
138
+ hasState(name) {
139
+ return this.states.hasState(name);
140
+ }
141
+ };
142
+
143
+ // src/error/StateEventNotFoundError.ts
144
+ var StateEventNotFoundError = class extends FinitaError {
145
+ code = "stateEventNotFound";
146
+ stateName;
147
+ eventName;
148
+ constructor(stateName, eventName) {
149
+ super(`State "${stateName}" has no event "${eventName}"`);
150
+ this.name = "StateEventNotFoundError";
151
+ this.stateName = stateName;
152
+ this.eventName = eventName;
153
+ }
154
+ };
155
+
156
+ // src/State.ts
157
+ var State = class {
158
+ name;
159
+ _transitions = null;
160
+ events;
161
+ metadata;
162
+ constructor(key, name, eventNames, metadata) {
163
+ if (key !== INTERNAL_CONSTRUCTION_KEY) {
164
+ throw new Error("State is not user-constructible; use ProcessBuilder.");
73
165
  }
74
- const targetName = transition.getTargetState().getName();
75
- const eventName = transition.getEventName();
76
- const conditionName = transition.getConditionName();
77
- for (const existing of this.transitions) {
78
- if (existing.getTargetState().getName() === targetName && existing.getEventName() === eventName && existing.getConditionName() === conditionName) {
79
- return;
80
- }
166
+ this.name = name;
167
+ const events = /* @__PURE__ */ new Map();
168
+ for (const en of eventNames) {
169
+ events.set(en, new Event(en));
81
170
  }
82
- this.transitions.add(transition);
83
- if (eventName) {
84
- this.getEvent(eventName);
171
+ this.events = events;
172
+ this.metadata = new Map(metadata);
173
+ }
174
+ /**
175
+ * Internal: populate transitions after State construction.
176
+ * May only be called once and only with the construction key.
177
+ * Used by ProcessBuilder to break the cycle: State must exist before
178
+ * Transitions can target it, but State needs its transitions to be useful.
179
+ */
180
+ _initTransitions(key, transitions) {
181
+ if (key !== INTERNAL_CONSTRUCTION_KEY) {
182
+ throw new Error("_initTransitions is internal");
183
+ }
184
+ if (this._transitions !== null) {
185
+ throw new Error(`State "${this.name}" transitions already set`);
186
+ }
187
+ this._transitions = new Set(transitions);
188
+ }
189
+ getName() {
190
+ return this.name;
191
+ }
192
+ getTransitions() {
193
+ if (this._transitions === null) {
194
+ return [];
85
195
  }
196
+ return this._transitions;
86
197
  }
87
198
  getEventNames() {
88
199
  return Array.from(this.events.keys());
@@ -91,10 +202,9 @@ var State = class {
91
202
  return this.events.has(name);
92
203
  }
93
204
  getEvent(name) {
94
- let event = this.events.get(name);
205
+ const event = this.events.get(name);
95
206
  if (!event) {
96
- event = new Event(name);
97
- this.events.set(name, event);
207
+ throw new StateEventNotFoundError(this.name, name);
98
208
  }
99
209
  return event;
100
210
  }
@@ -104,15 +214,9 @@ var State = class {
104
214
  getMetadataValue(key) {
105
215
  return this.metadata.get(key);
106
216
  }
107
- setMetadataValue(key, value) {
108
- this.metadata.set(key, value);
109
- }
110
217
  hasMetadataValue(key) {
111
218
  return this.metadata.has(key);
112
219
  }
113
- deleteMetadataValue(key) {
114
- this.metadata.delete(key);
115
- }
116
220
  };
117
221
 
118
222
  // src/Transition.ts
@@ -120,11 +224,17 @@ var Transition = class {
120
224
  targetState;
121
225
  eventName;
122
226
  condition;
123
- weight = 1;
124
- constructor(targetState, eventName = null, condition = null) {
227
+ weight;
228
+ constructor(key, targetState, eventName, condition, weight) {
229
+ if (key !== INTERNAL_CONSTRUCTION_KEY) {
230
+ throw new Error(
231
+ "Transition is not user-constructible; use ProcessBuilder."
232
+ );
233
+ }
125
234
  this.targetState = targetState;
126
235
  this.eventName = eventName;
127
236
  this.condition = condition;
237
+ this.weight = weight;
128
238
  }
129
239
  getTargetState() {
130
240
  return this.targetState;
@@ -133,117 +243,31 @@ var Transition = class {
133
243
  return this.eventName;
134
244
  }
135
245
  getConditionName() {
136
- if (this.condition) {
137
- return this.condition.getName();
138
- }
139
- return null;
246
+ return this.condition ? this.condition.getName() : null;
140
247
  }
141
248
  getCondition() {
142
249
  return this.condition;
143
250
  }
144
251
  async isActive(subject, context, event) {
145
- let result;
252
+ let active;
146
253
  if (event) {
147
- result = event.getName() === this.eventName;
254
+ active = event.getName() === this.eventName;
148
255
  } else {
149
- result = this.eventName === null;
256
+ active = this.eventName === null;
150
257
  }
151
- if (this.condition && result) {
152
- result = await this.condition.checkCondition(subject, context);
258
+ if (this.condition && active) {
259
+ active = await this.condition.checkCondition(subject, context);
153
260
  }
154
- return result;
261
+ return active;
155
262
  }
156
263
  getWeight() {
157
264
  return this.weight;
158
265
  }
159
- setWeight(weight) {
160
- this.weight = weight;
161
- }
162
- };
163
-
164
- // src/util/StateCollectionMerger.ts
165
- var StateCollectionMerger = class {
166
- targetCollection;
167
- stateNamePrefix = "";
168
- constructor(targetCollection) {
169
- this.targetCollection = targetCollection;
170
- }
171
- getStateNamePrefix() {
172
- return this.stateNamePrefix;
173
- }
174
- setStateNamePrefix(prefix) {
175
- this.stateNamePrefix = prefix;
176
- }
177
- getTargetCollection() {
178
- return this.targetCollection;
179
- }
180
- createState(name) {
181
- return new State(name);
182
- }
183
- findOrCreateState(name) {
184
- const prefixedName = this.stateNamePrefix + name;
185
- if (this.targetCollection.hasState(prefixedName)) {
186
- return this.targetCollection.getState(prefixedName);
187
- }
188
- const state = this.createState(prefixedName);
189
- this.targetCollection.addState(state);
190
- return state;
191
- }
192
- createCondition(sourceTransition) {
193
- return sourceTransition.getCondition();
194
- }
195
- createTransition(sourceTransition) {
196
- const targetStateName = sourceTransition.getTargetState().getName();
197
- const targetState = this.findOrCreateState(targetStateName);
198
- this.mergeMetadata(sourceTransition.getTargetState(), targetState);
199
- const eventName = sourceTransition.getEventName();
200
- const condition = this.createCondition(sourceTransition);
201
- const transition = new Transition(targetState, eventName, condition);
202
- transition.setWeight(sourceTransition.getWeight());
203
- return transition;
204
- }
205
- mergeMetadata(source, target) {
206
- const metadata = source.getMetadata();
207
- for (const [key, value] of Object.entries(metadata)) {
208
- target.setMetadataValue(key, value);
209
- }
210
- }
211
- mergeEvent(source, target, eventName) {
212
- const sourceEvent = source.getEvent(eventName);
213
- const targetEvent = target.getEvent(eventName);
214
- const sourceMetadata = sourceEvent.getMetadata();
215
- for (const [key, value] of Object.entries(sourceMetadata)) {
216
- targetEvent.setMetadataValue(key, value);
217
- }
218
- for (const observer of sourceEvent.getObservers()) {
219
- targetEvent.attach(observer);
220
- }
221
- }
222
- mergeState(sourceState) {
223
- const targetState = this.findOrCreateState(sourceState.getName());
224
- this.mergeMetadata(sourceState, targetState);
225
- for (const sourceTransition of sourceState.getTransitions()) {
226
- const targetTransition = this.createTransition(sourceTransition);
227
- targetState.addTransition(targetTransition);
228
- }
229
- for (const eventName of sourceState.getEventNames()) {
230
- this.mergeEvent(sourceState, targetState, eventName);
231
- }
232
- }
233
- merge(source) {
234
- if ("getStates" in source && typeof source.getStates === "function") {
235
- const collection = source;
236
- for (const state of collection.getStates()) {
237
- this.mergeState(state);
238
- }
239
- } else {
240
- this.mergeState(source);
241
- }
242
- }
243
266
  };
244
267
 
245
268
  // src/error/DuplicateStateError.ts
246
- var DuplicateStateError = class extends Error {
269
+ var DuplicateStateError = class extends FinitaError {
270
+ code = "duplicateState";
247
271
  stateName;
248
272
  constructor(stateName) {
249
273
  super(
@@ -254,81 +278,309 @@ var DuplicateStateError = class extends Error {
254
278
  }
255
279
  };
256
280
 
257
- // src/StateCollection.ts
258
- var StateCollection = class {
259
- states = /* @__PURE__ */ new Map();
260
- stateCollectionMerger = null;
261
- getState(name) {
262
- const state = this.states.get(name);
263
- if (!state) {
264
- throw new Error(`State "${name}" not found`);
265
- }
266
- return state;
267
- }
268
- getStates() {
269
- return this.states.values();
270
- }
271
- hasState(name) {
272
- return this.states.has(name);
273
- }
274
- addState(state) {
275
- const existing = this.states.get(state.getName());
276
- if (existing && existing !== state) {
277
- throw new DuplicateStateError(state.getName());
278
- }
279
- this.states.set(state.getName(), state);
281
+ // src/error/ProcessFinalizedError.ts
282
+ var ProcessFinalizedError = class extends FinitaError {
283
+ code = "processFinalized";
284
+ processName;
285
+ constructor(processName) {
286
+ super(
287
+ `Process "${processName}" has already been built; ProcessBuilder.build() may only be called once`
288
+ );
289
+ this.name = "ProcessFinalizedError";
290
+ this.processName = processName;
280
291
  }
281
- getStateCollectionMerger() {
282
- if (!this.stateCollectionMerger) {
283
- this.stateCollectionMerger = new StateCollectionMerger(this);
284
- }
285
- return this.stateCollectionMerger;
292
+ };
293
+
294
+ // src/error/GraphValidationError.ts
295
+ var GraphValidationError = class extends FinitaError {
296
+ code;
297
+ details;
298
+ constructor(code, message, details = {}) {
299
+ super(`[${code}] ${message}`);
300
+ this.name = "GraphValidationError";
301
+ this.code = code;
302
+ this.details = Object.freeze({ ...details });
286
303
  }
287
- merge(source) {
288
- const merger = this.getStateCollectionMerger();
289
- merger.merge(source);
304
+ };
305
+
306
+ // src/error/DuplicateTransitionError.ts
307
+ var DuplicateTransitionError = class extends FinitaError {
308
+ code = "duplicateTransition";
309
+ conflict;
310
+ constructor(conflict) {
311
+ const eventLabel = conflict.eventName ?? "<automatic>";
312
+ const existing = conflict.existingConditionName ?? "<no condition>";
313
+ const incoming = conflict.newConditionName ?? "<no condition>";
314
+ super(
315
+ `Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"`
316
+ );
317
+ this.name = "DuplicateTransitionError";
318
+ this.conflict = Object.freeze({ ...conflict });
290
319
  }
291
320
  };
292
321
 
293
- // src/Process.ts
294
- var Process = class {
295
- name;
296
- initialState;
297
- states;
298
- constructor(name, initialState) {
299
- this.name = name;
300
- this.initialState = initialState;
301
- this.states = new StateCollection();
302
- this.registerState(initialState);
322
+ // src/ProcessBuilder.ts
323
+ var ProcessBuilder = class {
324
+ processName;
325
+ stateSpecs = /* @__PURE__ */ new Map();
326
+ transitionSpecs = [];
327
+ built = false;
328
+ constructor(processName) {
329
+ this.processName = processName;
330
+ }
331
+ addState(name, options = {}) {
332
+ if (this.built) {
333
+ throw new ProcessFinalizedError(this.processName);
334
+ }
335
+ if (this.stateSpecs.has(name)) {
336
+ throw new DuplicateStateError(name);
337
+ }
338
+ this.stateSpecs.set(name, {
339
+ name,
340
+ initial: options.initial === true,
341
+ metadata: new Map(Object.entries(options.metadata ?? {}))
342
+ });
343
+ return this;
303
344
  }
304
- registerState(state) {
305
- const name = state.getName();
306
- if (this.states.hasState(name)) {
307
- if (this.states.getState(name) !== state) {
308
- throw new DuplicateStateError(name);
345
+ addTransition(fromState, toState, options = {}) {
346
+ if (this.built) {
347
+ throw new ProcessFinalizedError(this.processName);
348
+ }
349
+ let eventName = null;
350
+ 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;
360
+ }
361
+ if (options.condition) {
362
+ 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
+ );
309
369
  }
310
- return;
311
370
  }
312
- this.states.addState(state);
313
- for (const transition of state.getTransitions()) {
314
- const targetState = transition.getTargetState();
315
- this.registerState(targetState);
371
+ this.transitionSpecs.push({
372
+ fromState,
373
+ toState,
374
+ eventName,
375
+ condition: options.condition ?? null,
376
+ weight: options.weight ?? 1
377
+ });
378
+ return this;
379
+ }
380
+ build(options = {}) {
381
+ if (this.built) {
382
+ throw new ProcessFinalizedError(this.processName);
383
+ }
384
+ this.built = true;
385
+ this.validateInitialState();
386
+ this.validateTransitionEndpoints();
387
+ this.validateNoConflictingDuplicates();
388
+ const initialName = this.findInitialStateName();
389
+ const eventNamesByState = this.collectEventNamesByState();
390
+ const finalStates = this.buildAllStates(eventNamesByState);
391
+ if (options.strictOrphans) {
392
+ this.validateOrphans(finalStates, initialName);
393
+ }
394
+ const initialState = finalStates.get(initialName);
395
+ return new Process(
396
+ INTERNAL_CONSTRUCTION_KEY,
397
+ this.processName,
398
+ initialState,
399
+ finalStates.values()
400
+ );
401
+ }
402
+ // --- private helpers ---
403
+ validateInitialState() {
404
+ const initials = Array.from(this.stateSpecs.values()).filter(
405
+ (s) => s.initial
406
+ );
407
+ if (initials.length === 0) {
408
+ throw new GraphValidationError(
409
+ "missingInitialState",
410
+ `Process "${this.processName}" has no state declared with { initial: true }`,
411
+ { processName: this.processName }
412
+ );
413
+ }
414
+ if (initials.length > 1) {
415
+ throw new GraphValidationError(
416
+ "multipleInitialStates",
417
+ `Process "${this.processName}" declares multiple initial states: ${initials.map((s) => `"${s.name}"`).join(", ")}`,
418
+ {
419
+ processName: this.processName,
420
+ initialStates: initials.map((s) => s.name)
421
+ }
422
+ );
316
423
  }
317
424
  }
318
- getName() {
319
- return this.name;
425
+ findInitialStateName() {
426
+ return Array.from(this.stateSpecs.values()).find((s) => s.initial).name;
320
427
  }
321
- getInitialState() {
322
- return this.initialState;
428
+ validateTransitionEndpoints() {
429
+ for (const t of this.transitionSpecs) {
430
+ if (!this.stateSpecs.has(t.fromState)) {
431
+ throw new GraphValidationError(
432
+ "unknownSource",
433
+ `Transition source state "${t.fromState}" was not declared with addState`,
434
+ {
435
+ fromState: t.fromState,
436
+ toState: t.toState,
437
+ eventName: t.eventName
438
+ }
439
+ );
440
+ }
441
+ if (!this.stateSpecs.has(t.toState)) {
442
+ throw new GraphValidationError(
443
+ "unknownTarget",
444
+ `Transition target state "${t.toState}" was not declared with addState`,
445
+ {
446
+ fromState: t.fromState,
447
+ toState: t.toState,
448
+ eventName: t.eventName
449
+ }
450
+ );
451
+ }
452
+ }
323
453
  }
324
- getStates() {
325
- return this.states.getStates();
454
+ validateNoConflictingDuplicates() {
455
+ const seen = /* @__PURE__ */ new Map();
456
+ for (const t of this.transitionSpecs) {
457
+ const key = `${t.fromState}\0${t.eventName ?? ""}\0${t.toState}`;
458
+ const existing = seen.get(key);
459
+ if (!existing) {
460
+ seen.set(key, t);
461
+ continue;
462
+ }
463
+ if (existing.condition !== t.condition) {
464
+ throw new DuplicateTransitionError({
465
+ fromState: t.fromState,
466
+ toState: t.toState,
467
+ eventName: t.eventName,
468
+ existingConditionName: existing.condition ? existing.condition.getName() : null,
469
+ newConditionName: t.condition ? t.condition.getName() : null
470
+ });
471
+ }
472
+ }
326
473
  }
327
- getState(name) {
328
- return this.states.getState(name);
474
+ collectEventNamesByState() {
475
+ const out = /* @__PURE__ */ new Map();
476
+ for (const t of this.transitionSpecs) {
477
+ if (t.eventName === null) continue;
478
+ let bucket = out.get(t.fromState);
479
+ if (!bucket) {
480
+ bucket = /* @__PURE__ */ new Set();
481
+ out.set(t.fromState, bucket);
482
+ }
483
+ bucket.add(t.eventName);
484
+ }
485
+ return new Map(
486
+ Array.from(out.entries()).map(([k, v]) => [k, Array.from(v)])
487
+ );
329
488
  }
330
- hasState(name) {
331
- return this.states.hasState(name);
489
+ /**
490
+ * Two-phase construction:
491
+ * Phase 1 — create all final State instances with no transitions.
492
+ * Phase 2 — build Transitions targeting the Phase-1 States, then attach
493
+ * them via State._initTransitions.
494
+ *
495
+ * Because every Transition is created after every State exists, target
496
+ * identity holds by construction for any graph topology (acyclic, cyclic,
497
+ * self-loop).
498
+ */
499
+ buildAllStates(eventNamesByState) {
500
+ const built = /* @__PURE__ */ new Map();
501
+ for (const spec of this.stateSpecs.values()) {
502
+ built.set(
503
+ spec.name,
504
+ new State(
505
+ INTERNAL_CONSTRUCTION_KEY,
506
+ spec.name,
507
+ eventNamesByState.get(spec.name) ?? [],
508
+ spec.metadata
509
+ )
510
+ );
511
+ }
512
+ 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
+ const transitionsByState = /* @__PURE__ */ new Map();
525
+ for (const spec of this.stateSpecs.values()) {
526
+ transitionsByState.set(spec.name, []);
527
+ }
528
+ for (const tSpec of this.transitionSpecs) {
529
+ const dedupKey = `${tSpec.fromState}\0${tSpec.eventName ?? ""}\0${tSpec.toState}\0${idForCondition(tSpec.condition)}`;
530
+ if (dedupSeen.has(dedupKey)) continue;
531
+ dedupSeen.add(dedupKey);
532
+ const targetState = built.get(tSpec.toState);
533
+ const transition = new Transition(
534
+ INTERNAL_CONSTRUCTION_KEY,
535
+ targetState,
536
+ tSpec.eventName,
537
+ tSpec.condition,
538
+ tSpec.weight
539
+ );
540
+ transitionsByState.get(tSpec.fromState).push(transition);
541
+ }
542
+ for (const [name, transitions] of transitionsByState) {
543
+ built.get(name)._initTransitions(
544
+ INTERNAL_CONSTRUCTION_KEY,
545
+ transitions
546
+ );
547
+ }
548
+ return built;
549
+ }
550
+ validateOrphans(states, initialName) {
551
+ const reachable = /* @__PURE__ */ new Set();
552
+ const queue = [initialName];
553
+ while (queue.length > 0) {
554
+ const name = queue.shift();
555
+ if (reachable.has(name)) continue;
556
+ reachable.add(name);
557
+ const s = states.get(name);
558
+ for (const t of s.getTransitions()) {
559
+ queue.push(t.getTargetState().getName());
560
+ }
561
+ }
562
+ const orphans = [];
563
+ for (const name of states.keys()) {
564
+ if (!reachable.has(name)) orphans.push(name);
565
+ }
566
+ if (orphans.length > 0) {
567
+ throw new GraphValidationError(
568
+ "orphanState",
569
+ `Process "${this.processName}" has unreachable states: ${orphans.map((n) => `"${n}"`).join(", ")}`,
570
+ { processName: this.processName, orphanStates: orphans }
571
+ );
572
+ }
573
+ }
574
+ };
575
+
576
+ // src/error/AmbiguousTransitionError.ts
577
+ var AmbiguousTransitionError = class extends FinitaError {
578
+ code = "ambiguousTransition";
579
+ activeCount;
580
+ constructor(activeCount) {
581
+ super(`More than one transition is active! (active count: ${activeCount})`);
582
+ this.name = "AmbiguousTransitionError";
583
+ this.activeCount = activeCount;
332
584
  }
333
585
  };
334
586
 
@@ -342,7 +594,7 @@ var OneOrNoneActiveTransition = class {
342
594
  case 1:
343
595
  return arr[0];
344
596
  default:
345
- throw new Error("More than one transition is active!");
597
+ throw new AmbiguousTransitionError(arr.length);
346
598
  }
347
599
  }
348
600
  };
@@ -366,19 +618,15 @@ var NullMutex = class {
366
618
  }
367
619
  };
368
620
 
369
- // src/Dispatcher.ts
621
+ // src/internal/Dispatcher.ts
370
622
  var Dispatcher = class {
371
623
  commands = [];
372
- onReadyCallbacks = [];
373
624
  ready = false;
374
- dispatch(event, args = [], onReadyCallback) {
625
+ dispatch(event, args = []) {
375
626
  if (this.ready) {
376
627
  throw new Error("Was already invoked!");
377
628
  }
378
629
  this.commands.push({ event, args });
379
- if (onReadyCallback) {
380
- this.onReadyCallbacks.push(onReadyCallback);
381
- }
382
630
  }
383
631
  async invoke() {
384
632
  if (this.ready) {
@@ -388,12 +636,20 @@ var Dispatcher = class {
388
636
  await event.invoke(...args);
389
637
  }
390
638
  this.ready = true;
391
- for (const callback of this.onReadyCallbacks) {
392
- await callback.invoke();
393
- }
394
639
  }
395
- isReady() {
396
- return this.ready;
640
+ };
641
+
642
+ // src/internal/OperationQueue.ts
643
+ var OperationQueue = class {
644
+ items = [];
645
+ enqueue(op) {
646
+ this.items.push(op);
647
+ }
648
+ dequeue() {
649
+ return this.items.shift();
650
+ }
651
+ isEmpty() {
652
+ return this.items.length === 0;
397
653
  }
398
654
  };
399
655
 
@@ -411,7 +667,8 @@ var ActiveTransitionFilter = class {
411
667
  };
412
668
 
413
669
  // src/error/WrongEventForStateError.ts
414
- var WrongEventForStateError = class extends Error {
670
+ var WrongEventForStateError = class extends FinitaError {
671
+ code = "wrongEventForState";
415
672
  stateName;
416
673
  eventName;
417
674
  constructor(stateName, eventName) {
@@ -423,41 +680,52 @@ var WrongEventForStateError = class extends Error {
423
680
  };
424
681
 
425
682
  // src/error/LockCanNotBeAcquiredError.ts
426
- var LockCanNotBeAcquiredError = class extends Error {
683
+ var LockCanNotBeAcquiredError = class extends FinitaError {
684
+ code = "lockCanNotBeAcquired";
427
685
  constructor(message = "Lock can not be acquired!") {
428
686
  super(message);
429
687
  this.name = "LockCanNotBeAcquiredError";
430
688
  }
431
689
  };
432
690
 
691
+ // src/error/AutomaticTransitionCycleError.ts
692
+ var AutomaticTransitionCycleError = class extends FinitaError {
693
+ code = "automaticTransitionCycle";
694
+ targetStateName;
695
+ visitedStateNames;
696
+ constructor(targetStateName, visitedStateNames) {
697
+ const visited = Array.from(visitedStateNames);
698
+ super(
699
+ `Automatic transition cycle detected: state "${targetStateName}" was already visited \u2014 this would cause infinite recursion`
700
+ );
701
+ this.name = "AutomaticTransitionCycleError";
702
+ this.targetStateName = targetStateName;
703
+ this.visitedStateNames = Object.freeze([...visited]);
704
+ }
705
+ };
706
+
433
707
  // src/Statemachine.ts
434
708
  var Statemachine = class {
435
709
  subject;
436
- currentState;
437
- lastState = null;
438
- transitionSelector;
439
- selectedTransition = null;
440
710
  process;
711
+ transitionSelector;
441
712
  mutex;
442
- autoreleaseLock = true;
443
- dispatcher = null;
444
- currentEvent = null;
445
- currentContext = null;
446
- observers = /* @__PURE__ */ new Set();
447
- constructor(subject, process, stateName, transitionSelector, mutex) {
713
+ currentState;
714
+ lastState = null;
715
+ autoreleaseLock;
716
+ queue = new OperationQueue();
717
+ running = false;
718
+ beforeObservers = [];
719
+ afterObservers = [];
720
+ constructor(subject, process, options = {}) {
448
721
  this.subject = subject;
449
- if (stateName) {
450
- this.currentState = process.getState(stateName);
451
- } else {
452
- this.currentState = process.getInitialState();
453
- }
454
- this.transitionSelector = transitionSelector ?? new OneOrNoneActiveTransition();
455
722
  this.process = process;
456
- this.mutex = mutex ?? new NullMutex();
457
- }
458
- getProcess() {
459
- return this.process;
723
+ this.currentState = options.initialStateName ? process.getState(options.initialStateName) : process.getInitialState();
724
+ this.transitionSelector = options.transitionSelector ?? new OneOrNoneActiveTransition();
725
+ this.mutex = options.mutex ?? new NullMutex();
726
+ this.autoreleaseLock = options.autoreleaseLock ?? true;
460
727
  }
728
+ // --- public getters ---
461
729
  getCurrentState() {
462
730
  return this.currentState;
463
731
  }
@@ -467,147 +735,32 @@ var Statemachine = class {
467
735
  getSubject() {
468
736
  return this.subject;
469
737
  }
470
- getSelectedTransition() {
471
- return this.selectedTransition;
472
- }
473
- getCurrentContext() {
474
- return this.currentContext;
475
- }
476
- async doCheckTransitions(context, event, automaticVisited) {
477
- try {
478
- const transitions = this.currentState.getTransitions();
479
- const activeTransitions = await ActiveTransitionFilter.filter(
480
- transitions,
481
- this.subject,
482
- context,
483
- event
484
- );
485
- this.selectedTransition = this.transitionSelector.selectTransition(activeTransitions);
486
- if (this.selectedTransition) {
487
- const targetState = this.selectedTransition.getTargetState();
488
- if (this.selectedTransition.getEventName() === null) {
489
- if (!automaticVisited) {
490
- automaticVisited = /* @__PURE__ */ new Set();
491
- }
492
- automaticVisited.add(this.currentState);
493
- if (automaticVisited.has(targetState)) {
494
- throw new Error(
495
- `Automatic transition cycle detected: state "${targetState.getName()}" was already visited \u2014 this would cause infinite recursion`
496
- );
497
- }
498
- }
499
- if (this.currentState !== targetState) {
500
- this.lastState = this.currentState;
501
- this.currentState = targetState;
502
- this.currentContext = context;
503
- this.currentEvent = event ?? null;
504
- try {
505
- await this.notify();
506
- } finally {
507
- this.currentContext = null;
508
- this.currentEvent = null;
509
- this.selectedTransition = null;
510
- this.lastState = null;
511
- }
512
- }
513
- await this.doCheckTransitions(context, void 0, automaticVisited);
514
- }
515
- } catch (error) {
516
- let message = `Exception was thrown when doing a transition from current state "${this.currentState.getName()}"`;
517
- if (this.currentEvent) {
518
- message += ` with event "${this.currentEvent.getName()}"`;
519
- }
520
- const named = this.subject;
521
- if (named && typeof named.getName === "function") {
522
- message += ` for "${named.getName()}"`;
523
- }
524
- throw new Error(message, { cause: error });
525
- }
526
- }
527
- async onDispatcherReady() {
528
- if (this.dispatcher && this.dispatcher.isReady()) {
529
- const context = this.currentContext;
530
- const event = this.currentEvent ?? void 0;
531
- this.dispatcher = null;
532
- this.currentContext = null;
533
- this.currentEvent = null;
534
- try {
535
- await this.doCheckTransitions(context, event);
536
- } finally {
537
- if (this.autoreleaseLock && this.isLockAcquired()) {
538
- await this.releaseLock();
539
- }
540
- }
541
- }
738
+ getProcess() {
739
+ return this.process;
542
740
  }
543
- async dispatchEvent(dispatcher, name, context) {
544
- if (this.dispatcher) {
545
- throw new Error("Event dispatching is still running!");
546
- }
547
- if (this.currentState.hasEvent(name)) {
548
- await this.acquireLockOrThrowException();
549
- try {
550
- this.dispatcher = dispatcher;
551
- this.currentContext = context ?? /* @__PURE__ */ new Map();
552
- this.currentEvent = this.currentState.getEvent(name);
553
- dispatcher.dispatch(
554
- this.currentEvent,
555
- [this.subject, this.currentContext],
556
- { invoke: () => this.onDispatcherReady() }
557
- );
558
- } catch (error) {
559
- this.dispatcher = null;
560
- this.currentContext = null;
561
- this.currentEvent = null;
562
- if (this.autoreleaseLock) {
563
- await this.releaseLock();
564
- }
565
- throw error;
566
- }
567
- } else {
568
- throw new WrongEventForStateError(this.currentState.getName(), name);
569
- }
741
+ // --- public observer attach/detach ---
742
+ attachBefore(observer) {
743
+ this.beforeObservers.push(observer);
570
744
  }
571
- async triggerEvent(name, context) {
572
- const dispatcher = new Dispatcher();
573
- try {
574
- await this.dispatchEvent(dispatcher, name, context);
575
- await dispatcher.invoke();
576
- } finally {
577
- this.dispatcher = null;
578
- this.currentContext = null;
579
- this.currentEvent = null;
580
- if (this.autoreleaseLock && this.isLockAcquired()) {
581
- await this.releaseLock();
582
- }
583
- }
745
+ detachBefore(observer) {
746
+ const idx = this.beforeObservers.indexOf(observer);
747
+ if (idx >= 0) this.beforeObservers.splice(idx, 1);
584
748
  }
585
- async checkTransitions(context) {
586
- await this.acquireLockOrThrowException();
587
- const ctx = context ?? /* @__PURE__ */ new Map();
588
- try {
589
- await this.doCheckTransitions(ctx);
590
- } finally {
591
- if (this.autoreleaseLock && this.isLockAcquired()) {
592
- await this.releaseLock();
593
- }
594
- }
749
+ getBeforeObservers() {
750
+ return this.beforeObservers;
595
751
  }
596
- async acquireLockOrThrowException() {
597
- if (!await this.acquireLock()) {
598
- throw new LockCanNotBeAcquiredError("Lock can not be acquired!");
599
- }
752
+ attachAfter(observer) {
753
+ this.afterObservers.push(observer);
600
754
  }
601
- isAutoreleaseLock() {
602
- return this.autoreleaseLock;
755
+ detachAfter(observer) {
756
+ const idx = this.afterObservers.indexOf(observer);
757
+ if (idx >= 0) this.afterObservers.splice(idx, 1);
603
758
  }
604
- setAutoreleaseLock(autorelease) {
605
- this.autoreleaseLock = autorelease;
759
+ getAfterObservers() {
760
+ return this.afterObservers;
606
761
  }
762
+ // --- public locking ---
607
763
  async acquireLock() {
608
- if (this.mutex.isAcquired()) {
609
- return true;
610
- }
611
764
  return this.mutex.acquireLock();
612
765
  }
613
766
  async releaseLock() {
@@ -616,19 +769,179 @@ var Statemachine = class {
616
769
  isLockAcquired() {
617
770
  return this.mutex.isAcquired();
618
771
  }
619
- attach(observer) {
620
- this.observers.add(observer);
772
+ isAutoreleaseLock() {
773
+ return this.autoreleaseLock;
621
774
  }
622
- detach(observer) {
623
- this.observers.delete(observer);
775
+ setAutoreleaseLock(autorelease) {
776
+ this.autoreleaseLock = autorelease;
624
777
  }
625
- async notify() {
626
- for (const observer of this.observers) {
627
- await observer.update(this);
778
+ // --- public top-level operations ---
779
+ triggerEvent(name, context) {
780
+ 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();
789
+ });
790
+ }
791
+ checkTransitions(context) {
792
+ 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();
801
+ });
802
+ }
803
+ // --- internal runner ---
804
+ async runIfIdle() {
805
+ if (this.running) return;
806
+ this.running = true;
807
+ try {
808
+ while (!this.queue.isEmpty()) {
809
+ const op = this.queue.dequeue();
810
+ await this.runOperation(op);
811
+ }
812
+ } finally {
813
+ this.running = false;
628
814
  }
629
815
  }
630
- getObservers() {
631
- return this.observers;
816
+ async runOperation(op) {
817
+ let acquiredHere = false;
818
+ try {
819
+ if (!this.mutex.isAcquired()) {
820
+ if (!await this.mutex.acquireLock()) {
821
+ throw new LockCanNotBeAcquiredError("Lock can not be acquired!");
822
+ }
823
+ acquiredHere = true;
824
+ }
825
+ const event = op.kind === "triggerEvent" ? this.resolveEvent(op.eventName) : null;
826
+ await this.processOperation(event, op.context);
827
+ op.resolve();
828
+ } catch (err) {
829
+ op.reject(err);
830
+ } finally {
831
+ if (acquiredHere && this.autoreleaseLock) {
832
+ try {
833
+ await this.mutex.releaseLock();
834
+ } catch {
835
+ }
836
+ }
837
+ }
838
+ }
839
+ resolveEvent(name) {
840
+ if (!this.currentState.hasEvent(name)) {
841
+ throw new WrongEventForStateError(this.currentState.getName(), name);
842
+ }
843
+ return this.currentState.getEvent(name);
844
+ }
845
+ /**
846
+ * Drive transitions starting from the current state, following automatic
847
+ * transitions until quiescent. The first iteration may use the supplied
848
+ * event; subsequent iterations are automatic.
849
+ */
850
+ async processOperation(initialEvent, context) {
851
+ let event = initialEvent;
852
+ const automaticVisited = /* @__PURE__ */ new Set();
853
+ if (event) {
854
+ const dispatcher = new Dispatcher();
855
+ dispatcher.dispatch(event, [this.subject, context]);
856
+ await dispatcher.invoke();
857
+ }
858
+ while (true) {
859
+ const transitions = this.currentState.getTransitions();
860
+ const active = await ActiveTransitionFilter.filter(
861
+ transitions,
862
+ this.subject,
863
+ context,
864
+ event ?? void 0
865
+ );
866
+ const selected = this.transitionSelector.selectTransition(
867
+ active
868
+ );
869
+ if (!selected) {
870
+ return;
871
+ }
872
+ const target = selected.getTargetState();
873
+ if (selected.getEventName() === null) {
874
+ automaticVisited.add(this.currentState);
875
+ if (automaticVisited.has(target)) {
876
+ throw new AutomaticTransitionCycleError(
877
+ target.getName(),
878
+ Array.from(automaticVisited).map((s) => s.getName())
879
+ );
880
+ }
881
+ }
882
+ if (this.currentState !== target) {
883
+ const proposedFrame = Object.freeze({
884
+ fromState: this.currentState,
885
+ toState: target,
886
+ transition: selected,
887
+ event,
888
+ condition: selected.getCondition(),
889
+ context: this.readonlyContext(context),
890
+ timestamp: Date.now(),
891
+ machineName: this.process.getName()
892
+ });
893
+ for (const observer of this.beforeObservers) {
894
+ await observer.notify(proposedFrame);
895
+ }
896
+ const fromState = this.currentState;
897
+ this.lastState = fromState;
898
+ 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
+ const enqueueCtx = {
910
+ enqueue: (chainedEventName, chainedCtx) => {
911
+ this.queue.enqueue({
912
+ kind: "triggerEvent",
913
+ eventName: chainedEventName,
914
+ context: chainedCtx ?? /* @__PURE__ */ new Map(),
915
+ resolve: () => {
916
+ },
917
+ reject: () => {
918
+ }
919
+ });
920
+ }
921
+ };
922
+ const errors = [];
923
+ for (const observer of this.afterObservers) {
924
+ try {
925
+ await observer.notify(committedFrame, enqueueCtx);
926
+ } catch (err) {
927
+ errors.push(err);
928
+ }
929
+ }
930
+ if (errors.length === 1) {
931
+ throw errors[0];
932
+ }
933
+ if (errors.length > 1) {
934
+ throw new AggregateError(
935
+ errors,
936
+ `${errors.length} after-transition observer(s) threw`
937
+ );
938
+ }
939
+ }
940
+ event = null;
941
+ }
942
+ }
943
+ readonlyContext(ctx) {
944
+ return new Map(ctx);
632
945
  }
633
946
  };
634
947
 
@@ -783,62 +1096,36 @@ var CallbackObserver = class {
783
1096
  };
784
1097
 
785
1098
  // src/observer/StatefulStatusChanger.ts
786
- function isStatemachine(obj) {
787
- return typeof obj === "object" && obj !== null && "getCurrentState" in obj && "getSubject" in obj;
788
- }
789
- function isStateful(obj) {
790
- return typeof obj === "object" && obj !== null && "setCurrentStateName" in obj && typeof obj.setCurrentStateName === "function";
791
- }
792
1099
  var StatefulStatusChanger = class {
793
- update(subject) {
794
- if (isStatemachine(subject)) {
795
- const subjectObj = subject.getSubject();
796
- if (isStateful(subjectObj)) {
797
- const stateName = subject.getCurrentState().getName();
798
- subjectObj.setCurrentStateName(stateName);
799
- }
800
- }
1100
+ subject;
1101
+ constructor(subject) {
1102
+ this.subject = subject;
1103
+ }
1104
+ notify(frame) {
1105
+ this.subject.setCurrentStateName(frame.toState.getName());
801
1106
  }
802
1107
  };
803
1108
 
804
1109
  // src/observer/OnEnterObserver.ts
805
- function isStatemachine2(obj) {
806
- return typeof obj === "object" && obj !== null && "getCurrentState" in obj && "triggerEvent" in obj;
807
- }
808
1110
  var OnEnterObserver = class _OnEnterObserver {
809
1111
  static DEFAULT_EVENT_NAME = "onEnter";
810
1112
  eventName;
811
1113
  constructor(eventName = _OnEnterObserver.DEFAULT_EVENT_NAME) {
812
1114
  this.eventName = eventName;
813
1115
  }
814
- async update(subject) {
815
- if (isStatemachine2(subject) && subject.getCurrentState().hasEvent(this.eventName)) {
816
- const sm = subject;
817
- const autorelease = sm.isAutoreleaseLock();
818
- sm.setAutoreleaseLock(false);
819
- try {
820
- await sm.triggerEvent(
821
- this.eventName,
822
- sm.getCurrentContext() ?? void 0
823
- );
824
- } finally {
825
- sm.setAutoreleaseLock(autorelease);
826
- }
1116
+ notify(frame, ctx) {
1117
+ if (frame.toState.hasEvent(this.eventName)) {
1118
+ ctx.enqueue(this.eventName, new Map(frame.context));
827
1119
  }
828
1120
  }
829
1121
  };
830
1122
 
831
1123
  // src/observer/TransitionLogger.ts
832
- function isStatemachine3(obj) {
833
- return typeof obj === "object" && obj !== null && "getCurrentState" in obj && "getSubject" in obj;
834
- }
835
1124
  function isNamed(obj) {
836
1125
  return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
837
1126
  }
838
- function convertToString(obj) {
839
- if (isNamed(obj)) {
840
- return obj.getName();
841
- }
1127
+ function asString(obj) {
1128
+ if (isNamed(obj)) return obj.getName();
842
1129
  return String(obj);
843
1130
  }
844
1131
  var TransitionLogger = class {
@@ -848,40 +1135,23 @@ var TransitionLogger = class {
848
1135
  this.logger = logger;
849
1136
  this.loggerLevel = loggerLevel;
850
1137
  }
851
- update(subject) {
852
- if (!isStatemachine3(subject)) {
853
- return;
854
- }
855
- const context = {};
856
- context["subject"] = subject.getSubject();
857
- context["currentState"] = subject.getCurrentState();
858
- context["lastState"] = subject.getLastState();
859
- context["transition"] = subject.getSelectedTransition();
1138
+ notify(frame) {
860
1139
  let message = "Transition";
861
- if (context["subject"] != null) {
862
- message += ` for "${convertToString(context["subject"])}"`;
863
- }
864
- if (context["lastState"] != null) {
865
- message += ` from "${convertToString(context["lastState"])}"`;
866
- }
867
- if (context["currentState"] != null) {
868
- message += ` to "${convertToString(context["currentState"])}"`;
869
- }
870
- const transition = context["transition"];
871
- if (transition && typeof transition.getEventName === "function") {
872
- const eventName = transition.getEventName();
873
- const condition = transition.getConditionName?.();
874
- if (eventName || condition) {
875
- message += " with";
876
- if (eventName) {
877
- message += ` event "${eventName}"`;
878
- }
879
- if (condition) {
880
- message += ` condition "${condition}"`;
881
- }
882
- }
883
- }
884
- this.logger.log(this.loggerLevel, message, context);
1140
+ message += ` from "${asString(frame.fromState)}" to "${asString(frame.toState)}"`;
1141
+ const eventName = frame.event ? frame.event.getName() : null;
1142
+ const conditionName = frame.condition ? frame.condition.getName() : null;
1143
+ if (eventName || conditionName) {
1144
+ message += " with";
1145
+ if (eventName) message += ` event "${eventName}"`;
1146
+ if (conditionName) message += ` condition "${conditionName}"`;
1147
+ }
1148
+ this.logger.log(this.loggerLevel, message, {
1149
+ fromState: frame.fromState,
1150
+ toState: frame.toState,
1151
+ event: frame.event,
1152
+ transition: frame.transition,
1153
+ machineName: frame.machineName
1154
+ });
885
1155
  }
886
1156
  };
887
1157
 
@@ -1047,7 +1317,8 @@ var MutexFactory = class {
1047
1317
  var Factory = class {
1048
1318
  processDetector;
1049
1319
  stateNameDetector;
1050
- statemachineObservers = /* @__PURE__ */ new Set();
1320
+ beforeObservers = /* @__PURE__ */ new Set();
1321
+ afterObservers = /* @__PURE__ */ new Set();
1051
1322
  transitionSelector = null;
1052
1323
  mutexFactory = null;
1053
1324
  constructor(processDetector, stateNameDetector) {
@@ -1060,30 +1331,30 @@ var Factory = class {
1060
1331
  setTransitionSelector(selector) {
1061
1332
  this.transitionSelector = selector;
1062
1333
  }
1063
- attachStatemachineObserver(observer) {
1064
- this.statemachineObservers.add(observer);
1334
+ attachBeforeObserver(observer) {
1335
+ this.beforeObservers.add(observer);
1065
1336
  }
1066
- detachStatemachineObserver(observer) {
1067
- this.statemachineObservers.delete(observer);
1337
+ detachBeforeObserver(observer) {
1338
+ this.beforeObservers.delete(observer);
1068
1339
  }
1069
- getStatemachineObservers() {
1070
- return this.statemachineObservers;
1340
+ attachAfterObserver(observer) {
1341
+ this.afterObservers.add(observer);
1342
+ }
1343
+ detachAfterObserver(observer) {
1344
+ this.afterObservers.delete(observer);
1071
1345
  }
1072
1346
  async createStatemachine(subject) {
1073
1347
  const process = this.processDetector.detectProcess(subject);
1074
- const stateName = this.stateNameDetector ? this.stateNameDetector.detectCurrentStateName(subject) : null;
1075
- const mutex = this.mutexFactory ? await this.mutexFactory.createMutex(subject) : null;
1076
- const statemachine = new Statemachine(
1077
- subject,
1078
- process,
1079
- stateName,
1080
- this.transitionSelector,
1081
- mutex
1082
- );
1083
- for (const observer of this.statemachineObservers) {
1084
- statemachine.attach(observer);
1085
- }
1086
- return statemachine;
1348
+ const stateName = this.stateNameDetector ? this.stateNameDetector.detectCurrentStateName(subject) : void 0;
1349
+ const mutex = this.mutexFactory ? await this.mutexFactory.createMutex(subject) : void 0;
1350
+ const sm = new Statemachine(subject, process, {
1351
+ initialStateName: stateName ?? void 0,
1352
+ transitionSelector: this.transitionSelector ?? void 0,
1353
+ mutex: mutex ?? void 0
1354
+ });
1355
+ for (const o of this.beforeObservers) sm.attachBefore(o);
1356
+ for (const o of this.afterObservers) sm.attachAfter(o);
1357
+ return sm;
1087
1358
  }
1088
1359
  };
1089
1360
 
@@ -1098,6 +1369,21 @@ var SingleProcessDetector = class {
1098
1369
  }
1099
1370
  };
1100
1371
 
1372
+ // src/error/ProcessNotFoundError.ts
1373
+ var ProcessNotFoundError = class extends FinitaError {
1374
+ code = "processNotFound";
1375
+ processName;
1376
+ availableProcesses;
1377
+ constructor(processName, availableProcesses) {
1378
+ const list = Array.from(availableProcesses);
1379
+ const display = list.length > 0 ? list.map((n) => `"${n}"`).join(", ") : "(none)";
1380
+ super(`Process "${processName}" not found. Available: ${display}`);
1381
+ this.name = "ProcessNotFoundError";
1382
+ this.processName = processName;
1383
+ this.availableProcesses = Object.freeze([...list]);
1384
+ }
1385
+ };
1386
+
1101
1387
  // src/factory/AbstractNamedProcessDetector.ts
1102
1388
  var AbstractNamedProcessDetector = class {
1103
1389
  processes = /* @__PURE__ */ new Map();
@@ -1111,74 +1397,39 @@ var AbstractNamedProcessDetector = class {
1111
1397
  const name = this.detectProcessName(subject);
1112
1398
  const process = this.processes.get(name);
1113
1399
  if (!process) {
1114
- throw new Error(`Process "${name}" not found`);
1400
+ throw new ProcessNotFoundError(name, this.processes.keys());
1115
1401
  }
1116
1402
  return process;
1117
1403
  }
1118
1404
  };
1119
1405
 
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
+
1120
1423
  // src/factory/StatefulStateNameDetector.ts
1121
- function isStateful2(obj) {
1424
+ function isStateful(obj) {
1122
1425
  return typeof obj === "object" && obj !== null && "getCurrentStateName" in obj && typeof obj.getCurrentStateName === "function";
1123
1426
  }
1124
1427
  var StatefulStateNameDetector = class {
1125
1428
  detectCurrentStateName(subject) {
1126
- if (isStateful2(subject)) {
1429
+ if (isStateful(subject)) {
1127
1430
  return subject.getCurrentStateName();
1128
1431
  }
1129
- throw new Error("Subject has to implement the StatefulInterface!");
1130
- }
1131
- };
1132
-
1133
- // src/util/SetupHelper.ts
1134
- var SetupHelper = class {
1135
- stateCollection;
1136
- constructor(stateCollection) {
1137
- this.stateCollection = stateCollection;
1138
- }
1139
- findOrCreateState(name) {
1140
- if (!this.stateCollection.hasState(name)) {
1141
- this.stateCollection.addState(new State(name));
1142
- }
1143
- return this.stateCollection.getState(name);
1144
- }
1145
- findTransition(sourceState, targetState, eventName = null, condition = null) {
1146
- const conditionName = condition ? condition.getName() : null;
1147
- for (const transition of sourceState.getTransitions()) {
1148
- const hasSameTargetState = transition.getTargetState() === targetState;
1149
- const hasSameCondition = transition.getConditionName() === conditionName;
1150
- const hasSameEvent = transition.getEventName() === eventName;
1151
- if (hasSameTargetState && hasSameCondition && hasSameEvent) {
1152
- return transition;
1153
- }
1154
- }
1155
- return null;
1156
- }
1157
- findOrCreateTransition(sourceStateName, targetStateName, eventName = null, condition = null) {
1158
- const sourceState = this.findOrCreateState(sourceStateName);
1159
- const targetState = this.findOrCreateState(targetStateName);
1160
- let transition = this.findTransition(
1161
- sourceState,
1162
- targetState,
1163
- eventName,
1164
- condition
1165
- );
1166
- if (!transition) {
1167
- transition = new Transition(targetState, eventName, condition);
1168
- sourceState.addTransition(transition);
1169
- }
1170
- return transition;
1171
- }
1172
- findOrCreateEvent(sourceStateName, eventName) {
1173
- const sourceState = this.findOrCreateState(sourceStateName);
1174
- return sourceState.getEvent(eventName);
1175
- }
1176
- addCommand(sourceStateName, eventName, command) {
1177
- this.findOrCreateEvent(sourceStateName, eventName).attach(command);
1178
- }
1179
- addCommandAndSelfTransition(sourceStateName, eventName, command) {
1180
- this.addCommand(sourceStateName, eventName, command);
1181
- this.findOrCreateTransition(sourceStateName, sourceStateName, eventName);
1432
+ throw new InvalidSubjectError("StatefulInterface", ["getCurrentStateName"]);
1182
1433
  }
1183
1434
  };
1184
1435
 
@@ -1198,7 +1449,7 @@ function toMermaidId(name) {
1198
1449
  function escapeMermaidLabel(str) {
1199
1450
  return str.replace(/"/g, "#quot;");
1200
1451
  }
1201
- function convertToString2(obj) {
1452
+ function convertToString(obj) {
1202
1453
  if (isNamed2(obj)) {
1203
1454
  return obj.getName();
1204
1455
  }
@@ -1207,6 +1458,7 @@ function convertToString2(obj) {
1207
1458
  var GraphBuilder = class {
1208
1459
  nodes = /* @__PURE__ */ new Map();
1209
1460
  edges = [];
1461
+ statesWithEdges = /* @__PURE__ */ new Set();
1210
1462
  getOrCreateNode(state) {
1211
1463
  const name = state.getName();
1212
1464
  let node = this.nodes.get(name);
@@ -1224,7 +1476,7 @@ var GraphBuilder = class {
1224
1476
  const event = state.getEvent(eventName);
1225
1477
  const observerNames = [];
1226
1478
  for (const observer of event.getObservers()) {
1227
- observerNames.push(convertToString2(observer));
1479
+ observerNames.push(convertToString(observer));
1228
1480
  }
1229
1481
  if (observerNames.length > 0) {
1230
1482
  parts.push(`C: ${observerNames.join(", ")}`);
@@ -1239,6 +1491,9 @@ var GraphBuilder = class {
1239
1491
  }
1240
1492
  addState(state) {
1241
1493
  this.getOrCreateNode(state);
1494
+ const name = state.getName();
1495
+ if (this.statesWithEdges.has(name)) return;
1496
+ this.statesWithEdges.add(name);
1242
1497
  for (const transition of state.getTransitions()) {
1243
1498
  const sourceNode = this.getOrCreateNode(state);
1244
1499
  const targetNode = this.getOrCreateNode(transition.getTargetState());
@@ -1316,19 +1571,24 @@ var GraphBuilder = class {
1316
1571
  export {
1317
1572
  AbstractNamedProcessDetector,
1318
1573
  ActiveTransitionFilter,
1574
+ AmbiguousTransitionError,
1319
1575
  AndComposite,
1576
+ AutomaticTransitionCycleError,
1320
1577
  CallbackCondition,
1321
1578
  CallbackObserver,
1322
1579
  Contradiction,
1323
- Dispatcher,
1324
1580
  DuplicateStateError,
1581
+ DuplicateTransitionError,
1325
1582
  Event,
1326
1583
  Factory,
1327
1584
  FilterStateByEvent,
1328
1585
  FilterStateByFinalState,
1329
1586
  FilterStateByTransition,
1330
1587
  FilterTransitionByEvent,
1588
+ FinitaError,
1331
1589
  GraphBuilder,
1590
+ GraphValidationError,
1591
+ InvalidSubjectError,
1332
1592
  LockAdapterMutex,
1333
1593
  LockCanNotBeAcquiredError,
1334
1594
  MutexFactory,
@@ -1338,12 +1598,14 @@ export {
1338
1598
  OneOrNoneActiveTransition,
1339
1599
  OrComposite,
1340
1600
  Process,
1601
+ ProcessBuilder,
1602
+ ProcessFinalizedError,
1603
+ ProcessNotFoundError,
1341
1604
  ScoreTransition,
1342
- SetupHelper,
1343
1605
  SingleProcessDetector,
1344
1606
  State,
1345
- StateCollection,
1346
- StateCollectionMerger,
1607
+ StateEventNotFoundError,
1608
+ StateNotFoundError,
1347
1609
  StatefulStateNameDetector,
1348
1610
  StatefulStatusChanger,
1349
1611
  Statemachine,