@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.cjs CHANGED
@@ -22,19 +22,24 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AbstractNamedProcessDetector: () => AbstractNamedProcessDetector,
24
24
  ActiveTransitionFilter: () => ActiveTransitionFilter,
25
+ AmbiguousTransitionError: () => AmbiguousTransitionError,
25
26
  AndComposite: () => AndComposite,
27
+ AutomaticTransitionCycleError: () => AutomaticTransitionCycleError,
26
28
  CallbackCondition: () => CallbackCondition,
27
29
  CallbackObserver: () => CallbackObserver,
28
30
  Contradiction: () => Contradiction,
29
- Dispatcher: () => Dispatcher,
30
31
  DuplicateStateError: () => DuplicateStateError,
32
+ DuplicateTransitionError: () => DuplicateTransitionError,
31
33
  Event: () => Event,
32
34
  Factory: () => Factory,
33
35
  FilterStateByEvent: () => FilterStateByEvent,
34
36
  FilterStateByFinalState: () => FilterStateByFinalState,
35
37
  FilterStateByTransition: () => FilterStateByTransition,
36
38
  FilterTransitionByEvent: () => FilterTransitionByEvent,
39
+ FinitaError: () => FinitaError,
37
40
  GraphBuilder: () => GraphBuilder,
41
+ GraphValidationError: () => GraphValidationError,
42
+ InvalidSubjectError: () => InvalidSubjectError,
38
43
  LockAdapterMutex: () => LockAdapterMutex,
39
44
  LockCanNotBeAcquiredError: () => LockCanNotBeAcquiredError,
40
45
  MutexFactory: () => MutexFactory,
@@ -44,12 +49,14 @@ __export(index_exports, {
44
49
  OneOrNoneActiveTransition: () => OneOrNoneActiveTransition,
45
50
  OrComposite: () => OrComposite,
46
51
  Process: () => Process,
52
+ ProcessBuilder: () => ProcessBuilder,
53
+ ProcessFinalizedError: () => ProcessFinalizedError,
54
+ ProcessNotFoundError: () => ProcessNotFoundError,
47
55
  ScoreTransition: () => ScoreTransition,
48
- SetupHelper: () => SetupHelper,
49
56
  SingleProcessDetector: () => SingleProcessDetector,
50
57
  State: () => State,
51
- StateCollection: () => StateCollection,
52
- StateCollectionMerger: () => StateCollectionMerger,
58
+ StateEventNotFoundError: () => StateEventNotFoundError,
59
+ StateNotFoundError: () => StateNotFoundError,
53
60
  StatefulStateNameDetector: () => StatefulStateNameDetector,
54
61
  StatefulStatusChanger: () => StatefulStatusChanger,
55
62
  Statemachine: () => Statemachine,
@@ -116,37 +123,148 @@ var Event = class {
116
123
  }
117
124
  };
118
125
 
119
- // src/State.ts
120
- var State = class {
126
+ // src/internal/InternalConstruction.ts
127
+ var INTERNAL_CONSTRUCTION_KEY = /* @__PURE__ */ Symbol(
128
+ "@camcima/finita/InternalConstruction"
129
+ );
130
+
131
+ // src/error/FinitaError.ts
132
+ var FinitaError = class _FinitaError extends Error {
133
+ constructor(message) {
134
+ super(message);
135
+ if (new.target === _FinitaError) {
136
+ throw new TypeError(
137
+ "FinitaError is abstract and cannot be instantiated directly"
138
+ );
139
+ }
140
+ }
141
+ };
142
+
143
+ // src/error/StateNotFoundError.ts
144
+ var StateNotFoundError = class extends FinitaError {
145
+ code = "stateNotFound";
146
+ stateName;
147
+ availableStates;
148
+ constructor(stateName, availableStates) {
149
+ const list = Array.from(availableStates);
150
+ const display = list.length > 0 ? list.map((n) => `"${n}"`).join(", ") : "(none)";
151
+ super(`State "${stateName}" not found. Available: ${display}`);
152
+ this.name = "StateNotFoundError";
153
+ this.stateName = stateName;
154
+ this.availableStates = Object.freeze([...list]);
155
+ }
156
+ };
157
+
158
+ // src/StateCollection.ts
159
+ var StateCollection = class {
160
+ states;
161
+ constructor(states) {
162
+ const map = /* @__PURE__ */ new Map();
163
+ for (const s of states) {
164
+ map.set(s.getName(), s);
165
+ }
166
+ this.states = map;
167
+ }
168
+ getStates() {
169
+ return this.states.values();
170
+ }
171
+ getState(name) {
172
+ const s = this.states.get(name);
173
+ if (!s) {
174
+ throw new StateNotFoundError(name, this.states.keys());
175
+ }
176
+ return s;
177
+ }
178
+ hasState(name) {
179
+ return this.states.has(name);
180
+ }
181
+ };
182
+
183
+ // src/Process.ts
184
+ var Process = class {
121
185
  name;
122
- transitions = /* @__PURE__ */ new Set();
123
- events = /* @__PURE__ */ new Map();
124
- metadata = /* @__PURE__ */ new Map();
125
- constructor(name) {
186
+ initialState;
187
+ states;
188
+ constructor(key, name, initialState, states) {
189
+ if (key !== INTERNAL_CONSTRUCTION_KEY) {
190
+ throw new Error("Process is not user-constructible; use ProcessBuilder.");
191
+ }
126
192
  this.name = name;
193
+ this.initialState = initialState;
194
+ this.states = new StateCollection(states);
195
+ Object.freeze(this);
127
196
  }
128
197
  getName() {
129
198
  return this.name;
130
199
  }
131
- getTransitions() {
132
- return this.transitions;
200
+ getInitialState() {
201
+ return this.initialState;
202
+ }
203
+ getStates() {
204
+ return this.states.getStates();
133
205
  }
134
- addTransition(transition) {
135
- if (this.transitions.has(transition)) {
136
- return;
206
+ getState(name) {
207
+ return this.states.getState(name);
208
+ }
209
+ hasState(name) {
210
+ return this.states.hasState(name);
211
+ }
212
+ };
213
+
214
+ // src/error/StateEventNotFoundError.ts
215
+ var StateEventNotFoundError = class extends FinitaError {
216
+ code = "stateEventNotFound";
217
+ stateName;
218
+ eventName;
219
+ constructor(stateName, eventName) {
220
+ super(`State "${stateName}" has no event "${eventName}"`);
221
+ this.name = "StateEventNotFoundError";
222
+ this.stateName = stateName;
223
+ this.eventName = eventName;
224
+ }
225
+ };
226
+
227
+ // src/State.ts
228
+ var State = class {
229
+ name;
230
+ _transitions = null;
231
+ events;
232
+ metadata;
233
+ constructor(key, name, eventNames, metadata) {
234
+ if (key !== INTERNAL_CONSTRUCTION_KEY) {
235
+ throw new Error("State is not user-constructible; use ProcessBuilder.");
137
236
  }
138
- const targetName = transition.getTargetState().getName();
139
- const eventName = transition.getEventName();
140
- const conditionName = transition.getConditionName();
141
- for (const existing of this.transitions) {
142
- if (existing.getTargetState().getName() === targetName && existing.getEventName() === eventName && existing.getConditionName() === conditionName) {
143
- return;
144
- }
237
+ this.name = name;
238
+ const events = /* @__PURE__ */ new Map();
239
+ for (const en of eventNames) {
240
+ events.set(en, new Event(en));
145
241
  }
146
- this.transitions.add(transition);
147
- if (eventName) {
148
- this.getEvent(eventName);
242
+ this.events = events;
243
+ this.metadata = new Map(metadata);
244
+ }
245
+ /**
246
+ * Internal: populate transitions after State construction.
247
+ * May only be called once and only with the construction key.
248
+ * Used by ProcessBuilder to break the cycle: State must exist before
249
+ * Transitions can target it, but State needs its transitions to be useful.
250
+ */
251
+ _initTransitions(key, transitions) {
252
+ if (key !== INTERNAL_CONSTRUCTION_KEY) {
253
+ throw new Error("_initTransitions is internal");
254
+ }
255
+ if (this._transitions !== null) {
256
+ throw new Error(`State "${this.name}" transitions already set`);
257
+ }
258
+ this._transitions = new Set(transitions);
259
+ }
260
+ getName() {
261
+ return this.name;
262
+ }
263
+ getTransitions() {
264
+ if (this._transitions === null) {
265
+ return [];
149
266
  }
267
+ return this._transitions;
150
268
  }
151
269
  getEventNames() {
152
270
  return Array.from(this.events.keys());
@@ -155,10 +273,9 @@ var State = class {
155
273
  return this.events.has(name);
156
274
  }
157
275
  getEvent(name) {
158
- let event = this.events.get(name);
276
+ const event = this.events.get(name);
159
277
  if (!event) {
160
- event = new Event(name);
161
- this.events.set(name, event);
278
+ throw new StateEventNotFoundError(this.name, name);
162
279
  }
163
280
  return event;
164
281
  }
@@ -168,15 +285,9 @@ var State = class {
168
285
  getMetadataValue(key) {
169
286
  return this.metadata.get(key);
170
287
  }
171
- setMetadataValue(key, value) {
172
- this.metadata.set(key, value);
173
- }
174
288
  hasMetadataValue(key) {
175
289
  return this.metadata.has(key);
176
290
  }
177
- deleteMetadataValue(key) {
178
- this.metadata.delete(key);
179
- }
180
291
  };
181
292
 
182
293
  // src/Transition.ts
@@ -184,11 +295,17 @@ var Transition = class {
184
295
  targetState;
185
296
  eventName;
186
297
  condition;
187
- weight = 1;
188
- constructor(targetState, eventName = null, condition = null) {
298
+ weight;
299
+ constructor(key, targetState, eventName, condition, weight) {
300
+ if (key !== INTERNAL_CONSTRUCTION_KEY) {
301
+ throw new Error(
302
+ "Transition is not user-constructible; use ProcessBuilder."
303
+ );
304
+ }
189
305
  this.targetState = targetState;
190
306
  this.eventName = eventName;
191
307
  this.condition = condition;
308
+ this.weight = weight;
192
309
  }
193
310
  getTargetState() {
194
311
  return this.targetState;
@@ -197,117 +314,31 @@ var Transition = class {
197
314
  return this.eventName;
198
315
  }
199
316
  getConditionName() {
200
- if (this.condition) {
201
- return this.condition.getName();
202
- }
203
- return null;
317
+ return this.condition ? this.condition.getName() : null;
204
318
  }
205
319
  getCondition() {
206
320
  return this.condition;
207
321
  }
208
322
  async isActive(subject, context, event) {
209
- let result;
323
+ let active;
210
324
  if (event) {
211
- result = event.getName() === this.eventName;
325
+ active = event.getName() === this.eventName;
212
326
  } else {
213
- result = this.eventName === null;
327
+ active = this.eventName === null;
214
328
  }
215
- if (this.condition && result) {
216
- result = await this.condition.checkCondition(subject, context);
329
+ if (this.condition && active) {
330
+ active = await this.condition.checkCondition(subject, context);
217
331
  }
218
- return result;
332
+ return active;
219
333
  }
220
334
  getWeight() {
221
335
  return this.weight;
222
336
  }
223
- setWeight(weight) {
224
- this.weight = weight;
225
- }
226
- };
227
-
228
- // src/util/StateCollectionMerger.ts
229
- var StateCollectionMerger = class {
230
- targetCollection;
231
- stateNamePrefix = "";
232
- constructor(targetCollection) {
233
- this.targetCollection = targetCollection;
234
- }
235
- getStateNamePrefix() {
236
- return this.stateNamePrefix;
237
- }
238
- setStateNamePrefix(prefix) {
239
- this.stateNamePrefix = prefix;
240
- }
241
- getTargetCollection() {
242
- return this.targetCollection;
243
- }
244
- createState(name) {
245
- return new State(name);
246
- }
247
- findOrCreateState(name) {
248
- const prefixedName = this.stateNamePrefix + name;
249
- if (this.targetCollection.hasState(prefixedName)) {
250
- return this.targetCollection.getState(prefixedName);
251
- }
252
- const state = this.createState(prefixedName);
253
- this.targetCollection.addState(state);
254
- return state;
255
- }
256
- createCondition(sourceTransition) {
257
- return sourceTransition.getCondition();
258
- }
259
- createTransition(sourceTransition) {
260
- const targetStateName = sourceTransition.getTargetState().getName();
261
- const targetState = this.findOrCreateState(targetStateName);
262
- this.mergeMetadata(sourceTransition.getTargetState(), targetState);
263
- const eventName = sourceTransition.getEventName();
264
- const condition = this.createCondition(sourceTransition);
265
- const transition = new Transition(targetState, eventName, condition);
266
- transition.setWeight(sourceTransition.getWeight());
267
- return transition;
268
- }
269
- mergeMetadata(source, target) {
270
- const metadata = source.getMetadata();
271
- for (const [key, value] of Object.entries(metadata)) {
272
- target.setMetadataValue(key, value);
273
- }
274
- }
275
- mergeEvent(source, target, eventName) {
276
- const sourceEvent = source.getEvent(eventName);
277
- const targetEvent = target.getEvent(eventName);
278
- const sourceMetadata = sourceEvent.getMetadata();
279
- for (const [key, value] of Object.entries(sourceMetadata)) {
280
- targetEvent.setMetadataValue(key, value);
281
- }
282
- for (const observer of sourceEvent.getObservers()) {
283
- targetEvent.attach(observer);
284
- }
285
- }
286
- mergeState(sourceState) {
287
- const targetState = this.findOrCreateState(sourceState.getName());
288
- this.mergeMetadata(sourceState, targetState);
289
- for (const sourceTransition of sourceState.getTransitions()) {
290
- const targetTransition = this.createTransition(sourceTransition);
291
- targetState.addTransition(targetTransition);
292
- }
293
- for (const eventName of sourceState.getEventNames()) {
294
- this.mergeEvent(sourceState, targetState, eventName);
295
- }
296
- }
297
- merge(source) {
298
- if ("getStates" in source && typeof source.getStates === "function") {
299
- const collection = source;
300
- for (const state of collection.getStates()) {
301
- this.mergeState(state);
302
- }
303
- } else {
304
- this.mergeState(source);
305
- }
306
- }
307
337
  };
308
338
 
309
339
  // src/error/DuplicateStateError.ts
310
- var DuplicateStateError = class extends Error {
340
+ var DuplicateStateError = class extends FinitaError {
341
+ code = "duplicateState";
311
342
  stateName;
312
343
  constructor(stateName) {
313
344
  super(
@@ -318,81 +349,309 @@ var DuplicateStateError = class extends Error {
318
349
  }
319
350
  };
320
351
 
321
- // src/StateCollection.ts
322
- var StateCollection = class {
323
- states = /* @__PURE__ */ new Map();
324
- stateCollectionMerger = null;
325
- getState(name) {
326
- const state = this.states.get(name);
327
- if (!state) {
328
- throw new Error(`State "${name}" not found`);
329
- }
330
- return state;
331
- }
332
- getStates() {
333
- return this.states.values();
334
- }
335
- hasState(name) {
336
- return this.states.has(name);
337
- }
338
- addState(state) {
339
- const existing = this.states.get(state.getName());
340
- if (existing && existing !== state) {
341
- throw new DuplicateStateError(state.getName());
342
- }
343
- this.states.set(state.getName(), state);
352
+ // src/error/ProcessFinalizedError.ts
353
+ var ProcessFinalizedError = class extends FinitaError {
354
+ code = "processFinalized";
355
+ processName;
356
+ constructor(processName) {
357
+ super(
358
+ `Process "${processName}" has already been built; ProcessBuilder.build() may only be called once`
359
+ );
360
+ this.name = "ProcessFinalizedError";
361
+ this.processName = processName;
344
362
  }
345
- getStateCollectionMerger() {
346
- if (!this.stateCollectionMerger) {
347
- this.stateCollectionMerger = new StateCollectionMerger(this);
348
- }
349
- return this.stateCollectionMerger;
363
+ };
364
+
365
+ // src/error/GraphValidationError.ts
366
+ var GraphValidationError = class extends FinitaError {
367
+ code;
368
+ details;
369
+ constructor(code, message, details = {}) {
370
+ super(`[${code}] ${message}`);
371
+ this.name = "GraphValidationError";
372
+ this.code = code;
373
+ this.details = Object.freeze({ ...details });
350
374
  }
351
- merge(source) {
352
- const merger = this.getStateCollectionMerger();
353
- merger.merge(source);
375
+ };
376
+
377
+ // src/error/DuplicateTransitionError.ts
378
+ var DuplicateTransitionError = class extends FinitaError {
379
+ code = "duplicateTransition";
380
+ conflict;
381
+ constructor(conflict) {
382
+ const eventLabel = conflict.eventName ?? "<automatic>";
383
+ const existing = conflict.existingConditionName ?? "<no condition>";
384
+ const incoming = conflict.newConditionName ?? "<no condition>";
385
+ super(
386
+ `Conflicting transition declarations from "${conflict.fromState}" to "${conflict.toState}" on event "${eventLabel}": existing condition "${existing}" vs new condition "${incoming}"`
387
+ );
388
+ this.name = "DuplicateTransitionError";
389
+ this.conflict = Object.freeze({ ...conflict });
354
390
  }
355
391
  };
356
392
 
357
- // src/Process.ts
358
- var Process = class {
359
- name;
360
- initialState;
361
- states;
362
- constructor(name, initialState) {
363
- this.name = name;
364
- this.initialState = initialState;
365
- this.states = new StateCollection();
366
- this.registerState(initialState);
393
+ // src/ProcessBuilder.ts
394
+ var ProcessBuilder = class {
395
+ processName;
396
+ stateSpecs = /* @__PURE__ */ new Map();
397
+ transitionSpecs = [];
398
+ built = false;
399
+ constructor(processName) {
400
+ this.processName = processName;
401
+ }
402
+ addState(name, options = {}) {
403
+ if (this.built) {
404
+ throw new ProcessFinalizedError(this.processName);
405
+ }
406
+ if (this.stateSpecs.has(name)) {
407
+ throw new DuplicateStateError(name);
408
+ }
409
+ this.stateSpecs.set(name, {
410
+ name,
411
+ initial: options.initial === true,
412
+ metadata: new Map(Object.entries(options.metadata ?? {}))
413
+ });
414
+ return this;
367
415
  }
368
- registerState(state) {
369
- const name = state.getName();
370
- if (this.states.hasState(name)) {
371
- if (this.states.getState(name) !== state) {
372
- throw new DuplicateStateError(name);
416
+ addTransition(fromState, toState, options = {}) {
417
+ if (this.built) {
418
+ throw new ProcessFinalizedError(this.processName);
419
+ }
420
+ let eventName = null;
421
+ 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;
431
+ }
432
+ if (options.condition) {
433
+ 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
+ );
373
440
  }
374
- return;
375
441
  }
376
- this.states.addState(state);
377
- for (const transition of state.getTransitions()) {
378
- const targetState = transition.getTargetState();
379
- this.registerState(targetState);
442
+ this.transitionSpecs.push({
443
+ fromState,
444
+ toState,
445
+ eventName,
446
+ condition: options.condition ?? null,
447
+ weight: options.weight ?? 1
448
+ });
449
+ return this;
450
+ }
451
+ build(options = {}) {
452
+ if (this.built) {
453
+ throw new ProcessFinalizedError(this.processName);
454
+ }
455
+ this.built = true;
456
+ this.validateInitialState();
457
+ this.validateTransitionEndpoints();
458
+ this.validateNoConflictingDuplicates();
459
+ const initialName = this.findInitialStateName();
460
+ const eventNamesByState = this.collectEventNamesByState();
461
+ const finalStates = this.buildAllStates(eventNamesByState);
462
+ if (options.strictOrphans) {
463
+ this.validateOrphans(finalStates, initialName);
464
+ }
465
+ const initialState = finalStates.get(initialName);
466
+ return new Process(
467
+ INTERNAL_CONSTRUCTION_KEY,
468
+ this.processName,
469
+ initialState,
470
+ finalStates.values()
471
+ );
472
+ }
473
+ // --- private helpers ---
474
+ validateInitialState() {
475
+ const initials = Array.from(this.stateSpecs.values()).filter(
476
+ (s) => s.initial
477
+ );
478
+ if (initials.length === 0) {
479
+ throw new GraphValidationError(
480
+ "missingInitialState",
481
+ `Process "${this.processName}" has no state declared with { initial: true }`,
482
+ { processName: this.processName }
483
+ );
484
+ }
485
+ if (initials.length > 1) {
486
+ throw new GraphValidationError(
487
+ "multipleInitialStates",
488
+ `Process "${this.processName}" declares multiple initial states: ${initials.map((s) => `"${s.name}"`).join(", ")}`,
489
+ {
490
+ processName: this.processName,
491
+ initialStates: initials.map((s) => s.name)
492
+ }
493
+ );
380
494
  }
381
495
  }
382
- getName() {
383
- return this.name;
496
+ findInitialStateName() {
497
+ return Array.from(this.stateSpecs.values()).find((s) => s.initial).name;
384
498
  }
385
- getInitialState() {
386
- return this.initialState;
499
+ validateTransitionEndpoints() {
500
+ for (const t of this.transitionSpecs) {
501
+ if (!this.stateSpecs.has(t.fromState)) {
502
+ throw new GraphValidationError(
503
+ "unknownSource",
504
+ `Transition source state "${t.fromState}" was not declared with addState`,
505
+ {
506
+ fromState: t.fromState,
507
+ toState: t.toState,
508
+ eventName: t.eventName
509
+ }
510
+ );
511
+ }
512
+ if (!this.stateSpecs.has(t.toState)) {
513
+ throw new GraphValidationError(
514
+ "unknownTarget",
515
+ `Transition target state "${t.toState}" was not declared with addState`,
516
+ {
517
+ fromState: t.fromState,
518
+ toState: t.toState,
519
+ eventName: t.eventName
520
+ }
521
+ );
522
+ }
523
+ }
387
524
  }
388
- getStates() {
389
- return this.states.getStates();
525
+ validateNoConflictingDuplicates() {
526
+ const seen = /* @__PURE__ */ new Map();
527
+ for (const t of this.transitionSpecs) {
528
+ const key = `${t.fromState}\0${t.eventName ?? ""}\0${t.toState}`;
529
+ const existing = seen.get(key);
530
+ if (!existing) {
531
+ seen.set(key, t);
532
+ continue;
533
+ }
534
+ if (existing.condition !== t.condition) {
535
+ throw new DuplicateTransitionError({
536
+ fromState: t.fromState,
537
+ toState: t.toState,
538
+ eventName: t.eventName,
539
+ existingConditionName: existing.condition ? existing.condition.getName() : null,
540
+ newConditionName: t.condition ? t.condition.getName() : null
541
+ });
542
+ }
543
+ }
390
544
  }
391
- getState(name) {
392
- return this.states.getState(name);
545
+ collectEventNamesByState() {
546
+ const out = /* @__PURE__ */ new Map();
547
+ for (const t of this.transitionSpecs) {
548
+ if (t.eventName === null) continue;
549
+ let bucket = out.get(t.fromState);
550
+ if (!bucket) {
551
+ bucket = /* @__PURE__ */ new Set();
552
+ out.set(t.fromState, bucket);
553
+ }
554
+ bucket.add(t.eventName);
555
+ }
556
+ return new Map(
557
+ Array.from(out.entries()).map(([k, v]) => [k, Array.from(v)])
558
+ );
393
559
  }
394
- hasState(name) {
395
- return this.states.hasState(name);
560
+ /**
561
+ * Two-phase construction:
562
+ * Phase 1 — create all final State instances with no transitions.
563
+ * Phase 2 — build Transitions targeting the Phase-1 States, then attach
564
+ * them via State._initTransitions.
565
+ *
566
+ * Because every Transition is created after every State exists, target
567
+ * identity holds by construction for any graph topology (acyclic, cyclic,
568
+ * self-loop).
569
+ */
570
+ buildAllStates(eventNamesByState) {
571
+ const built = /* @__PURE__ */ new Map();
572
+ for (const spec of this.stateSpecs.values()) {
573
+ built.set(
574
+ spec.name,
575
+ new State(
576
+ INTERNAL_CONSTRUCTION_KEY,
577
+ spec.name,
578
+ eventNamesByState.get(spec.name) ?? [],
579
+ spec.metadata
580
+ )
581
+ );
582
+ }
583
+ 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
+ const transitionsByState = /* @__PURE__ */ new Map();
596
+ for (const spec of this.stateSpecs.values()) {
597
+ transitionsByState.set(spec.name, []);
598
+ }
599
+ for (const tSpec of this.transitionSpecs) {
600
+ const dedupKey = `${tSpec.fromState}\0${tSpec.eventName ?? ""}\0${tSpec.toState}\0${idForCondition(tSpec.condition)}`;
601
+ if (dedupSeen.has(dedupKey)) continue;
602
+ dedupSeen.add(dedupKey);
603
+ const targetState = built.get(tSpec.toState);
604
+ const transition = new Transition(
605
+ INTERNAL_CONSTRUCTION_KEY,
606
+ targetState,
607
+ tSpec.eventName,
608
+ tSpec.condition,
609
+ tSpec.weight
610
+ );
611
+ transitionsByState.get(tSpec.fromState).push(transition);
612
+ }
613
+ for (const [name, transitions] of transitionsByState) {
614
+ built.get(name)._initTransitions(
615
+ INTERNAL_CONSTRUCTION_KEY,
616
+ transitions
617
+ );
618
+ }
619
+ return built;
620
+ }
621
+ validateOrphans(states, initialName) {
622
+ const reachable = /* @__PURE__ */ new Set();
623
+ const queue = [initialName];
624
+ while (queue.length > 0) {
625
+ const name = queue.shift();
626
+ if (reachable.has(name)) continue;
627
+ reachable.add(name);
628
+ const s = states.get(name);
629
+ for (const t of s.getTransitions()) {
630
+ queue.push(t.getTargetState().getName());
631
+ }
632
+ }
633
+ const orphans = [];
634
+ for (const name of states.keys()) {
635
+ if (!reachable.has(name)) orphans.push(name);
636
+ }
637
+ if (orphans.length > 0) {
638
+ throw new GraphValidationError(
639
+ "orphanState",
640
+ `Process "${this.processName}" has unreachable states: ${orphans.map((n) => `"${n}"`).join(", ")}`,
641
+ { processName: this.processName, orphanStates: orphans }
642
+ );
643
+ }
644
+ }
645
+ };
646
+
647
+ // src/error/AmbiguousTransitionError.ts
648
+ var AmbiguousTransitionError = class extends FinitaError {
649
+ code = "ambiguousTransition";
650
+ activeCount;
651
+ constructor(activeCount) {
652
+ super(`More than one transition is active! (active count: ${activeCount})`);
653
+ this.name = "AmbiguousTransitionError";
654
+ this.activeCount = activeCount;
396
655
  }
397
656
  };
398
657
 
@@ -406,7 +665,7 @@ var OneOrNoneActiveTransition = class {
406
665
  case 1:
407
666
  return arr[0];
408
667
  default:
409
- throw new Error("More than one transition is active!");
668
+ throw new AmbiguousTransitionError(arr.length);
410
669
  }
411
670
  }
412
671
  };
@@ -430,19 +689,15 @@ var NullMutex = class {
430
689
  }
431
690
  };
432
691
 
433
- // src/Dispatcher.ts
692
+ // src/internal/Dispatcher.ts
434
693
  var Dispatcher = class {
435
694
  commands = [];
436
- onReadyCallbacks = [];
437
695
  ready = false;
438
- dispatch(event, args = [], onReadyCallback) {
696
+ dispatch(event, args = []) {
439
697
  if (this.ready) {
440
698
  throw new Error("Was already invoked!");
441
699
  }
442
700
  this.commands.push({ event, args });
443
- if (onReadyCallback) {
444
- this.onReadyCallbacks.push(onReadyCallback);
445
- }
446
701
  }
447
702
  async invoke() {
448
703
  if (this.ready) {
@@ -452,12 +707,20 @@ var Dispatcher = class {
452
707
  await event.invoke(...args);
453
708
  }
454
709
  this.ready = true;
455
- for (const callback of this.onReadyCallbacks) {
456
- await callback.invoke();
457
- }
458
710
  }
459
- isReady() {
460
- return this.ready;
711
+ };
712
+
713
+ // src/internal/OperationQueue.ts
714
+ var OperationQueue = class {
715
+ items = [];
716
+ enqueue(op) {
717
+ this.items.push(op);
718
+ }
719
+ dequeue() {
720
+ return this.items.shift();
721
+ }
722
+ isEmpty() {
723
+ return this.items.length === 0;
461
724
  }
462
725
  };
463
726
 
@@ -475,7 +738,8 @@ var ActiveTransitionFilter = class {
475
738
  };
476
739
 
477
740
  // src/error/WrongEventForStateError.ts
478
- var WrongEventForStateError = class extends Error {
741
+ var WrongEventForStateError = class extends FinitaError {
742
+ code = "wrongEventForState";
479
743
  stateName;
480
744
  eventName;
481
745
  constructor(stateName, eventName) {
@@ -487,41 +751,52 @@ var WrongEventForStateError = class extends Error {
487
751
  };
488
752
 
489
753
  // src/error/LockCanNotBeAcquiredError.ts
490
- var LockCanNotBeAcquiredError = class extends Error {
754
+ var LockCanNotBeAcquiredError = class extends FinitaError {
755
+ code = "lockCanNotBeAcquired";
491
756
  constructor(message = "Lock can not be acquired!") {
492
757
  super(message);
493
758
  this.name = "LockCanNotBeAcquiredError";
494
759
  }
495
760
  };
496
761
 
762
+ // src/error/AutomaticTransitionCycleError.ts
763
+ var AutomaticTransitionCycleError = class extends FinitaError {
764
+ code = "automaticTransitionCycle";
765
+ targetStateName;
766
+ visitedStateNames;
767
+ constructor(targetStateName, visitedStateNames) {
768
+ const visited = Array.from(visitedStateNames);
769
+ super(
770
+ `Automatic transition cycle detected: state "${targetStateName}" was already visited \u2014 this would cause infinite recursion`
771
+ );
772
+ this.name = "AutomaticTransitionCycleError";
773
+ this.targetStateName = targetStateName;
774
+ this.visitedStateNames = Object.freeze([...visited]);
775
+ }
776
+ };
777
+
497
778
  // src/Statemachine.ts
498
779
  var Statemachine = class {
499
780
  subject;
500
- currentState;
501
- lastState = null;
502
- transitionSelector;
503
- selectedTransition = null;
504
781
  process;
782
+ transitionSelector;
505
783
  mutex;
506
- autoreleaseLock = true;
507
- dispatcher = null;
508
- currentEvent = null;
509
- currentContext = null;
510
- observers = /* @__PURE__ */ new Set();
511
- constructor(subject, process, stateName, transitionSelector, mutex) {
784
+ currentState;
785
+ lastState = null;
786
+ autoreleaseLock;
787
+ queue = new OperationQueue();
788
+ running = false;
789
+ beforeObservers = [];
790
+ afterObservers = [];
791
+ constructor(subject, process, options = {}) {
512
792
  this.subject = subject;
513
- if (stateName) {
514
- this.currentState = process.getState(stateName);
515
- } else {
516
- this.currentState = process.getInitialState();
517
- }
518
- this.transitionSelector = transitionSelector ?? new OneOrNoneActiveTransition();
519
793
  this.process = process;
520
- this.mutex = mutex ?? new NullMutex();
521
- }
522
- getProcess() {
523
- return this.process;
794
+ this.currentState = options.initialStateName ? process.getState(options.initialStateName) : process.getInitialState();
795
+ this.transitionSelector = options.transitionSelector ?? new OneOrNoneActiveTransition();
796
+ this.mutex = options.mutex ?? new NullMutex();
797
+ this.autoreleaseLock = options.autoreleaseLock ?? true;
524
798
  }
799
+ // --- public getters ---
525
800
  getCurrentState() {
526
801
  return this.currentState;
527
802
  }
@@ -531,147 +806,32 @@ var Statemachine = class {
531
806
  getSubject() {
532
807
  return this.subject;
533
808
  }
534
- getSelectedTransition() {
535
- return this.selectedTransition;
536
- }
537
- getCurrentContext() {
538
- return this.currentContext;
539
- }
540
- async doCheckTransitions(context, event, automaticVisited) {
541
- try {
542
- const transitions = this.currentState.getTransitions();
543
- const activeTransitions = await ActiveTransitionFilter.filter(
544
- transitions,
545
- this.subject,
546
- context,
547
- event
548
- );
549
- this.selectedTransition = this.transitionSelector.selectTransition(activeTransitions);
550
- if (this.selectedTransition) {
551
- const targetState = this.selectedTransition.getTargetState();
552
- if (this.selectedTransition.getEventName() === null) {
553
- if (!automaticVisited) {
554
- automaticVisited = /* @__PURE__ */ new Set();
555
- }
556
- automaticVisited.add(this.currentState);
557
- if (automaticVisited.has(targetState)) {
558
- throw new Error(
559
- `Automatic transition cycle detected: state "${targetState.getName()}" was already visited \u2014 this would cause infinite recursion`
560
- );
561
- }
562
- }
563
- if (this.currentState !== targetState) {
564
- this.lastState = this.currentState;
565
- this.currentState = targetState;
566
- this.currentContext = context;
567
- this.currentEvent = event ?? null;
568
- try {
569
- await this.notify();
570
- } finally {
571
- this.currentContext = null;
572
- this.currentEvent = null;
573
- this.selectedTransition = null;
574
- this.lastState = null;
575
- }
576
- }
577
- await this.doCheckTransitions(context, void 0, automaticVisited);
578
- }
579
- } catch (error) {
580
- let message = `Exception was thrown when doing a transition from current state "${this.currentState.getName()}"`;
581
- if (this.currentEvent) {
582
- message += ` with event "${this.currentEvent.getName()}"`;
583
- }
584
- const named = this.subject;
585
- if (named && typeof named.getName === "function") {
586
- message += ` for "${named.getName()}"`;
587
- }
588
- throw new Error(message, { cause: error });
589
- }
590
- }
591
- async onDispatcherReady() {
592
- if (this.dispatcher && this.dispatcher.isReady()) {
593
- const context = this.currentContext;
594
- const event = this.currentEvent ?? void 0;
595
- this.dispatcher = null;
596
- this.currentContext = null;
597
- this.currentEvent = null;
598
- try {
599
- await this.doCheckTransitions(context, event);
600
- } finally {
601
- if (this.autoreleaseLock && this.isLockAcquired()) {
602
- await this.releaseLock();
603
- }
604
- }
605
- }
809
+ getProcess() {
810
+ return this.process;
606
811
  }
607
- async dispatchEvent(dispatcher, name, context) {
608
- if (this.dispatcher) {
609
- throw new Error("Event dispatching is still running!");
610
- }
611
- if (this.currentState.hasEvent(name)) {
612
- await this.acquireLockOrThrowException();
613
- try {
614
- this.dispatcher = dispatcher;
615
- this.currentContext = context ?? /* @__PURE__ */ new Map();
616
- this.currentEvent = this.currentState.getEvent(name);
617
- dispatcher.dispatch(
618
- this.currentEvent,
619
- [this.subject, this.currentContext],
620
- { invoke: () => this.onDispatcherReady() }
621
- );
622
- } catch (error) {
623
- this.dispatcher = null;
624
- this.currentContext = null;
625
- this.currentEvent = null;
626
- if (this.autoreleaseLock) {
627
- await this.releaseLock();
628
- }
629
- throw error;
630
- }
631
- } else {
632
- throw new WrongEventForStateError(this.currentState.getName(), name);
633
- }
812
+ // --- public observer attach/detach ---
813
+ attachBefore(observer) {
814
+ this.beforeObservers.push(observer);
634
815
  }
635
- async triggerEvent(name, context) {
636
- const dispatcher = new Dispatcher();
637
- try {
638
- await this.dispatchEvent(dispatcher, name, context);
639
- await dispatcher.invoke();
640
- } finally {
641
- this.dispatcher = null;
642
- this.currentContext = null;
643
- this.currentEvent = null;
644
- if (this.autoreleaseLock && this.isLockAcquired()) {
645
- await this.releaseLock();
646
- }
647
- }
816
+ detachBefore(observer) {
817
+ const idx = this.beforeObservers.indexOf(observer);
818
+ if (idx >= 0) this.beforeObservers.splice(idx, 1);
648
819
  }
649
- async checkTransitions(context) {
650
- await this.acquireLockOrThrowException();
651
- const ctx = context ?? /* @__PURE__ */ new Map();
652
- try {
653
- await this.doCheckTransitions(ctx);
654
- } finally {
655
- if (this.autoreleaseLock && this.isLockAcquired()) {
656
- await this.releaseLock();
657
- }
658
- }
820
+ getBeforeObservers() {
821
+ return this.beforeObservers;
659
822
  }
660
- async acquireLockOrThrowException() {
661
- if (!await this.acquireLock()) {
662
- throw new LockCanNotBeAcquiredError("Lock can not be acquired!");
663
- }
823
+ attachAfter(observer) {
824
+ this.afterObservers.push(observer);
664
825
  }
665
- isAutoreleaseLock() {
666
- return this.autoreleaseLock;
826
+ detachAfter(observer) {
827
+ const idx = this.afterObservers.indexOf(observer);
828
+ if (idx >= 0) this.afterObservers.splice(idx, 1);
667
829
  }
668
- setAutoreleaseLock(autorelease) {
669
- this.autoreleaseLock = autorelease;
830
+ getAfterObservers() {
831
+ return this.afterObservers;
670
832
  }
833
+ // --- public locking ---
671
834
  async acquireLock() {
672
- if (this.mutex.isAcquired()) {
673
- return true;
674
- }
675
835
  return this.mutex.acquireLock();
676
836
  }
677
837
  async releaseLock() {
@@ -680,19 +840,179 @@ var Statemachine = class {
680
840
  isLockAcquired() {
681
841
  return this.mutex.isAcquired();
682
842
  }
683
- attach(observer) {
684
- this.observers.add(observer);
843
+ isAutoreleaseLock() {
844
+ return this.autoreleaseLock;
685
845
  }
686
- detach(observer) {
687
- this.observers.delete(observer);
846
+ setAutoreleaseLock(autorelease) {
847
+ this.autoreleaseLock = autorelease;
688
848
  }
689
- async notify() {
690
- for (const observer of this.observers) {
691
- await observer.update(this);
849
+ // --- public top-level operations ---
850
+ triggerEvent(name, context) {
851
+ 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();
860
+ });
861
+ }
862
+ checkTransitions(context) {
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();
872
+ });
873
+ }
874
+ // --- internal runner ---
875
+ async runIfIdle() {
876
+ if (this.running) return;
877
+ this.running = true;
878
+ try {
879
+ while (!this.queue.isEmpty()) {
880
+ const op = this.queue.dequeue();
881
+ await this.runOperation(op);
882
+ }
883
+ } finally {
884
+ this.running = false;
692
885
  }
693
886
  }
694
- getObservers() {
695
- return this.observers;
887
+ async runOperation(op) {
888
+ let acquiredHere = false;
889
+ try {
890
+ if (!this.mutex.isAcquired()) {
891
+ if (!await this.mutex.acquireLock()) {
892
+ throw new LockCanNotBeAcquiredError("Lock can not be acquired!");
893
+ }
894
+ acquiredHere = true;
895
+ }
896
+ const event = op.kind === "triggerEvent" ? this.resolveEvent(op.eventName) : null;
897
+ await this.processOperation(event, op.context);
898
+ op.resolve();
899
+ } catch (err) {
900
+ op.reject(err);
901
+ } finally {
902
+ if (acquiredHere && this.autoreleaseLock) {
903
+ try {
904
+ await this.mutex.releaseLock();
905
+ } catch {
906
+ }
907
+ }
908
+ }
909
+ }
910
+ resolveEvent(name) {
911
+ if (!this.currentState.hasEvent(name)) {
912
+ throw new WrongEventForStateError(this.currentState.getName(), name);
913
+ }
914
+ return this.currentState.getEvent(name);
915
+ }
916
+ /**
917
+ * Drive transitions starting from the current state, following automatic
918
+ * transitions until quiescent. The first iteration may use the supplied
919
+ * event; subsequent iterations are automatic.
920
+ */
921
+ async processOperation(initialEvent, context) {
922
+ let event = initialEvent;
923
+ const automaticVisited = /* @__PURE__ */ new Set();
924
+ if (event) {
925
+ const dispatcher = new Dispatcher();
926
+ dispatcher.dispatch(event, [this.subject, context]);
927
+ await dispatcher.invoke();
928
+ }
929
+ while (true) {
930
+ const transitions = this.currentState.getTransitions();
931
+ const active = await ActiveTransitionFilter.filter(
932
+ transitions,
933
+ this.subject,
934
+ context,
935
+ event ?? void 0
936
+ );
937
+ const selected = this.transitionSelector.selectTransition(
938
+ active
939
+ );
940
+ if (!selected) {
941
+ return;
942
+ }
943
+ const target = selected.getTargetState();
944
+ if (selected.getEventName() === null) {
945
+ automaticVisited.add(this.currentState);
946
+ if (automaticVisited.has(target)) {
947
+ throw new AutomaticTransitionCycleError(
948
+ target.getName(),
949
+ Array.from(automaticVisited).map((s) => s.getName())
950
+ );
951
+ }
952
+ }
953
+ if (this.currentState !== target) {
954
+ const proposedFrame = Object.freeze({
955
+ fromState: this.currentState,
956
+ toState: target,
957
+ transition: selected,
958
+ event,
959
+ condition: selected.getCondition(),
960
+ context: this.readonlyContext(context),
961
+ timestamp: Date.now(),
962
+ machineName: this.process.getName()
963
+ });
964
+ for (const observer of this.beforeObservers) {
965
+ await observer.notify(proposedFrame);
966
+ }
967
+ const fromState = this.currentState;
968
+ this.lastState = fromState;
969
+ 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
+ const enqueueCtx = {
981
+ enqueue: (chainedEventName, chainedCtx) => {
982
+ this.queue.enqueue({
983
+ kind: "triggerEvent",
984
+ eventName: chainedEventName,
985
+ context: chainedCtx ?? /* @__PURE__ */ new Map(),
986
+ resolve: () => {
987
+ },
988
+ reject: () => {
989
+ }
990
+ });
991
+ }
992
+ };
993
+ const errors = [];
994
+ for (const observer of this.afterObservers) {
995
+ try {
996
+ await observer.notify(committedFrame, enqueueCtx);
997
+ } catch (err) {
998
+ errors.push(err);
999
+ }
1000
+ }
1001
+ if (errors.length === 1) {
1002
+ throw errors[0];
1003
+ }
1004
+ if (errors.length > 1) {
1005
+ throw new AggregateError(
1006
+ errors,
1007
+ `${errors.length} after-transition observer(s) threw`
1008
+ );
1009
+ }
1010
+ }
1011
+ event = null;
1012
+ }
1013
+ }
1014
+ readonlyContext(ctx) {
1015
+ return new Map(ctx);
696
1016
  }
697
1017
  };
698
1018
 
@@ -847,62 +1167,36 @@ var CallbackObserver = class {
847
1167
  };
848
1168
 
849
1169
  // src/observer/StatefulStatusChanger.ts
850
- function isStatemachine(obj) {
851
- return typeof obj === "object" && obj !== null && "getCurrentState" in obj && "getSubject" in obj;
852
- }
853
- function isStateful(obj) {
854
- return typeof obj === "object" && obj !== null && "setCurrentStateName" in obj && typeof obj.setCurrentStateName === "function";
855
- }
856
1170
  var StatefulStatusChanger = class {
857
- update(subject) {
858
- if (isStatemachine(subject)) {
859
- const subjectObj = subject.getSubject();
860
- if (isStateful(subjectObj)) {
861
- const stateName = subject.getCurrentState().getName();
862
- subjectObj.setCurrentStateName(stateName);
863
- }
864
- }
1171
+ subject;
1172
+ constructor(subject) {
1173
+ this.subject = subject;
1174
+ }
1175
+ notify(frame) {
1176
+ this.subject.setCurrentStateName(frame.toState.getName());
865
1177
  }
866
1178
  };
867
1179
 
868
1180
  // src/observer/OnEnterObserver.ts
869
- function isStatemachine2(obj) {
870
- return typeof obj === "object" && obj !== null && "getCurrentState" in obj && "triggerEvent" in obj;
871
- }
872
1181
  var OnEnterObserver = class _OnEnterObserver {
873
1182
  static DEFAULT_EVENT_NAME = "onEnter";
874
1183
  eventName;
875
1184
  constructor(eventName = _OnEnterObserver.DEFAULT_EVENT_NAME) {
876
1185
  this.eventName = eventName;
877
1186
  }
878
- async update(subject) {
879
- if (isStatemachine2(subject) && subject.getCurrentState().hasEvent(this.eventName)) {
880
- const sm = subject;
881
- const autorelease = sm.isAutoreleaseLock();
882
- sm.setAutoreleaseLock(false);
883
- try {
884
- await sm.triggerEvent(
885
- this.eventName,
886
- sm.getCurrentContext() ?? void 0
887
- );
888
- } finally {
889
- sm.setAutoreleaseLock(autorelease);
890
- }
1187
+ notify(frame, ctx) {
1188
+ if (frame.toState.hasEvent(this.eventName)) {
1189
+ ctx.enqueue(this.eventName, new Map(frame.context));
891
1190
  }
892
1191
  }
893
1192
  };
894
1193
 
895
1194
  // src/observer/TransitionLogger.ts
896
- function isStatemachine3(obj) {
897
- return typeof obj === "object" && obj !== null && "getCurrentState" in obj && "getSubject" in obj;
898
- }
899
1195
  function isNamed(obj) {
900
1196
  return typeof obj === "object" && obj !== null && "getName" in obj && typeof obj.getName === "function";
901
1197
  }
902
- function convertToString(obj) {
903
- if (isNamed(obj)) {
904
- return obj.getName();
905
- }
1198
+ function asString(obj) {
1199
+ if (isNamed(obj)) return obj.getName();
906
1200
  return String(obj);
907
1201
  }
908
1202
  var TransitionLogger = class {
@@ -912,40 +1206,23 @@ var TransitionLogger = class {
912
1206
  this.logger = logger;
913
1207
  this.loggerLevel = loggerLevel;
914
1208
  }
915
- update(subject) {
916
- if (!isStatemachine3(subject)) {
917
- return;
918
- }
919
- const context = {};
920
- context["subject"] = subject.getSubject();
921
- context["currentState"] = subject.getCurrentState();
922
- context["lastState"] = subject.getLastState();
923
- context["transition"] = subject.getSelectedTransition();
1209
+ notify(frame) {
924
1210
  let message = "Transition";
925
- if (context["subject"] != null) {
926
- message += ` for "${convertToString(context["subject"])}"`;
927
- }
928
- if (context["lastState"] != null) {
929
- message += ` from "${convertToString(context["lastState"])}"`;
930
- }
931
- if (context["currentState"] != null) {
932
- message += ` to "${convertToString(context["currentState"])}"`;
933
- }
934
- const transition = context["transition"];
935
- if (transition && typeof transition.getEventName === "function") {
936
- const eventName = transition.getEventName();
937
- const condition = transition.getConditionName?.();
938
- if (eventName || condition) {
939
- message += " with";
940
- if (eventName) {
941
- message += ` event "${eventName}"`;
942
- }
943
- if (condition) {
944
- message += ` condition "${condition}"`;
945
- }
946
- }
947
- }
948
- this.logger.log(this.loggerLevel, message, context);
1211
+ message += ` from "${asString(frame.fromState)}" to "${asString(frame.toState)}"`;
1212
+ const eventName = frame.event ? frame.event.getName() : null;
1213
+ const conditionName = frame.condition ? frame.condition.getName() : null;
1214
+ if (eventName || conditionName) {
1215
+ message += " with";
1216
+ if (eventName) message += ` event "${eventName}"`;
1217
+ if (conditionName) message += ` condition "${conditionName}"`;
1218
+ }
1219
+ this.logger.log(this.loggerLevel, message, {
1220
+ fromState: frame.fromState,
1221
+ toState: frame.toState,
1222
+ event: frame.event,
1223
+ transition: frame.transition,
1224
+ machineName: frame.machineName
1225
+ });
949
1226
  }
950
1227
  };
951
1228
 
@@ -1111,7 +1388,8 @@ var MutexFactory = class {
1111
1388
  var Factory = class {
1112
1389
  processDetector;
1113
1390
  stateNameDetector;
1114
- statemachineObservers = /* @__PURE__ */ new Set();
1391
+ beforeObservers = /* @__PURE__ */ new Set();
1392
+ afterObservers = /* @__PURE__ */ new Set();
1115
1393
  transitionSelector = null;
1116
1394
  mutexFactory = null;
1117
1395
  constructor(processDetector, stateNameDetector) {
@@ -1124,30 +1402,30 @@ var Factory = class {
1124
1402
  setTransitionSelector(selector) {
1125
1403
  this.transitionSelector = selector;
1126
1404
  }
1127
- attachStatemachineObserver(observer) {
1128
- this.statemachineObservers.add(observer);
1405
+ attachBeforeObserver(observer) {
1406
+ this.beforeObservers.add(observer);
1129
1407
  }
1130
- detachStatemachineObserver(observer) {
1131
- this.statemachineObservers.delete(observer);
1408
+ detachBeforeObserver(observer) {
1409
+ this.beforeObservers.delete(observer);
1132
1410
  }
1133
- getStatemachineObservers() {
1134
- return this.statemachineObservers;
1411
+ attachAfterObserver(observer) {
1412
+ this.afterObservers.add(observer);
1413
+ }
1414
+ detachAfterObserver(observer) {
1415
+ this.afterObservers.delete(observer);
1135
1416
  }
1136
1417
  async createStatemachine(subject) {
1137
1418
  const process = this.processDetector.detectProcess(subject);
1138
- const stateName = this.stateNameDetector ? this.stateNameDetector.detectCurrentStateName(subject) : null;
1139
- const mutex = this.mutexFactory ? await this.mutexFactory.createMutex(subject) : null;
1140
- const statemachine = new Statemachine(
1141
- subject,
1142
- process,
1143
- stateName,
1144
- this.transitionSelector,
1145
- mutex
1146
- );
1147
- for (const observer of this.statemachineObservers) {
1148
- statemachine.attach(observer);
1149
- }
1150
- return statemachine;
1419
+ const stateName = this.stateNameDetector ? this.stateNameDetector.detectCurrentStateName(subject) : void 0;
1420
+ const mutex = this.mutexFactory ? await this.mutexFactory.createMutex(subject) : void 0;
1421
+ const sm = new Statemachine(subject, process, {
1422
+ initialStateName: stateName ?? void 0,
1423
+ transitionSelector: this.transitionSelector ?? void 0,
1424
+ mutex: mutex ?? void 0
1425
+ });
1426
+ for (const o of this.beforeObservers) sm.attachBefore(o);
1427
+ for (const o of this.afterObservers) sm.attachAfter(o);
1428
+ return sm;
1151
1429
  }
1152
1430
  };
1153
1431
 
@@ -1162,6 +1440,21 @@ var SingleProcessDetector = class {
1162
1440
  }
1163
1441
  };
1164
1442
 
1443
+ // src/error/ProcessNotFoundError.ts
1444
+ var ProcessNotFoundError = class extends FinitaError {
1445
+ code = "processNotFound";
1446
+ processName;
1447
+ availableProcesses;
1448
+ constructor(processName, availableProcesses) {
1449
+ const list = Array.from(availableProcesses);
1450
+ const display = list.length > 0 ? list.map((n) => `"${n}"`).join(", ") : "(none)";
1451
+ super(`Process "${processName}" not found. Available: ${display}`);
1452
+ this.name = "ProcessNotFoundError";
1453
+ this.processName = processName;
1454
+ this.availableProcesses = Object.freeze([...list]);
1455
+ }
1456
+ };
1457
+
1165
1458
  // src/factory/AbstractNamedProcessDetector.ts
1166
1459
  var AbstractNamedProcessDetector = class {
1167
1460
  processes = /* @__PURE__ */ new Map();
@@ -1175,74 +1468,39 @@ var AbstractNamedProcessDetector = class {
1175
1468
  const name = this.detectProcessName(subject);
1176
1469
  const process = this.processes.get(name);
1177
1470
  if (!process) {
1178
- throw new Error(`Process "${name}" not found`);
1471
+ throw new ProcessNotFoundError(name, this.processes.keys());
1179
1472
  }
1180
1473
  return process;
1181
1474
  }
1182
1475
  };
1183
1476
 
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
+
1184
1494
  // src/factory/StatefulStateNameDetector.ts
1185
- function isStateful2(obj) {
1495
+ function isStateful(obj) {
1186
1496
  return typeof obj === "object" && obj !== null && "getCurrentStateName" in obj && typeof obj.getCurrentStateName === "function";
1187
1497
  }
1188
1498
  var StatefulStateNameDetector = class {
1189
1499
  detectCurrentStateName(subject) {
1190
- if (isStateful2(subject)) {
1500
+ if (isStateful(subject)) {
1191
1501
  return subject.getCurrentStateName();
1192
1502
  }
1193
- throw new Error("Subject has to implement the StatefulInterface!");
1194
- }
1195
- };
1196
-
1197
- // src/util/SetupHelper.ts
1198
- var SetupHelper = class {
1199
- stateCollection;
1200
- constructor(stateCollection) {
1201
- this.stateCollection = stateCollection;
1202
- }
1203
- findOrCreateState(name) {
1204
- if (!this.stateCollection.hasState(name)) {
1205
- this.stateCollection.addState(new State(name));
1206
- }
1207
- return this.stateCollection.getState(name);
1208
- }
1209
- findTransition(sourceState, targetState, eventName = null, condition = null) {
1210
- const conditionName = condition ? condition.getName() : null;
1211
- for (const transition of sourceState.getTransitions()) {
1212
- const hasSameTargetState = transition.getTargetState() === targetState;
1213
- const hasSameCondition = transition.getConditionName() === conditionName;
1214
- const hasSameEvent = transition.getEventName() === eventName;
1215
- if (hasSameTargetState && hasSameCondition && hasSameEvent) {
1216
- return transition;
1217
- }
1218
- }
1219
- return null;
1220
- }
1221
- findOrCreateTransition(sourceStateName, targetStateName, eventName = null, condition = null) {
1222
- const sourceState = this.findOrCreateState(sourceStateName);
1223
- const targetState = this.findOrCreateState(targetStateName);
1224
- let transition = this.findTransition(
1225
- sourceState,
1226
- targetState,
1227
- eventName,
1228
- condition
1229
- );
1230
- if (!transition) {
1231
- transition = new Transition(targetState, eventName, condition);
1232
- sourceState.addTransition(transition);
1233
- }
1234
- return transition;
1235
- }
1236
- findOrCreateEvent(sourceStateName, eventName) {
1237
- const sourceState = this.findOrCreateState(sourceStateName);
1238
- return sourceState.getEvent(eventName);
1239
- }
1240
- addCommand(sourceStateName, eventName, command) {
1241
- this.findOrCreateEvent(sourceStateName, eventName).attach(command);
1242
- }
1243
- addCommandAndSelfTransition(sourceStateName, eventName, command) {
1244
- this.addCommand(sourceStateName, eventName, command);
1245
- this.findOrCreateTransition(sourceStateName, sourceStateName, eventName);
1503
+ throw new InvalidSubjectError("StatefulInterface", ["getCurrentStateName"]);
1246
1504
  }
1247
1505
  };
1248
1506
 
@@ -1262,7 +1520,7 @@ function toMermaidId(name) {
1262
1520
  function escapeMermaidLabel(str) {
1263
1521
  return str.replace(/"/g, "#quot;");
1264
1522
  }
1265
- function convertToString2(obj) {
1523
+ function convertToString(obj) {
1266
1524
  if (isNamed2(obj)) {
1267
1525
  return obj.getName();
1268
1526
  }
@@ -1271,6 +1529,7 @@ function convertToString2(obj) {
1271
1529
  var GraphBuilder = class {
1272
1530
  nodes = /* @__PURE__ */ new Map();
1273
1531
  edges = [];
1532
+ statesWithEdges = /* @__PURE__ */ new Set();
1274
1533
  getOrCreateNode(state) {
1275
1534
  const name = state.getName();
1276
1535
  let node = this.nodes.get(name);
@@ -1288,7 +1547,7 @@ var GraphBuilder = class {
1288
1547
  const event = state.getEvent(eventName);
1289
1548
  const observerNames = [];
1290
1549
  for (const observer of event.getObservers()) {
1291
- observerNames.push(convertToString2(observer));
1550
+ observerNames.push(convertToString(observer));
1292
1551
  }
1293
1552
  if (observerNames.length > 0) {
1294
1553
  parts.push(`C: ${observerNames.join(", ")}`);
@@ -1303,6 +1562,9 @@ var GraphBuilder = class {
1303
1562
  }
1304
1563
  addState(state) {
1305
1564
  this.getOrCreateNode(state);
1565
+ const name = state.getName();
1566
+ if (this.statesWithEdges.has(name)) return;
1567
+ this.statesWithEdges.add(name);
1306
1568
  for (const transition of state.getTransitions()) {
1307
1569
  const sourceNode = this.getOrCreateNode(state);
1308
1570
  const targetNode = this.getOrCreateNode(transition.getTargetState());
@@ -1381,19 +1643,24 @@ var GraphBuilder = class {
1381
1643
  0 && (module.exports = {
1382
1644
  AbstractNamedProcessDetector,
1383
1645
  ActiveTransitionFilter,
1646
+ AmbiguousTransitionError,
1384
1647
  AndComposite,
1648
+ AutomaticTransitionCycleError,
1385
1649
  CallbackCondition,
1386
1650
  CallbackObserver,
1387
1651
  Contradiction,
1388
- Dispatcher,
1389
1652
  DuplicateStateError,
1653
+ DuplicateTransitionError,
1390
1654
  Event,
1391
1655
  Factory,
1392
1656
  FilterStateByEvent,
1393
1657
  FilterStateByFinalState,
1394
1658
  FilterStateByTransition,
1395
1659
  FilterTransitionByEvent,
1660
+ FinitaError,
1396
1661
  GraphBuilder,
1662
+ GraphValidationError,
1663
+ InvalidSubjectError,
1397
1664
  LockAdapterMutex,
1398
1665
  LockCanNotBeAcquiredError,
1399
1666
  MutexFactory,
@@ -1403,12 +1670,14 @@ var GraphBuilder = class {
1403
1670
  OneOrNoneActiveTransition,
1404
1671
  OrComposite,
1405
1672
  Process,
1673
+ ProcessBuilder,
1674
+ ProcessFinalizedError,
1675
+ ProcessNotFoundError,
1406
1676
  ScoreTransition,
1407
- SetupHelper,
1408
1677
  SingleProcessDetector,
1409
1678
  State,
1410
- StateCollection,
1411
- StateCollectionMerger,
1679
+ StateEventNotFoundError,
1680
+ StateNotFoundError,
1412
1681
  StatefulStateNameDetector,
1413
1682
  StatefulStatusChanger,
1414
1683
  Statemachine,