machina 4.0.2 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,661 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/emitter.ts
4
+ /**
5
+ * Minimal typed event emitter used internally by Fsm and BehavioralFsm.
6
+ *
7
+ * Two listener categories exist at runtime: named listeners (keyed by event
8
+ * name) and wildcard listeners (keyed as `"*"`). The wildcard fires on EVERY
9
+ * emit, regardless of event name, and its callback receives both the event
10
+ * name and the data. Named listeners receive only the data.
11
+ *
12
+ * @typeParam TEventMap - Record mapping event names to their payload types.
13
+ * Constrains `on()` and `emit()` to matching event/payload pairs.
14
+ */
15
+ var Emitter = class {
16
+ listeners = /* @__PURE__ */ new Map();
17
+ on(event, cb) {
18
+ let set = this.listeners.get(event);
19
+ if (!set) {
20
+ set = /* @__PURE__ */ new Set();
21
+ this.listeners.set(event, set);
22
+ }
23
+ set.add(cb);
24
+ return { off: () => {
25
+ set.delete(cb);
26
+ } };
27
+ }
28
+ /**
29
+ * Emit a named event, notifying wildcard listeners first, then named listeners.
30
+ *
31
+ * Wildcard listeners receive `(eventName, data)`; named listeners receive
32
+ * only `data`. The firing order (wildcards before named) is intentional —
33
+ * it lets relay listeners (like Fsm's bfsm proxy) observe all events
34
+ * before specific subscribers react.
35
+ */
36
+ emit(event, data) {
37
+ const wildcards = this.listeners.get("*");
38
+ if (wildcards) for (const cb of wildcards) cb(event, data);
39
+ const named = this.listeners.get(event);
40
+ if (named) for (const cb of named) cb(data);
41
+ }
42
+ /**
43
+ * Remove all listeners (named and wildcard).
44
+ *
45
+ * Existing `Subscription` objects remain valid — their `off()` closures
46
+ * hold a reference to the now-empty Set, so calling them is harmless.
47
+ * Called by `dispose()` to prevent memory retention after an FSM shuts down.
48
+ */
49
+ clear() {
50
+ this.listeners.clear();
51
+ }
52
+ };
53
+
54
+ //#endregion
55
+ //#region src/types.ts
56
+ /**
57
+ * Symbol used as a property key to identify machina FSM instances at runtime.
58
+ * Each class stamps itself with a MachinaType value so the ChildLink adapter
59
+ * can dispatch handle()/canHandle()/reset() correctly without circular imports.
60
+ */
61
+ const MACHINA_TYPE = Symbol("machina.type");
62
+
63
+ //#endregion
64
+ //#region src/behavioral-fsm.ts
65
+ const MAX_TRANSITION_DEPTH = 20;
66
+ /**
67
+ * Defines FSM behavior (states + transitions) while tracking per-client state
68
+ * in a `WeakMap`. A single `BehavioralFsm` instance can drive any number of
69
+ * independent client objects simultaneously — each gets its own state,
70
+ * deferred queue, and lifecycle.
71
+ *
72
+ * Prefer `createBehavioralFsm()` over constructing this directly — the factory
73
+ * infers all generic parameters from the config object.
74
+ *
75
+ * All public methods silently no-op after `dispose()` is called.
76
+ *
77
+ * @typeParam TClient - The client object type. Must be an object (non-primitive)
78
+ * so it can serve as a WeakMap key.
79
+ * @typeParam TStateNames - String literal union of valid state names.
80
+ * @typeParam TInputNames - String literal union of valid input names.
81
+ */
82
+ var BehavioralFsm = class {
83
+ id;
84
+ initialState;
85
+ [MACHINA_TYPE] = "BehavioralFsm";
86
+ states;
87
+ emitter = new Emitter();
88
+ clients = /* @__PURE__ */ new WeakMap();
89
+ knownClients = /* @__PURE__ */ new Set();
90
+ childSubscriptions = [];
91
+ disposed = false;
92
+ transitionDepth = 0;
93
+ constructor(config) {
94
+ this.id = config.id;
95
+ this.initialState = config.initialState;
96
+ this.states = config.states;
97
+ this.wrapChildLinks();
98
+ this.setupChildSubscriptions();
99
+ }
100
+ /**
101
+ * Dispatch an input to the given client's current state handler.
102
+ *
103
+ * Delegation order: if the current state has a `_child` FSM that can
104
+ * handle the input, it is dispatched there. If the child emits `nohandler`,
105
+ * the input bubbles up to this FSM's local handler. If no handler exists
106
+ * here either, `nohandler` is emitted on this FSM's emitter.
107
+ *
108
+ * No-ops silently when disposed.
109
+ */
110
+ handle(client, inputName, ...args) {
111
+ if (this.disposed) return;
112
+ const meta = this.getOrCreateClientMeta(client);
113
+ meta.currentActionArgs = args;
114
+ const childLink = this.states[meta.state]?._child;
115
+ if (childLink) {
116
+ if (childLink.canHandle(client, inputName)) {
117
+ try {
118
+ childLink.handle(client, inputName, ...args);
119
+ } finally {
120
+ meta.currentActionArgs = void 0;
121
+ }
122
+ return;
123
+ }
124
+ }
125
+ this.handleLocally(client, inputName, args, meta);
126
+ }
127
+ /**
128
+ * Returns true if the client's current state has a handler for `inputName`
129
+ * (or a catch-all `"*"` handler). Does NOT initialize the client — no
130
+ * `_onEnter`, no events, no side effects. Unseen clients are treated as
131
+ * if they were already in `initialState`. Returns false when disposed.
132
+ */
133
+ canHandle(client, inputName) {
134
+ if (this.disposed) return false;
135
+ const state = this.clients.get(client)?.state ?? this.initialState;
136
+ const stateObj = this.states[state];
137
+ return !!(stateObj?.[inputName] ?? stateObj?.["*"]);
138
+ }
139
+ /**
140
+ * Transition the client back to `initialState`, firing `_onEnter` and
141
+ * lifecycle events as if entering it fresh. No-ops when disposed.
142
+ */
143
+ reset(client) {
144
+ if (this.disposed) return;
145
+ this.transition(client, this.initialState);
146
+ }
147
+ /**
148
+ * Returns the client's current state, or `undefined` if the client has
149
+ * never been initialized (i.e. `handle()`, `transition()`, or `reset()`
150
+ * have never been called for it). Does NOT trigger initialization.
151
+ */
152
+ currentState(client) {
153
+ return this.clients.get(client)?.state;
154
+ }
155
+ /**
156
+ * Directly transition `client` to `toState`, running the full lifecycle:
157
+ * `_onExit` for the current state → `transitioning` event → update state →
158
+ * `_onEnter` for new state → `transitioned` event → child reset → deferred
159
+ * queue replay → bounce (if `_onEnter` returned a state name).
160
+ *
161
+ * Same-state transitions are silently ignored. Transitions to unknown state
162
+ * names emit `invalidstate` instead of throwing. Throws if the transition
163
+ * depth exceeds `MAX_TRANSITION_DEPTH` (likely an `_onEnter` → transition loop).
164
+ *
165
+ * No-ops when disposed.
166
+ */
167
+ transition(client, toState) {
168
+ if (this.disposed) return;
169
+ const meta = this.getOrCreateClientMeta(client);
170
+ const fromState = meta.state;
171
+ if (toState === fromState) return;
172
+ if (!(toState in this.states)) {
173
+ this.emitter.emit("invalidstate", {
174
+ stateName: toState,
175
+ client
176
+ });
177
+ return;
178
+ }
179
+ this.transitionDepth++;
180
+ if (this.transitionDepth > MAX_TRANSITION_DEPTH) {
181
+ this.transitionDepth = 0;
182
+ throw new Error(`Max transition depth (${MAX_TRANSITION_DEPTH}) exceeded in FSM "${this.id}". Likely an infinite _onEnter → transition loop.`);
183
+ }
184
+ try {
185
+ const curStateObj = this.states[fromState];
186
+ const newStateObj = this.states[toState];
187
+ if (curStateObj?._onExit && typeof curStateObj._onExit === "function") {
188
+ const exitArgs = this.buildHandlerArgs(client, "", meta);
189
+ curStateObj._onExit(exitArgs);
190
+ }
191
+ meta.state = toState;
192
+ const payload = {
193
+ fromState,
194
+ toState,
195
+ client
196
+ };
197
+ this.emitter.emit("transitioning", payload);
198
+ let bounceTarget = void 0;
199
+ if (newStateObj?._onEnter && typeof newStateObj._onEnter === "function") {
200
+ const enterArgs = this.buildHandlerArgs(client, "", meta);
201
+ bounceTarget = newStateObj._onEnter(enterArgs);
202
+ }
203
+ this.emitter.emit("transitioned", payload);
204
+ const childLink = newStateObj?._child;
205
+ if (childLink) childLink.reset(client);
206
+ this.processQueue(client, meta);
207
+ if (typeof bounceTarget === "string" && meta.state === toState) this.transition(client, bounceTarget);
208
+ } finally {
209
+ this.transitionDepth--;
210
+ }
211
+ }
212
+ /**
213
+ * Returns the client's state as a dot-delimited path including any active
214
+ * child FSM states (e.g. `"active.connecting.retrying"`). Returns just the
215
+ * current state name when no child is active. Returns `""` for clients that
216
+ * have never been initialized (unlike `currentState()` which returns `undefined`).
217
+ */
218
+ compositeState(client) {
219
+ const meta = this.clients.get(client);
220
+ if (!meta) return "";
221
+ const childLink = this.states[meta.state]?._child;
222
+ if (childLink) {
223
+ const childComposite = childLink.compositeState(client);
224
+ if (childComposite) return `${meta.state}.${childComposite}`;
225
+ }
226
+ return meta.state;
227
+ }
228
+ on(eventName, callback) {
229
+ if (this.disposed) return { off() {} };
230
+ return this.emitter.on(eventName, callback);
231
+ }
232
+ /**
233
+ * Emit a custom event through the FSM. Built-in lifecycle events are
234
+ * emitted automatically — this is for user-defined events from handlers.
235
+ * No-ops when disposed.
236
+ */
237
+ emit(eventName, data) {
238
+ if (this.disposed) return;
239
+ this.emitter.emit(eventName, data);
240
+ }
241
+ /**
242
+ * Permanently shut down this FSM. Irreversible — all subsequent method
243
+ * calls become silent no-ops. Tears down child subscriptions, clears all
244
+ * listeners, and cascades disposal to child FSMs (unless `preserveChildren`
245
+ * is set). The same child appearing in multiple states is disposed once.
246
+ */
247
+ dispose(options) {
248
+ this.disposed = true;
249
+ for (const sub of this.childSubscriptions) sub.off();
250
+ if (!options?.preserveChildren) {
251
+ const seen = /* @__PURE__ */ new Set();
252
+ for (const stateName of Object.keys(this.states)) {
253
+ const childLink = this.states[stateName]?._child;
254
+ if (childLink && !seen.has(childLink)) {
255
+ seen.add(childLink);
256
+ childLink.dispose();
257
+ }
258
+ }
259
+ }
260
+ this.emitter.clear();
261
+ }
262
+ /**
263
+ * Walks all states at construction time, detects raw FSM instances assigned
264
+ * to _child, and wraps them into ChildLink adapters via createChildLink().
265
+ * Must run BEFORE setupChildSubscriptions() so the subscriptions see
266
+ * ChildLink objects, not raw FSM instances.
267
+ */
268
+ wrapChildLinks() {
269
+ for (const stateName of Object.keys(this.states)) {
270
+ const stateObj = this.states[stateName];
271
+ const rawChild = stateObj?._child;
272
+ if (!rawChild) continue;
273
+ if (typeof rawChild !== "object") throw new Error(`State "${stateName}"._child: expected an Fsm or BehavioralFsm instance, got ${String(rawChild)}`);
274
+ if (!(MACHINA_TYPE in rawChild)) throw new Error(`State "${stateName}"._child: expected an Fsm or BehavioralFsm instance, got a plain object`);
275
+ stateObj._child = createChildLink(rawChild);
276
+ }
277
+ }
278
+ /**
279
+ * Walks all states at construction time, finds states with _child, and
280
+ * subscribes once to each unique child's wildcard events. Subscriptions are
281
+ * stored for cleanup in dispose(). We deduplicate by child reference to
282
+ * avoid double-subscribing when the same child appears in multiple states.
283
+ */
284
+ setupChildSubscriptions() {
285
+ const seenChildren = /* @__PURE__ */ new Set();
286
+ for (const stateName of Object.keys(this.states)) {
287
+ const childLink = this.states[stateName]?._child;
288
+ if (!childLink || seenChildren.has(childLink)) continue;
289
+ seenChildren.add(childLink);
290
+ const sub = childLink.onAny((eventName, data) => {
291
+ if (eventName === "nohandler") {
292
+ const payload = data;
293
+ if (payload.client !== void 0) this.bubbleNohandler(payload.client, childLink, payload.inputName, payload.args ?? []);
294
+ else for (const ref of this.knownClients) {
295
+ const client = ref.deref();
296
+ if (client === void 0) {
297
+ this.knownClients.delete(ref);
298
+ continue;
299
+ }
300
+ this.bubbleNohandler(client, childLink, payload.inputName, payload.args ?? []);
301
+ }
302
+ return;
303
+ }
304
+ const payload = data;
305
+ if (payload && typeof payload === "object" && "client" in payload) {
306
+ if (this.isChildActiveForClient(payload.client, childLink)) this.emitter.emit(eventName, data);
307
+ } else for (const ref of this.knownClients) {
308
+ const client = ref.deref();
309
+ if (!client) {
310
+ this.knownClients.delete(ref);
311
+ continue;
312
+ }
313
+ if (this.isChildActiveForClient(client, childLink)) {
314
+ this.emitter.emit(eventName, data);
315
+ break;
316
+ }
317
+ }
318
+ });
319
+ this.childSubscriptions.push(sub);
320
+ }
321
+ }
322
+ /**
323
+ * Bubbles a child nohandler to the parent for the given client.
324
+ * Only fires if the client is currently in a state that has this childLink.
325
+ * Extracted from the lambda in setupChildSubscriptions to keep it readable.
326
+ */
327
+ bubbleNohandler(client, childLink, inputName, args) {
328
+ if (!this.isChildActiveForClient(client, childLink)) return;
329
+ const meta = this.clients.get(client);
330
+ meta.currentActionArgs = args;
331
+ this.handleLocally(client, inputName, args, meta);
332
+ }
333
+ /**
334
+ * Returns true if the given client is currently in a parent state whose
335
+ * _child is the specified childLink. Returns false if the client has no
336
+ * meta (never initialized) or is in a state with a different (or no) child.
337
+ */
338
+ isChildActiveForClient(client, childLink) {
339
+ const meta = this.clients.get(client);
340
+ if (!meta) return false;
341
+ return this.states[meta.state]?._child === childLink;
342
+ }
343
+ /**
344
+ * The inner handler dispatch — no delegation, no initialization side effects
345
+ * beyond what getOrCreateClientMeta already did. Called by handle() after
346
+ * the delegation check, and by the nohandler child listener for bubbling.
347
+ */
348
+ handleLocally(client, inputName, args, meta) {
349
+ const stateObj = this.states[meta.state];
350
+ const handler = stateObj?.[inputName] ?? stateObj?.["*"];
351
+ if (!handler) {
352
+ this.emitter.emit("nohandler", {
353
+ inputName,
354
+ args,
355
+ client
356
+ });
357
+ meta.currentActionArgs = void 0;
358
+ return;
359
+ }
360
+ try {
361
+ this.emitter.emit("handling", {
362
+ inputName,
363
+ client
364
+ });
365
+ const handlerArgs = this.buildHandlerArgs(client, inputName, meta);
366
+ let targetState = void 0;
367
+ if (typeof handler === "string") targetState = handler;
368
+ else if (typeof handler === "function") targetState = handler(handlerArgs, ...args);
369
+ this.emitter.emit("handled", {
370
+ inputName,
371
+ client
372
+ });
373
+ if (typeof targetState === "string") this.transition(client, targetState);
374
+ } finally {
375
+ meta.currentActionArgs = void 0;
376
+ }
377
+ }
378
+ getOrCreateClientMeta(client) {
379
+ let meta = this.clients.get(client);
380
+ if (meta) return meta;
381
+ meta = {
382
+ state: void 0,
383
+ deferredQueue: []
384
+ };
385
+ this.clients.set(client, meta);
386
+ this.knownClients.add(new WeakRef(client));
387
+ this.transition(client, this.initialState);
388
+ return meta;
389
+ }
390
+ buildHandlerArgs(client, inputName, meta) {
391
+ return {
392
+ ctx: client,
393
+ inputName,
394
+ defer: (opts) => {
395
+ if (!meta.currentActionArgs) return;
396
+ const deferred = {
397
+ inputName,
398
+ args: [...meta.currentActionArgs],
399
+ untilState: opts?.until
400
+ };
401
+ meta.deferredQueue.push(deferred);
402
+ this.emitter.emit("deferred", {
403
+ inputName,
404
+ client
405
+ });
406
+ },
407
+ emit: (evtName, evtData) => {
408
+ this.emitter.emit(evtName, evtData);
409
+ }
410
+ };
411
+ }
412
+ processQueue(client, meta) {
413
+ const toReplay = [];
414
+ const remaining = [];
415
+ for (const item of meta.deferredQueue) if (item.untilState === void 0 || item.untilState === meta.state) toReplay.push(item);
416
+ else remaining.push(item);
417
+ meta.deferredQueue = remaining;
418
+ for (const item of toReplay) this.handle(client, item.inputName, ...item.args);
419
+ }
420
+ };
421
+ /**
422
+ * Internal factory called by `wrapChildLinks()` during construction.
423
+ * Wraps a raw Fsm or BehavioralFsm instance in a uniform `ChildLink` adapter
424
+ * so the parent engine doesn't need to know which type it's talking to.
425
+ *
426
+ * Users never call this directly — they assign an FSM instance to `_child`
427
+ * in their state config and `wrapChildLinks()` handles the wrapping.
428
+ */
429
+ function createChildLink(child) {
430
+ if (!child || typeof child !== "object") throw new Error(`createChildLink: expected an Fsm or BehavioralFsm instance, got ${String(child)}`);
431
+ const childType = child[MACHINA_TYPE];
432
+ if (childType === "BehavioralFsm") return {
433
+ instance: child,
434
+ canHandle(client, inputName) {
435
+ return child.canHandle(client, inputName);
436
+ },
437
+ handle(client, inputName, ...args) {
438
+ child.handle(client, inputName, ...args);
439
+ },
440
+ reset(client) {
441
+ child.transition(client, child.initialState);
442
+ },
443
+ onAny(callback) {
444
+ return child.on("*", callback);
445
+ },
446
+ compositeState(client) {
447
+ return child.compositeState(client);
448
+ },
449
+ dispose() {
450
+ child.dispose();
451
+ }
452
+ };
453
+ if (childType === "Fsm") return {
454
+ instance: child,
455
+ canHandle(_client, inputName) {
456
+ return child.canHandle(inputName);
457
+ },
458
+ handle(_client, inputName, ...args) {
459
+ child.handle(inputName, ...args);
460
+ },
461
+ reset(_client) {
462
+ child.reset();
463
+ },
464
+ onAny(callback) {
465
+ return child.on("*", callback);
466
+ },
467
+ compositeState(_client) {
468
+ return child.compositeState();
469
+ },
470
+ dispose() {
471
+ child.dispose();
472
+ }
473
+ };
474
+ throw new Error(`createChildLink: expected an Fsm or BehavioralFsm instance, got [MACHINA_TYPE] = ${String(childType ?? "undefined")}`);
475
+ }
476
+ /**
477
+ * Create a behavioral FSM (one definition, many clients) from a config object.
478
+ *
479
+ * Generic parameters are inferred automatically:
480
+ * - `TClient` must be provided explicitly as a type parameter (it can't be
481
+ * inferred from the config since no `context` property exists at the FSM level).
482
+ * - `TStates` is captured with `const` inference to preserve string literal
483
+ * types, enabling compile-time validation of transition targets and `handle()`
484
+ * input names.
485
+ *
486
+ * State names, input names, and all handler signatures derive from `TStates`.
487
+ *
488
+ * @example
489
+ * ```ts
490
+ * interface Connection { url: string; retries: number; }
491
+ *
492
+ * const connFsm = createBehavioralFsm<Connection>({
493
+ * id: "connectivity",
494
+ * initialState: "disconnected",
495
+ * states: {
496
+ * disconnected: { connect: "connecting" },
497
+ * connecting: { connected: "online", failed: "disconnected" },
498
+ * online: { disconnect: "disconnected" },
499
+ * },
500
+ * });
501
+ *
502
+ * const conn = { url: "wss://example.com", retries: 0 };
503
+ * connFsm.handle(conn, "connect");
504
+ * ```
505
+ */
506
+ function createBehavioralFsm(config) {
507
+ return new BehavioralFsm(config);
508
+ }
509
+
510
+ //#endregion
511
+ //#region src/fsm.ts
512
+ /**
513
+ * Single-client FSM. Wraps a BehavioralFsm and uses the config's `context`
514
+ * object as the implicit client, so callers never pass a client argument.
515
+ *
516
+ * Prefer `createFsm()` over constructing this directly — the factory infers
517
+ * all generic parameters from the config object.
518
+ *
519
+ * All public methods silently no-op after `dispose()` is called.
520
+ *
521
+ * @typeParam TCtx - The context type, inferred from `config.context`.
522
+ * @typeParam TStateNames - String literal union of valid state names.
523
+ * @typeParam TInputNames - String literal union of valid input names.
524
+ */
525
+ var Fsm = class {
526
+ id;
527
+ initialState;
528
+ [MACHINA_TYPE] = "Fsm";
529
+ states;
530
+ bfsm;
531
+ context;
532
+ emitter = new Emitter();
533
+ disposed = false;
534
+ constructor(config) {
535
+ this.id = config.id;
536
+ this.initialState = config.initialState;
537
+ this.context = config.context ?? {};
538
+ this.bfsm = new BehavioralFsm(config);
539
+ this.states = config.states;
540
+ this.bfsm.on("*", (eventName, data) => {
541
+ if (data && typeof data === "object" && "client" in data) {
542
+ const { client: _, ...payload } = data;
543
+ this.emitter.emit(eventName, payload);
544
+ } else this.emitter.emit(eventName, data);
545
+ });
546
+ this.bfsm.transition(this.context, config.initialState);
547
+ }
548
+ /**
549
+ * Dispatch an input to the current state's handler.
550
+ * If a `_child` FSM in the current state can handle it, delegation occurs
551
+ * there first; unhandled inputs bubble up to the parent.
552
+ * No-ops silently when disposed.
553
+ */
554
+ handle(inputName, ...args) {
555
+ if (this.disposed) return;
556
+ this.bfsm.handle(this.context, inputName, ...args);
557
+ }
558
+ /**
559
+ * Returns true if the current state has a handler for `inputName`
560
+ * (or a catch-all `"*"` handler). Does not trigger initialization
561
+ * or any side effects. Returns false when disposed.
562
+ */
563
+ canHandle(inputName) {
564
+ if (this.disposed) return false;
565
+ return this.bfsm.canHandle(this.context, inputName);
566
+ }
567
+ /**
568
+ * Transition back to `initialState`, firing `_onEnter` and lifecycle
569
+ * events as if entering it fresh. No-ops silently when disposed.
570
+ */
571
+ reset() {
572
+ if (this.disposed) return;
573
+ this.bfsm.reset(this.context);
574
+ }
575
+ /**
576
+ * Returns the current state name. Always defined — Fsm eagerly
577
+ * initializes into `initialState` during construction.
578
+ */
579
+ currentState() {
580
+ return this.bfsm.currentState(this.context);
581
+ }
582
+ /**
583
+ * Directly transition to `toState`, firing `_onExit`, `_onEnter`, and
584
+ * lifecycle events. Same-state transitions are silently ignored.
585
+ * No-ops when disposed.
586
+ */
587
+ transition(toState) {
588
+ if (this.disposed) return;
589
+ this.bfsm.transition(this.context, toState);
590
+ }
591
+ /**
592
+ * Returns the current state as a dot-delimited path that includes
593
+ * any active child FSM states (e.g. `"active.connecting.retrying"`).
594
+ * Returns just the current state name when no child is active.
595
+ */
596
+ compositeState() {
597
+ return this.bfsm.compositeState(this.context);
598
+ }
599
+ on(eventName, callback) {
600
+ if (this.disposed) return { off() {} };
601
+ return this.emitter.on(eventName, callback);
602
+ }
603
+ /**
604
+ * Emit a custom event through the FSM. Built-in lifecycle events are
605
+ * emitted automatically — this is for user-defined events from handlers.
606
+ * Routes through the BehavioralFsm so all relay paths are consistent.
607
+ * No-ops when disposed.
608
+ */
609
+ emit(eventName, data) {
610
+ if (this.disposed) return;
611
+ this.bfsm.emit(eventName, data);
612
+ }
613
+ /**
614
+ * Permanently shut down this FSM. Irreversible — all subsequent method
615
+ * calls become silent no-ops. Clears all listeners and cascades disposal
616
+ * to child FSMs (unless `preserveChildren` is set).
617
+ */
618
+ dispose(options) {
619
+ this.disposed = true;
620
+ this.bfsm.dispose(options);
621
+ this.emitter.clear();
622
+ }
623
+ };
624
+ /**
625
+ * Create a single-client FSM from a config object.
626
+ *
627
+ * Generic parameters are inferred automatically:
628
+ * - `TCtx` comes from `config.context` (defaults to `{}` if omitted).
629
+ * - `TStates` is captured with `const` inference to preserve string literal
630
+ * types, enabling compile-time validation of transition targets and `handle()`
631
+ * input names.
632
+ *
633
+ * State names, input names, and all handler signatures derive from `TStates`.
634
+ *
635
+ * @example
636
+ * ```ts
637
+ * const light = createFsm({
638
+ * id: "traffic-light",
639
+ * initialState: "green",
640
+ * context: { tickCount: 0 },
641
+ * states: {
642
+ * green: { timeout: "yellow" },
643
+ * yellow: { timeout: "red" },
644
+ * red: { timeout: "green" },
645
+ * },
646
+ * });
647
+ *
648
+ * light.handle("timeout"); // transitions green → yellow
649
+ * ```
650
+ */
651
+ function createFsm(config) {
652
+ return new Fsm(config);
653
+ }
654
+
655
+ //#endregion
656
+ exports.BehavioralFsm = BehavioralFsm;
657
+ exports.Fsm = Fsm;
658
+ exports.MACHINA_TYPE = MACHINA_TYPE;
659
+ exports.createBehavioralFsm = createBehavioralFsm;
660
+ exports.createFsm = createFsm;
661
+ //# sourceMappingURL=index.cjs.map