@alwatr/fsm 9.33.0 → 9.33.1
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/dev/main.js +5 -0
- package/dist/dev/main.js.map +11 -0
- package/dist/fsm-service.d.ts.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +3 -3
- package/package.json +13 -10
- package/src/fsm-service.ts +20 -19
package/dist/dev/main.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* 📦 @alwatr/fsm v9.33.1 */
|
|
2
|
+
import{createLogger as j}from"@alwatr/logger";import{queueMicrotask as q}from"@alwatr/delay";import{createPersistentStateSignal as P,createStateSignal as U}from"@alwatr/signal";class Z{config_;logger_;stateSignal;mailbox__=[];processing__=!0;destroyed__=!1;activeActorCleanups__=[];stateSignal__;constructor(z,J){this.config_=z;this.logger_=j(`fsm:${this.config_.name}`),this.logger_.logMethodArgs?.("constructor",z);let Q={name:z.initial,context:z.context};this.stateSignal__=J??(z.persistent?P({name:`fsm-state-${z.name}`,storageKey:z.persistent.storageKey??z.name,initialValue:Q,schemaVersion:z.persistent.schemaVersion}):U({name:`fsm-state-${z.name}`,initialValue:Q})),this.stateSignal=this.stateSignal__.asReadonly(),q(()=>this.start_())}get state(){return this.stateSignal__.get()}matches(...z){return z.includes(this.stateSignal__.get().name)}dispatch=(z)=>{this.logger_.logMethodArgs?.("dispatch",{event:z}),this.mailbox__.push(z),this.processMailbox__()};processMailbox__(){if(this.processing__)return;if(this.logger_.logMethod?.("processMailbox__"),this.destroyed__){this.logger_.incident?.("dispatch","dispatch_after_destroy");return}this.processing__=!0;try{for(let z=0;z<this.mailbox__.length;z++)if(this.processTransition__(this.mailbox__[z]),this.destroyed__)break}finally{this.processing__=!1,this.mailbox__.length=0}}processTransition__(z){let J=this.stateSignal__.get();this.logger_.logMethodArgs?.("processTransition__",{state:J.name,event:z});let Q=this.findTransition__(z,J);if(!Q){this.logger_.incident?.("processTransition__","ignored_event","No valid transition found for event",{state:J.name,event:z});return}let K=Q.target??J.name,W=Q.target!==void 0;if(W)this.executeEffects__(z,J.context,this.config_.states[J.name]?.exit),this.cleanupActors__();let X=this.applyAssigners__(z,J.context,Q.assigner),Y={name:K,context:X};if(this.stateSignal__.set(Y),W)this.executeEffects__(z,Y.context,this.config_.states[Y.name]?.entry),this.spawnActors__(z,Y.context,this.config_.states[Y.name]?.actor)}findTransition__(z,J){this.logger_.logMethod?.("findTransition__");let K=this.config_.states[J.name]?.on?.[z.type];if(!K)return;if(!Array.isArray(K)){if(!K.guard)return K;try{if(K.guard(z,J.context))return K}catch(W){this.logger_.error("findTransition__","guard_failed",W,{state:J.name,eventType:z.type})}return}for(let W=0;W<K.length;W++){let X=K[W];if(!X.guard)return X;try{if(X.guard(z,J.context))return X}catch(Y){this.logger_.error("findTransition__","guard_failed",Y,{state:J.name,eventType:z.type,index:W})}}return}executeEffects__(z,J,Q){if(!Q){this.logger_.logMethod?.("executeEffects__.skipped");return}if(this.logger_.logMethod?.("executeEffects__"),!Array.isArray(Q)){try{Q(z,J)}catch(K){this.logger_.error("executeEffects__","effect_failed",K,{event:z,context:J})}return}for(let K=0;K<Q.length;K++){let W=Q[K];try{W(z,J)}catch(X){this.logger_.error("executeEffects__","effect_failed",X,{event:z,context:J,index:K})}}}applyAssigners__(z,J,Q){if(!Q)return this.logger_.logMethod?.("applyAssigners__.skipped"),J;if(this.logger_.logMethod?.("applyAssigners__"),!Array.isArray(Q)){try{return Q(z,J)??J}catch(K){this.logger_.error("applyAssigners__","assigner_failed_atomic",K,{event:z,context:J})}return J}try{let K=J;for(let W=0;W<Q.length;W++){let X=Q[W],Y=X(z,K);if(Y)K=Y}return K}catch(K){return this.logger_.error("applyAssigners__","assigner_failed_atomic",K,{event:z,context:J}),J}}start_(){if(this.destroyed__)return;this.logger_.logMethod?.("start_");let z=this.stateSignal__.get(),J={type:"__init__"};if(this.executeEffects__(J,z.context,this.config_.states[z.name]?.entry),this.spawnActors__(J,z.context,this.config_.states[z.name]?.actor),this.processing__=!1,this.mailbox__.length>0)this.processMailbox__()}spawnActors__(z,J,Q){if(!Q){this.logger_.logMethod?.("spawnActors__.skipped");return}if(this.logger_.logMethod?.("spawnActors__"),!Array.isArray(Q)){try{let K=Q(J,this.dispatch);if(typeof K==="function")this.activeActorCleanups__.push(K)}catch(K){this.logger_.error("spawnActors__","actor_failed",K,{event:z,context:J})}return}for(let K=0;K<Q.length;K++){let W=Q[K];try{let X=W(J,this.dispatch);if(typeof X==="function")this.activeActorCleanups__.push(X)}catch(X){this.logger_.error("spawnActors__","actor_failed",X,{event:z,context:J,index:K})}}}cleanupActors__(){this.logger_.logMethodArgs?.("cleanupActors__",{count:this.activeActorCleanups__.length});for(let z=this.activeActorCleanups__.length-1;z>=0;z--)try{this.activeActorCleanups__[z]()}catch(J){this.logger_.error("cleanupActors__","cleanup_failed",J,{index:z})}this.activeActorCleanups__.length=0}destroy(z=!0){if(this.destroyed__)return;if(this.logger_.logMethod?.("destroy"),this.destroyed__=!0,this.mailbox__.length=0,this.cleanupActors__(),z)this.stateSignal__.destroy()}}function B(z){return new Z(z)}export{B as createFsmService,Z as FsmService};
|
|
3
|
+
|
|
4
|
+
//# debugId=2CCCCEC3109D01E664756E2164756E21
|
|
5
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fsm-service.ts", "../../src/facade.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import {createLogger, type AlwatrLogger} from '@alwatr/logger';\nimport {queueMicrotask} from '@alwatr/delay';\nimport type {SingleOrArray} from '@alwatr/type-helper';\nimport {\n createPersistentStateSignal,\n createStateSignal,\n type StateSignal,\n type PersistentStateSignal,\n type IReadonlySignal,\n} from '@alwatr/signal';\n\nimport type {StateMachineConfig, MachineState, MachineEvent, Transition, Effect, Assigner, Actor} from './type.js';\n\n/**\n * A generic, encapsulated service that creates, runs, and manages a finite state machine.\n * It handles signal creation, logic connection, and lifecycle management, providing a clean,\n * reactive API for interacting with the FSM.\n *\n * @template TState The union type of all possible state names.\n * @template TEvent The union type of all possible events.\n * @template TContext The type of the machine's context (extended state).\n */\nexport class FsmService<\n TState extends string,\n TEvent extends MachineEvent,\n TContext extends Record<string, unknown> = Record<string, never>,\n> {\n protected readonly logger_: AlwatrLogger;\n\n /** The public, read-only state signal. Subscribe to react to state changes. */\n public readonly stateSignal: IReadonlySignal<MachineState<TState, TContext>>;\n\n /**\n * The FIFO event mailbox. Events are processed strictly in dispatch order.\n */\n private readonly mailbox__: TEvent[] = [];\n\n /**\n * RTC re-entrancy guard. While `true`, an active loop is draining the mailbox;\n * re-entrant dispatches just enqueue and return.\n */\n private processing__ = true;\n\n /** Set once by `destroy()`. All dispatches after destruction are ignored (and logged). */\n private destroyed__ = false;\n\n /**\n * Cleanup callbacks for currently active state actors, in spawn order.\n * Executed in REVERSE (LIFO) order on state exit — standard resource semantics\n * (last acquired, first released).\n */\n private readonly activeActorCleanups__: (() => void)[] = [];\n\n private readonly stateSignal__:\n | StateSignal<MachineState<TState, TContext>>\n | PersistentStateSignal<MachineState<TState, TContext>>;\n\n constructor(\n protected readonly config_: StateMachineConfig<TState, TEvent, TContext>,\n stateSignal?: StateSignal<MachineState<TState, TContext>> | PersistentStateSignal<MachineState<TState, TContext>>,\n ) {\n this.logger_ = createLogger(`fsm:${this.config_.name}`);\n DEV_MODE && this.logger_.logMethodArgs?.('constructor', config_);\n\n const initialValue: MachineState<TState, TContext> = {\n name: config_.initial,\n context: config_.context,\n };\n this.stateSignal__ =\n stateSignal\n ?? (config_.persistent ?\n createPersistentStateSignal<MachineState<TState, TContext>>({\n name: `fsm-state-${config_.name}`,\n storageKey: config_.persistent.storageKey ?? config_.name,\n initialValue,\n schemaVersion: config_.persistent.schemaVersion,\n })\n : createStateSignal<MachineState<TState, TContext>>({\n name: `fsm-state-${config_.name}`,\n initialValue,\n }));\n\n this.stateSignal = this.stateSignal__.asReadonly();\n\n // Execute initial/rehydrated state entry effects and spawn its actors.\n queueMicrotask(() => this.start_());\n }\n\n /**\n * Synchronous accessor for the current machine state.\n * Prefer `stateSignal.subscribe()` for reactive consumers; use this getter for\n * imperative checks inside controllers/services.\n */\n public get state(): MachineState<TState, TContext> {\n return this.stateSignal__.get();\n }\n\n /**\n * Convenience predicate: returns true if the current finite state matches any\n * of the given names. Sugar for `service.state.name === 'x' || ...`.\n */\n public matches(...names: TState[]): boolean {\n return names.includes(this.stateSignal__.get().name);\n }\n\n /**\n * Dispatches an event to the FSM mailbox.\n *\n * Events are processed with Run-to-Completion semantics: if dispatched while a\n * transition is in flight (re-entrant dispatch from a guard/effect/actor), the\n * event is enqueued and processed deterministically right after the current\n * transition completes — in the same call stack, in FIFO order, with no loss.\n *\n * @param event The event to process.\n */\n public readonly dispatch = (event: TEvent): void => {\n DEV_MODE && this.logger_.logMethodArgs?.('dispatch', {event});\n this.mailbox__.push(event);\n this.processMailbox__();\n };\n\n private processMailbox__(): void {\n // RTC guard: an active loop is already draining the mailbox; it will pick\n // this event up after the current transition finishes.\n if (this.processing__) return;\n DEV_MODE && this.logger_.logMethod?.('processMailbox__');\n if (this.destroyed__) {\n DEV_MODE && this.logger_.incident?.('dispatch', 'dispatch_after_destroy');\n return;\n }\n this.processing__ = true;\n try {\n // Do NOT cache length. New events may be added during processing, and they MUST be processed in the same order (FIFO).\n for (let index = 0; index < this.mailbox__.length; index++) {\n this.processTransition__(this.mailbox__[index]);\n if (this.destroyed__) break;\n }\n } finally {\n this.processing__ = false;\n this.mailbox__.length = 0;\n }\n }\n\n /**\n * The core FSM logic that processes a single event and transitions the machine to a new state.\n * This step is atomic: exit effects -> assigners -> state commit -> entry effects -> actors.\n *\n * @param event The event to process.\n */\n private processTransition__(event: TEvent): void {\n const currentState = this.stateSignal__.get();\n DEV_MODE && this.logger_.logMethodArgs?.('processTransition__', {state: currentState.name, event});\n\n const transition = this.findTransition__(event, currentState);\n\n if (!transition) {\n DEV_MODE\n && this.logger_.incident?.('processTransition__', 'ignored_event', 'No valid transition found for event', {\n state: currentState.name,\n event,\n });\n return; // Event ignored, no transition occurs.\n }\n\n const targetStateName = transition.target ?? currentState.name;\n const isExternalTransition = transition.target !== undefined;\n\n // 1. External transition: run exit effects (with the OLD context, per SCXML semantics) and tear down the current state's actors.\n if (isExternalTransition) {\n this.executeEffects__(event, currentState.context, this.config_.states[currentState.name]?.exit);\n this.cleanupActors__();\n }\n\n // 2. Apply assigners to compute the next context (pure, atomic).\n const nextContext = this.applyAssigners__(event, currentState.context, transition.assigner);\n\n // 3. Commit the new state, notifying all subscribers (async via signal layer).\n const nextState: MachineState<TState, TContext> = {\n name: targetStateName,\n context: nextContext,\n };\n this.stateSignal__.set(nextState);\n\n // 4. External transition: run entry effects (with the NEW context) and spawn the target state's actors.\n if (isExternalTransition) {\n this.executeEffects__(event, nextState.context, this.config_.states[nextState.name]?.entry);\n this.spawnActors__(event, nextState.context, this.config_.states[nextState.name]?.actor);\n }\n }\n\n /**\n * Finds the first valid transition for the given event by evaluating guards in declaration order. A guard-less transition acts as an unconditional fallback.\n *\n * @param event The triggering event.\n * @param currentState The current state of the machine.\n * @returns The first matching transition or `undefined` if none are found.\n */\n private findTransition__(\n event: TEvent,\n currentState: MachineState<TState, TContext>,\n ): Transition<TState, TEvent, TContext> | undefined {\n DEV_MODE && this.logger_.logMethod?.('findTransition__');\n\n const currentStateConfig = this.config_.states[currentState.name];\n const transitions = currentStateConfig?.on?.[event.type as TEvent['type']] as\n | SingleOrArray<Transition<TState, TEvent, TContext>>\n | undefined;\n\n if (!transitions) return undefined;\n\n if (!Array.isArray(transitions)) {\n if (!transitions.guard) return transitions; // Unconditional fallback branch.\n try {\n if (transitions.guard(event, currentState.context)) {\n return transitions;\n }\n } catch (error) {\n this.logger_.error('findTransition__', 'guard_failed', error, {\n state: currentState.name,\n eventType: event.type,\n });\n }\n return undefined;\n }\n\n // else if transitions is an array\n\n for (let index = 0; index < transitions.length; index++) {\n const transition = transitions[index];\n if (!transition.guard) return transition; // Unconditional fallback branch.\n try {\n if (transition.guard(event, currentState.context)) {\n return transition;\n }\n } catch (error) {\n this.logger_.error('findTransition__', 'guard_failed', error, {\n state: currentState.name,\n eventType: event.type,\n index,\n });\n // Treated as guard === false: continue evaluating the next branch.\n }\n }\n\n return undefined;\n }\n\n /**\n * Sequentially executes a list of synchronous effects (side-effects).\n * Errors are caught and logged without stopping the FSM.\n *\n * @param event The event that triggered these effects.\n * @param context The context at the time of execution.\n * @param effects A single effect or an array of effects.\n */\n private executeEffects__(\n event: TEvent,\n context: Readonly<TContext>,\n effects?: SingleOrArray<Effect<TEvent, TContext>>,\n ): void {\n if (!effects) {\n DEV_MODE && this.logger_.logMethod?.('executeEffects__.skipped');\n return;\n }\n\n DEV_MODE && this.logger_.logMethod?.('executeEffects__');\n\n if (!Array.isArray(effects)) {\n try {\n effects(event, context);\n } catch (error) {\n this.logger_.error('executeEffects__', 'effect_failed', error, {\n event,\n context,\n });\n }\n return;\n }\n\n // else if effects is an array\n\n for (let index = 0; index < effects.length; index++) {\n const effect = effects[index];\n try {\n effect(event, context);\n } catch (error) {\n this.logger_.error('executeEffects__', 'effect_failed', error, {\n event,\n context,\n index,\n });\n }\n }\n }\n\n /**\n * Applies all assigner functions to the context to produce a new, updated context.\n *\n * This process is atomic (all-or-nothing): if any assigner throws, the original\n * context is returned and all updates are discarded.\n *\n * @param event The event that triggered the transition.\n * @param context The current context.\n * @param assigners A single assigner or an array of assigners.\n * @returns The new, updated context, or the original context if any assigner fails.\n */\n private applyAssigners__(\n event: TEvent,\n context: TContext,\n assigners?: SingleOrArray<Assigner<TEvent, TContext>>,\n ): TContext {\n if (!assigners) {\n DEV_MODE && this.logger_.logMethod?.('applyAssigners__.skipped');\n return context;\n }\n\n DEV_MODE && this.logger_.logMethod?.('applyAssigners__');\n\n if (!Array.isArray(assigners)) {\n try {\n return assigners(event, context) ?? context;\n } catch (error) {\n this.logger_.error('applyAssigners__', 'assigner_failed_atomic', error, {event, context});\n }\n return context;\n }\n\n // else if assigners is an array\n\n try {\n let accContext = context;\n for (let index = 0; index < assigners.length; index++) {\n const assigner = assigners[index];\n const nextContext = assigner(event, accContext);\n if (nextContext) {\n accContext = nextContext;\n }\n }\n return accContext;\n } catch (error) {\n this.logger_.error('applyAssigners__', 'assigner_failed_atomic', error, {event, context});\n // On ANY failure, discard all changes and return the original context.\n return context;\n }\n }\n\n /**\n * Starts the FSM by executing the entry effects and spawning the actors of the\n * initial (or rehydrated) state, using the synthetic `{type: '__init__'}` event.\n */\n protected start_(): void {\n if (this.destroyed__) return;\n DEV_MODE && this.logger_.logMethod?.('start_');\n const currentState = this.stateSignal__.get();\n const initEvent = {type: '__init__'} as unknown as TEvent;\n this.executeEffects__(initEvent, currentState.context, this.config_.states[currentState.name]?.entry);\n this.spawnActors__(initEvent, currentState.context, this.config_.states[currentState.name]?.actor);\n this.processing__ = false; // Allow processing of dispatched events after the initial setup is complete.\n if (this.mailbox__.length > 0) {\n this.processMailbox__(); // Process any events that were dispatched during the initial setup.\n }\n }\n\n /**\n * Spawns all configured actors for the entered state.\n */\n private spawnActors__(\n event: TEvent,\n context: Readonly<TContext>,\n actors?: SingleOrArray<Actor<TEvent, TContext>>,\n ): void {\n if (!actors) {\n DEV_MODE && this.logger_.logMethod?.('spawnActors__.skipped');\n return;\n }\n\n DEV_MODE && this.logger_.logMethod?.('spawnActors__');\n\n if (!Array.isArray(actors)) {\n try {\n const cleanup = actors(context, this.dispatch);\n if (typeof cleanup === 'function') {\n this.activeActorCleanups__.push(cleanup);\n }\n } catch (error) {\n this.logger_.error('spawnActors__', 'actor_failed', error, {event, context});\n }\n return;\n }\n\n // else if actors is an array\n\n for (let index = 0; index < actors.length; index++) {\n const actor = actors[index];\n try {\n const cleanup = actor(context, this.dispatch);\n if (typeof cleanup === 'function') {\n this.activeActorCleanups__.push(cleanup);\n }\n } catch (error) {\n this.logger_.error('spawnActors__', 'actor_failed', error, {event, context, index});\n }\n }\n }\n\n /**\n * Cleans up (destroys) all currently active state actors in REVERSE (LIFO) spawn order — standard resource-release semantics.\n */\n private cleanupActors__(): void {\n DEV_MODE && this.logger_.logMethodArgs?.('cleanupActors__', {count: this.activeActorCleanups__.length});\n for (let index = this.activeActorCleanups__.length - 1; index >= 0; index--) {\n try {\n this.activeActorCleanups__[index]();\n } catch (error) {\n this.logger_.error('cleanupActors__', 'cleanup_failed', error, {index});\n }\n }\n this.activeActorCleanups__.length = 0;\n }\n\n /**\n * Destroys the service, cleaning up actors, the mailbox, and owned signals to\n * prevent memory leaks. Idempotent — safe to call multiple times.\n *\n * @param destroyState If `true` (default), also destroys the state signal, preventing any future subscriptions or updates. Set to `false` to preserve the last state value for late subscribers even after destruction.\n */\n public destroy(destroyState = true): void {\n if (this.destroyed__) return;\n DEV_MODE && this.logger_.logMethod?.('destroy');\n this.destroyed__ = true;\n this.mailbox__.length = 0;\n this.cleanupActors__();\n if (destroyState) {\n this.stateSignal__.destroy();\n }\n }\n}\n",
|
|
6
|
+
"import {FsmService} from './fsm-service.js';\n\nimport type {MachineEvent, StateMachineConfig} from './type.js';\n\n/**\n * A simple and clean factory function for creating an `FsmService` instance.\n * This is the recommended way to instantiate a new state machine.\n *\n * @template TState - The union type of all possible states.\n * @template TEvent - The union type of all possible events.\n * @template TContext - The type of the machine's context.\n *\n * @param config - The machine's configuration object.\n * @returns A new, ready-to-use instance of `FsmService`.\n *\n * @example\n * ```ts\n * import {createFsmService} from '@alwatr/fsm';\n * import type {StateMachineConfig} from '@alwatr/fsm';\n *\n * // 1. Define types\n * type LightContext = {brightness: number};\n * type LightState = 'on' | 'off';\n * type LightEvent = {type: 'TOGGLE'} | {type: 'SET_BRIGHTNESS'; level: number};\n *\n * // 2. Config the state machine\n * const lightMachineConfig: StateMachineConfig<LightState, LightEvent, LightContext> = {\n * name: 'light-switch',\n * initial: 'off',\n * context: {brightness: 0},\n * states: {\n * off: {\n * on: {\n * TOGGLE: {\n * target: 'on',\n * assigners: [({context}) => ({...context, brightness: 100})],\n * },\n * },\n * },\n * on: {\n * on: {\n * TOGGLE: {target: 'off', assigners: [({context}) => ({...context, brightness: 0})]},\n * SET_BRIGHTNESS: {assigners: [({context, event}) => ({...context, brightness: event.level})]},\n * },\n * },\n * },\n * };\n *\n * // 3. Create the service\n * const lightService = createFsmService(lightMachineConfig);\n *\n * // 4. Use it in your application\n * lightService.stateSignal.subscribe((state) => {\n * console.log(`Light is ${state.name} with brightness ${state.context.brightness}`);\n * });\n *\n * lightService.dispatch({type: 'TOGGLE'}); // Light is on with brightness 100\n *\n * lightService.dispatch({type: 'SET_BRIGHTNESS', level: 50}); // Light is on with brightness 50\n *\n * // 5. Cleanup\n * // lightService.destroy();\n * ```\n */\nexport function createFsmService<\n TState extends string,\n TEvent extends MachineEvent,\n TContext extends Record<string, unknown> = Record<string, never>,\n>(config: StateMachineConfig<TState, TEvent, TContext>): FsmService<TState, TEvent, TContext> {\n return new FsmService(config);\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "2CCCCEC3109D01E664756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fsm-service.d.ts","sourceRoot":"","sources":["../src/fsm-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAG/D,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAC,kBAAkB,EAAE,YAAY,EAAE,YAAY,EAAsC,MAAM,WAAW,CAAC;AAEnH;;;;;;;;GAQG;AACH,qBAAa,UAAU,CACrB,MAAM,SAAS,MAAM,EACrB,MAAM,SAAS,YAAY,EAC3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;IAiC9D,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC;IA/B1E,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAEzC,+EAA+E;IAC/E,SAAgB,WAAW,EAAE,eAAe,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAE7E;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAE1C;;;OAGG;IACH,OAAO,CAAC,YAAY,CAAQ;IAE5B,0FAA0F;IAC1F,OAAO,CAAC,WAAW,CAAS;IAE5B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAsB;IAE5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAE4B;gBAGrC,OAAO,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EACxE,WAAW,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,GAAG,qBAAqB,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IA6BnH;;;;OAIG;IACH,IAAW,KAAK,IAAI,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAEjD;IAED;;;OAGG;IACI,OAAO,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO;IAI3C;;;;;;;;;OASG;IACH,SAAgB,QAAQ,GAAI,OAAO,MAAM,KAAG,IAAI,CAI9C;IAEF,OAAO,CAAC,gBAAgB;IAsBxB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;
|
|
1
|
+
{"version":3,"file":"fsm-service.d.ts","sourceRoot":"","sources":["../src/fsm-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAG/D,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAC,kBAAkB,EAAE,YAAY,EAAE,YAAY,EAAsC,MAAM,WAAW,CAAC;AAEnH;;;;;;;;GAQG;AACH,qBAAa,UAAU,CACrB,MAAM,SAAS,MAAM,EACrB,MAAM,SAAS,YAAY,EAC3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;IAiC9D,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC;IA/B1E,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAEzC,+EAA+E;IAC/E,SAAgB,WAAW,EAAE,eAAe,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAE7E;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAE1C;;;OAGG;IACH,OAAO,CAAC,YAAY,CAAQ;IAE5B,0FAA0F;IAC1F,OAAO,CAAC,WAAW,CAAS;IAE5B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAsB;IAE5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAE4B;gBAGrC,OAAO,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EACxE,WAAW,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,GAAG,qBAAqB,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IA6BnH;;;;OAIG;IACH,IAAW,KAAK,IAAI,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAEjD;IAED;;;OAGG;IACI,OAAO,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO;IAI3C;;;;;;;;;OASG;IACH,SAAgB,QAAQ,GAAI,OAAO,MAAM,KAAG,IAAI,CAI9C;IAEF,OAAO,CAAC,gBAAgB;IAsBxB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAyC3B;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAkDxB;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IAwCxB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,gBAAgB;IAwCxB;;;OAGG;IACH,SAAS,CAAC,MAAM,IAAI,IAAI;IAaxB;;OAEG;IACH,OAAO,CAAC,aAAa;IAuCrB;;OAEG;IACH,OAAO,CAAC,eAAe;IAYvB;;;;;OAKG;IACI,OAAO,CAAC,YAAY,UAAO,GAAG,IAAI;CAU1C"}
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/* 📦 @alwatr/fsm v9.33.
|
|
2
|
-
import{createLogger as j}from"@alwatr/logger";import{queueMicrotask as q}from"@alwatr/delay";import{createPersistentStateSignal as P,createStateSignal as U}from"@alwatr/signal";class Z{config_;logger_;stateSignal;mailbox__=[];processing__=!0;destroyed__=!1;activeActorCleanups__=[];stateSignal__;constructor(z,J){this.config_=z;this.logger_=j(`fsm:${this.config_.name}`)
|
|
1
|
+
/* 📦 @alwatr/fsm v9.33.1 */
|
|
2
|
+
import{createLogger as j}from"@alwatr/logger";import{queueMicrotask as q}from"@alwatr/delay";import{createPersistentStateSignal as P,createStateSignal as U}from"@alwatr/signal";class Z{config_;logger_;stateSignal;mailbox__=[];processing__=!0;destroyed__=!1;activeActorCleanups__=[];stateSignal__;constructor(z,J){this.config_=z;this.logger_=j(`fsm:${this.config_.name}`);let Q={name:z.initial,context:z.context};this.stateSignal__=J??(z.persistent?P({name:`fsm-state-${z.name}`,storageKey:z.persistent.storageKey??z.name,initialValue:Q,schemaVersion:z.persistent.schemaVersion}):U({name:`fsm-state-${z.name}`,initialValue:Q})),this.stateSignal=this.stateSignal__.asReadonly(),q(()=>this.start_())}get state(){return this.stateSignal__.get()}matches(...z){return z.includes(this.stateSignal__.get().name)}dispatch=(z)=>{this.mailbox__.push(z),this.processMailbox__()};processMailbox__(){if(this.processing__)return;if(this.destroyed__)return;this.processing__=!0;try{for(let z=0;z<this.mailbox__.length;z++)if(this.processTransition__(this.mailbox__[z]),this.destroyed__)break}finally{this.processing__=!1,this.mailbox__.length=0}}processTransition__(z){let J=this.stateSignal__.get(),Q=this.findTransition__(z,J);if(!Q)return;let K=Q.target??J.name,W=Q.target!==void 0;if(W)this.executeEffects__(z,J.context,this.config_.states[J.name]?.exit),this.cleanupActors__();let X=this.applyAssigners__(z,J.context,Q.assigner),Y={name:K,context:X};if(this.stateSignal__.set(Y),W)this.executeEffects__(z,Y.context,this.config_.states[Y.name]?.entry),this.spawnActors__(z,Y.context,this.config_.states[Y.name]?.actor)}findTransition__(z,J){let K=this.config_.states[J.name]?.on?.[z.type];if(!K)return;if(!Array.isArray(K)){if(!K.guard)return K;try{if(K.guard(z,J.context))return K}catch(W){this.logger_.error("findTransition__","guard_failed",W,{state:J.name,eventType:z.type})}return}for(let W=0;W<K.length;W++){let X=K[W];if(!X.guard)return X;try{if(X.guard(z,J.context))return X}catch(Y){this.logger_.error("findTransition__","guard_failed",Y,{state:J.name,eventType:z.type,index:W})}}return}executeEffects__(z,J,Q){if(!Q)return;if(!Array.isArray(Q)){try{Q(z,J)}catch(K){this.logger_.error("executeEffects__","effect_failed",K,{event:z,context:J})}return}for(let K=0;K<Q.length;K++){let W=Q[K];try{W(z,J)}catch(X){this.logger_.error("executeEffects__","effect_failed",X,{event:z,context:J,index:K})}}}applyAssigners__(z,J,Q){if(!Q)return J;if(!Array.isArray(Q)){try{return Q(z,J)??J}catch(K){this.logger_.error("applyAssigners__","assigner_failed_atomic",K,{event:z,context:J})}return J}try{let K=J;for(let W=0;W<Q.length;W++){let X=Q[W],Y=X(z,K);if(Y)K=Y}return K}catch(K){return this.logger_.error("applyAssigners__","assigner_failed_atomic",K,{event:z,context:J}),J}}start_(){if(this.destroyed__)return;let z=this.stateSignal__.get(),J={type:"__init__"};if(this.executeEffects__(J,z.context,this.config_.states[z.name]?.entry),this.spawnActors__(J,z.context,this.config_.states[z.name]?.actor),this.processing__=!1,this.mailbox__.length>0)this.processMailbox__()}spawnActors__(z,J,Q){if(!Q)return;if(!Array.isArray(Q)){try{let K=Q(J,this.dispatch);if(typeof K==="function")this.activeActorCleanups__.push(K)}catch(K){this.logger_.error("spawnActors__","actor_failed",K,{event:z,context:J})}return}for(let K=0;K<Q.length;K++){let W=Q[K];try{let X=W(J,this.dispatch);if(typeof X==="function")this.activeActorCleanups__.push(X)}catch(X){this.logger_.error("spawnActors__","actor_failed",X,{event:z,context:J,index:K})}}}cleanupActors__(){for(let z=this.activeActorCleanups__.length-1;z>=0;z--)try{this.activeActorCleanups__[z]()}catch(J){this.logger_.error("cleanupActors__","cleanup_failed",J,{index:z})}this.activeActorCleanups__.length=0}destroy(z=!0){if(this.destroyed__)return;if(this.destroyed__=!0,this.mailbox__.length=0,this.cleanupActors__(),z)this.stateSignal__.destroy()}}function B(z){return new Z(z)}export{B as createFsmService,Z as FsmService};
|
|
3
3
|
|
|
4
|
-
//# debugId=
|
|
4
|
+
//# debugId=21C5CE2D42EDADD164756E2164756E21
|
|
5
5
|
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/fsm-service.ts", "../src/facade.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import {createLogger, type AlwatrLogger} from '@alwatr/logger';\nimport {queueMicrotask} from '@alwatr/delay';\nimport type {SingleOrArray} from '@alwatr/type-helper';\nimport {\n createPersistentStateSignal,\n createStateSignal,\n type StateSignal,\n type PersistentStateSignal,\n type IReadonlySignal,\n} from '@alwatr/signal';\n\nimport type {StateMachineConfig, MachineState, MachineEvent, Transition, Effect, Assigner, Actor} from './type.js';\n\n/**\n * A generic, encapsulated service that creates, runs, and manages a finite state machine.\n * It handles signal creation, logic connection, and lifecycle management, providing a clean,\n * reactive API for interacting with the FSM.\n *\n * @template TState The union type of all possible state names.\n * @template TEvent The union type of all possible events.\n * @template TContext The type of the machine's context (extended state).\n */\nexport class FsmService<\n TState extends string,\n TEvent extends MachineEvent,\n TContext extends Record<string, unknown> = Record<string, never>,\n> {\n protected readonly logger_: AlwatrLogger;\n\n /** The public, read-only state signal. Subscribe to react to state changes. */\n public readonly stateSignal: IReadonlySignal<MachineState<TState, TContext>>;\n\n /**\n * The FIFO event mailbox. Events are processed strictly in dispatch order.\n */\n private readonly mailbox__: TEvent[] = [];\n\n /**\n * RTC re-entrancy guard. While `true`, an active loop is draining the mailbox;\n * re-entrant dispatches just enqueue and return.\n */\n private processing__ = true;\n\n /** Set once by `destroy()`. All dispatches after destruction are ignored (and logged). */\n private destroyed__ = false;\n\n /**\n * Cleanup callbacks for currently active state actors, in spawn order.\n * Executed in REVERSE (LIFO) order on state exit — standard resource semantics\n * (last acquired, first released).\n */\n private readonly activeActorCleanups__: (() => void)[] = [];\n\n private readonly stateSignal__:\n | StateSignal<MachineState<TState, TContext>>\n | PersistentStateSignal<MachineState<TState, TContext>>;\n\n constructor(\n protected readonly config_: StateMachineConfig<TState, TEvent, TContext>,\n stateSignal?: StateSignal<MachineState<TState, TContext>> | PersistentStateSignal<MachineState<TState, TContext>>,\n ) {\n this.logger_ = createLogger(`fsm:${this.config_.name}`);\n this.logger_.logMethodArgs?.('constructor', config_);\n\n const initialValue: MachineState<TState, TContext> = {\n name: config_.initial,\n context: config_.context,\n };\n this.stateSignal__ =\n stateSignal\n ?? (config_.persistent ?\n createPersistentStateSignal<MachineState<TState, TContext>>({\n name: `fsm-state-${config_.name}`,\n storageKey: config_.persistent.storageKey ?? config_.name,\n initialValue,\n schemaVersion: config_.persistent.schemaVersion,\n })\n : createStateSignal<MachineState<TState, TContext>>({\n name: `fsm-state-${config_.name}`,\n initialValue,\n }));\n\n this.stateSignal = this.stateSignal__.asReadonly();\n\n // Execute initial/rehydrated state entry effects and spawn its actors.\n queueMicrotask(() => this.start_());\n }\n\n /**\n * Synchronous accessor for the current machine state.\n * Prefer `stateSignal.subscribe()` for reactive consumers; use this getter for\n * imperative checks inside controllers/services.\n */\n public get state(): MachineState<TState, TContext> {\n return this.stateSignal__.get();\n }\n\n /**\n * Convenience predicate: returns true if the current finite state matches any\n * of the given names. Sugar for `service.state.name === 'x' || ...`.\n */\n public matches(...names: TState[]): boolean {\n return names.includes(this.stateSignal__.get().name);\n }\n\n /**\n * Dispatches an event to the FSM mailbox.\n *\n * Events are processed with Run-to-Completion semantics: if dispatched while a\n * transition is in flight (re-entrant dispatch from a guard/effect/actor), the\n * event is enqueued and processed deterministically right after the current\n * transition completes — in the same call stack, in FIFO order, with no loss.\n *\n * @param event The event to process.\n */\n public readonly dispatch = (event: TEvent): void => {\n this.logger_.logMethodArgs?.('dispatch', {event});\n this.mailbox__.push(event);\n this.processMailbox__();\n };\n\n private processMailbox__(): void {\n // RTC guard: an active loop is already draining the mailbox; it will pick\n // this event up after the current transition finishes.\n if (this.processing__) return;\n this.logger_.logMethod?.('processMailbox__');\n if (this.destroyed__) {\n this.logger_.incident?.('dispatch', 'dispatch_after_destroy', {event});\n return;\n }\n this.processing__ = true;\n try {\n // Do NOT cache length. New events may be added during processing, and they MUST be processed in the same order (FIFO).\n for (let index = 0; index < this.mailbox__.length; index++) {\n this.processTransition__(this.mailbox__[index]);\n if (this.destroyed__) break;\n }\n } finally {\n this.processing__ = false;\n this.mailbox__.length = 0;\n }\n }\n\n /**\n * The core FSM logic that processes a single event and transitions the machine to a new state.\n * This step is atomic: exit effects -> assigners -> state commit -> entry effects -> actors.\n *\n * @param event The event to process.\n */\n private processTransition__(event: TEvent): void {\n const currentState = this.stateSignal__.get();\n this.logger_.logMethodArgs?.('processTransition__', {state: currentState.name, event});\n\n const transition = this.findTransition__(event, currentState);\n\n if (!transition) {\n this.logger_.incident?.('processTransition__', 'ignored_event', 'No valid transition found for event', {\n state: currentState.name,\n event,\n });\n return; // Event ignored, no transition occurs.\n }\n\n const targetStateName = transition.target ?? currentState.name;\n const isExternalTransition = transition.target !== undefined;\n\n // 1. External transition: run exit effects (with the OLD context, per SCXML semantics) and tear down the current state's actors.\n if (isExternalTransition) {\n this.executeEffects__(event, currentState.context, this.config_.states[currentState.name]?.exit);\n this.cleanupActors__();\n }\n\n // 2. Apply assigners to compute the next context (pure, atomic).\n const nextContext = this.applyAssigners__(event, currentState.context, transition.assigner);\n\n // 3. Commit the new state, notifying all subscribers (async via signal layer).\n const nextState: MachineState<TState, TContext> = {\n name: targetStateName,\n context: nextContext,\n };\n this.stateSignal__.set(nextState);\n\n // 4. External transition: run entry effects (with the NEW context) and spawn the target state's actors.\n if (isExternalTransition) {\n this.executeEffects__(event, nextState.context, this.config_.states[nextState.name]?.entry);\n this.spawnActors__(event, nextState.context, this.config_.states[nextState.name]?.actor);\n }\n }\n\n /**\n * Finds the first valid transition for the given event by evaluating guards in declaration order. A guard-less transition acts as an unconditional fallback.\n *\n * @param event The triggering event.\n * @param currentState The current state of the machine.\n * @returns The first matching transition or `undefined` if none are found.\n */\n private findTransition__(\n event: TEvent,\n currentState: MachineState<TState, TContext>,\n ): Transition<TState, TEvent, TContext> | undefined {\n this.logger_.logMethod?.('findTransition__');\n\n const currentStateConfig = this.config_.states[currentState.name];\n const transitions = currentStateConfig?.on?.[event.type as TEvent['type']] as\n | SingleOrArray<Transition<TState, TEvent, TContext>>\n | undefined;\n\n if (!transitions) return undefined;\n\n if (!Array.isArray(transitions)) {\n if (!transitions.guard) return transitions; // Unconditional fallback branch.\n try {\n if (transitions.guard(event, currentState.context)) {\n return transitions;\n }\n } catch (error) {\n this.logger_.error('findTransition__', 'guard_failed', error, {\n state: currentState.name,\n eventType: event.type,\n });\n }\n return undefined;\n }\n\n // else if transitions is an array\n\n for (let index = 0; index < transitions.length; index++) {\n const transition = transitions[index];\n if (!transition.guard) return transition; // Unconditional fallback branch.\n try {\n if (transition.guard(event, currentState.context)) {\n return transition;\n }\n } catch (error) {\n this.logger_.error('findTransition__', 'guard_failed', error, {\n state: currentState.name,\n eventType: event.type,\n index,\n });\n // Treated as guard === false: continue evaluating the next branch.\n }\n }\n\n return undefined;\n }\n\n /**\n * Sequentially executes a list of synchronous effects (side-effects).\n * Errors are caught and logged without stopping the FSM.\n *\n * @param event The event that triggered these effects.\n * @param context The context at the time of execution.\n * @param effects A single effect or an array of effects.\n */\n private executeEffects__(\n event: TEvent,\n context: Readonly<TContext>,\n effects?: SingleOrArray<Effect<TEvent, TContext>>,\n ): void {\n if (!effects) {\n this.logger_.logMethod?.('executeEffects__.skipped');\n return;\n }\n\n this.logger_.logMethod?.('executeEffects__');\n\n if (!Array.isArray(effects)) {\n try {\n effects(event, context);\n } catch (error) {\n this.logger_.error('executeEffects__', 'effect_failed', error, {\n event,\n context,\n });\n }\n return;\n }\n\n // else if effects is an array\n\n for (let index = 0; index < effects.length; index++) {\n const effect = effects[index];\n try {\n effect(event, context);\n } catch (error) {\n this.logger_.error('executeEffects__', 'effect_failed', error, {\n event,\n context,\n index,\n });\n }\n }\n }\n\n /**\n * Applies all assigner functions to the context to produce a new, updated context.\n *\n * This process is atomic (all-or-nothing): if any assigner throws, the original\n * context is returned and all updates are discarded.\n *\n * @param event The event that triggered the transition.\n * @param context The current context.\n * @param assigners A single assigner or an array of assigners.\n * @returns The new, updated context, or the original context if any assigner fails.\n */\n private applyAssigners__(\n event: TEvent,\n context: TContext,\n assigners?: SingleOrArray<Assigner<TEvent, TContext>>,\n ): TContext {\n if (!assigners) {\n this.logger_.logMethod?.('applyAssigners__.skipped');\n return context;\n }\n\n this.logger_.logMethod?.('applyAssigners__');\n\n if (!Array.isArray(assigners)) {\n try {\n return assigners(event, context) ?? context;\n } catch (error) {\n this.logger_.error('applyAssigners__', 'assigner_failed_atomic', error, {event, context});\n }\n return context;\n }\n\n // else if assigners is an array\n\n try {\n let accContext = context;\n for (let index = 0; index < assigners.length; index++) {\n const assigner = assigners[index];\n const nextContext = assigner(event, accContext);\n if (nextContext) {\n accContext = nextContext;\n }\n }\n return accContext;\n } catch (error) {\n this.logger_.error('applyAssigners__', 'assigner_failed_atomic', error, {event, context});\n // On ANY failure, discard all changes and return the original context.\n return context;\n }\n }\n\n /**\n * Starts the FSM by executing the entry effects and spawning the actors of the\n * initial (or rehydrated) state, using the synthetic `{type: '__init__'}` event.\n */\n protected start_(): void {\n if (this.destroyed__) return;\n this.logger_.logMethod?.('start_');\n const currentState = this.stateSignal__.get();\n const initEvent = {type: '__init__'} as unknown as TEvent;\n this.executeEffects__(initEvent, currentState.context, this.config_.states[currentState.name]?.entry);\n this.spawnActors__(initEvent, currentState.context, this.config_.states[currentState.name]?.actor);\n this.processing__ = false; // Allow processing of dispatched events after the initial setup is complete.\n if (this.mailbox__.length > 0) {\n this.processMailbox__(); // Process any events that were dispatched during the initial setup.\n }\n }\n\n /**\n * Spawns all configured actors for the entered state.\n */\n private spawnActors__(\n event: TEvent,\n context: Readonly<TContext>,\n actors?: SingleOrArray<Actor<TEvent, TContext>>,\n ): void {\n if (!actors) {\n this.logger_.logMethod?.('spawnActors__.skipped');\n return;\n }\n\n this.logger_.logMethod?.('spawnActors__');\n\n if (!Array.isArray(actors)) {\n try {\n const cleanup = actors(context, this.dispatch);\n if (typeof cleanup === 'function') {\n this.activeActorCleanups__.push(cleanup);\n }\n } catch (error) {\n this.logger_.error('spawnActors__', 'actor_failed', error, {event, context});\n }\n return;\n }\n\n // else if actors is an array\n\n for (let index = 0; index < actors.length; index++) {\n const actor = actors[index];\n try {\n const cleanup = actor(context, this.dispatch);\n if (typeof cleanup === 'function') {\n this.activeActorCleanups__.push(cleanup);\n }\n } catch (error) {\n this.logger_.error('spawnActors__', 'actor_failed', error, {event, context, index});\n }\n }\n }\n\n /**\n * Cleans up (destroys) all currently active state actors in REVERSE (LIFO) spawn order — standard resource-release semantics.\n */\n private cleanupActors__(): void {\n this.logger_.logMethodArgs?.('cleanupActors__', {count: this.activeActorCleanups__.length});\n for (let index = this.activeActorCleanups__.length - 1; index >= 0; index--) {\n try {\n this.activeActorCleanups__[index]();\n } catch (error) {\n this.logger_.error('cleanupActors__', 'cleanup_failed', error, {index});\n }\n }\n this.activeActorCleanups__.length = 0;\n }\n\n /**\n * Destroys the service, cleaning up actors, the mailbox, and owned signals to\n * prevent memory leaks. Idempotent — safe to call multiple times.\n *\n * @param destroyState If `true` (default), also destroys the state signal, preventing any future subscriptions or updates. Set to `false` to preserve the last state value for late subscribers even after destruction.\n */\n public destroy(destroyState = true): void {\n if (this.destroyed__) return;\n this.logger_.logMethod?.('destroy');\n this.destroyed__ = true;\n this.mailbox__.length = 0;\n this.cleanupActors__();\n if (destroyState) {\n this.stateSignal__.destroy();\n }\n }\n}\n",
|
|
5
|
+
"import {createLogger, type AlwatrLogger} from '@alwatr/logger';\nimport {queueMicrotask} from '@alwatr/delay';\nimport type {SingleOrArray} from '@alwatr/type-helper';\nimport {\n createPersistentStateSignal,\n createStateSignal,\n type StateSignal,\n type PersistentStateSignal,\n type IReadonlySignal,\n} from '@alwatr/signal';\n\nimport type {StateMachineConfig, MachineState, MachineEvent, Transition, Effect, Assigner, Actor} from './type.js';\n\n/**\n * A generic, encapsulated service that creates, runs, and manages a finite state machine.\n * It handles signal creation, logic connection, and lifecycle management, providing a clean,\n * reactive API for interacting with the FSM.\n *\n * @template TState The union type of all possible state names.\n * @template TEvent The union type of all possible events.\n * @template TContext The type of the machine's context (extended state).\n */\nexport class FsmService<\n TState extends string,\n TEvent extends MachineEvent,\n TContext extends Record<string, unknown> = Record<string, never>,\n> {\n protected readonly logger_: AlwatrLogger;\n\n /** The public, read-only state signal. Subscribe to react to state changes. */\n public readonly stateSignal: IReadonlySignal<MachineState<TState, TContext>>;\n\n /**\n * The FIFO event mailbox. Events are processed strictly in dispatch order.\n */\n private readonly mailbox__: TEvent[] = [];\n\n /**\n * RTC re-entrancy guard. While `true`, an active loop is draining the mailbox;\n * re-entrant dispatches just enqueue and return.\n */\n private processing__ = true;\n\n /** Set once by `destroy()`. All dispatches after destruction are ignored (and logged). */\n private destroyed__ = false;\n\n /**\n * Cleanup callbacks for currently active state actors, in spawn order.\n * Executed in REVERSE (LIFO) order on state exit — standard resource semantics\n * (last acquired, first released).\n */\n private readonly activeActorCleanups__: (() => void)[] = [];\n\n private readonly stateSignal__:\n | StateSignal<MachineState<TState, TContext>>\n | PersistentStateSignal<MachineState<TState, TContext>>;\n\n constructor(\n protected readonly config_: StateMachineConfig<TState, TEvent, TContext>,\n stateSignal?: StateSignal<MachineState<TState, TContext>> | PersistentStateSignal<MachineState<TState, TContext>>,\n ) {\n this.logger_ = createLogger(`fsm:${this.config_.name}`);\n DEV_MODE && this.logger_.logMethodArgs?.('constructor', config_);\n\n const initialValue: MachineState<TState, TContext> = {\n name: config_.initial,\n context: config_.context,\n };\n this.stateSignal__ =\n stateSignal\n ?? (config_.persistent ?\n createPersistentStateSignal<MachineState<TState, TContext>>({\n name: `fsm-state-${config_.name}`,\n storageKey: config_.persistent.storageKey ?? config_.name,\n initialValue,\n schemaVersion: config_.persistent.schemaVersion,\n })\n : createStateSignal<MachineState<TState, TContext>>({\n name: `fsm-state-${config_.name}`,\n initialValue,\n }));\n\n this.stateSignal = this.stateSignal__.asReadonly();\n\n // Execute initial/rehydrated state entry effects and spawn its actors.\n queueMicrotask(() => this.start_());\n }\n\n /**\n * Synchronous accessor for the current machine state.\n * Prefer `stateSignal.subscribe()` for reactive consumers; use this getter for\n * imperative checks inside controllers/services.\n */\n public get state(): MachineState<TState, TContext> {\n return this.stateSignal__.get();\n }\n\n /**\n * Convenience predicate: returns true if the current finite state matches any\n * of the given names. Sugar for `service.state.name === 'x' || ...`.\n */\n public matches(...names: TState[]): boolean {\n return names.includes(this.stateSignal__.get().name);\n }\n\n /**\n * Dispatches an event to the FSM mailbox.\n *\n * Events are processed with Run-to-Completion semantics: if dispatched while a\n * transition is in flight (re-entrant dispatch from a guard/effect/actor), the\n * event is enqueued and processed deterministically right after the current\n * transition completes — in the same call stack, in FIFO order, with no loss.\n *\n * @param event The event to process.\n */\n public readonly dispatch = (event: TEvent): void => {\n DEV_MODE && this.logger_.logMethodArgs?.('dispatch', {event});\n this.mailbox__.push(event);\n this.processMailbox__();\n };\n\n private processMailbox__(): void {\n // RTC guard: an active loop is already draining the mailbox; it will pick\n // this event up after the current transition finishes.\n if (this.processing__) return;\n DEV_MODE && this.logger_.logMethod?.('processMailbox__');\n if (this.destroyed__) {\n DEV_MODE && this.logger_.incident?.('dispatch', 'dispatch_after_destroy');\n return;\n }\n this.processing__ = true;\n try {\n // Do NOT cache length. New events may be added during processing, and they MUST be processed in the same order (FIFO).\n for (let index = 0; index < this.mailbox__.length; index++) {\n this.processTransition__(this.mailbox__[index]);\n if (this.destroyed__) break;\n }\n } finally {\n this.processing__ = false;\n this.mailbox__.length = 0;\n }\n }\n\n /**\n * The core FSM logic that processes a single event and transitions the machine to a new state.\n * This step is atomic: exit effects -> assigners -> state commit -> entry effects -> actors.\n *\n * @param event The event to process.\n */\n private processTransition__(event: TEvent): void {\n const currentState = this.stateSignal__.get();\n DEV_MODE && this.logger_.logMethodArgs?.('processTransition__', {state: currentState.name, event});\n\n const transition = this.findTransition__(event, currentState);\n\n if (!transition) {\n DEV_MODE\n && this.logger_.incident?.('processTransition__', 'ignored_event', 'No valid transition found for event', {\n state: currentState.name,\n event,\n });\n return; // Event ignored, no transition occurs.\n }\n\n const targetStateName = transition.target ?? currentState.name;\n const isExternalTransition = transition.target !== undefined;\n\n // 1. External transition: run exit effects (with the OLD context, per SCXML semantics) and tear down the current state's actors.\n if (isExternalTransition) {\n this.executeEffects__(event, currentState.context, this.config_.states[currentState.name]?.exit);\n this.cleanupActors__();\n }\n\n // 2. Apply assigners to compute the next context (pure, atomic).\n const nextContext = this.applyAssigners__(event, currentState.context, transition.assigner);\n\n // 3. Commit the new state, notifying all subscribers (async via signal layer).\n const nextState: MachineState<TState, TContext> = {\n name: targetStateName,\n context: nextContext,\n };\n this.stateSignal__.set(nextState);\n\n // 4. External transition: run entry effects (with the NEW context) and spawn the target state's actors.\n if (isExternalTransition) {\n this.executeEffects__(event, nextState.context, this.config_.states[nextState.name]?.entry);\n this.spawnActors__(event, nextState.context, this.config_.states[nextState.name]?.actor);\n }\n }\n\n /**\n * Finds the first valid transition for the given event by evaluating guards in declaration order. A guard-less transition acts as an unconditional fallback.\n *\n * @param event The triggering event.\n * @param currentState The current state of the machine.\n * @returns The first matching transition or `undefined` if none are found.\n */\n private findTransition__(\n event: TEvent,\n currentState: MachineState<TState, TContext>,\n ): Transition<TState, TEvent, TContext> | undefined {\n DEV_MODE && this.logger_.logMethod?.('findTransition__');\n\n const currentStateConfig = this.config_.states[currentState.name];\n const transitions = currentStateConfig?.on?.[event.type as TEvent['type']] as\n | SingleOrArray<Transition<TState, TEvent, TContext>>\n | undefined;\n\n if (!transitions) return undefined;\n\n if (!Array.isArray(transitions)) {\n if (!transitions.guard) return transitions; // Unconditional fallback branch.\n try {\n if (transitions.guard(event, currentState.context)) {\n return transitions;\n }\n } catch (error) {\n this.logger_.error('findTransition__', 'guard_failed', error, {\n state: currentState.name,\n eventType: event.type,\n });\n }\n return undefined;\n }\n\n // else if transitions is an array\n\n for (let index = 0; index < transitions.length; index++) {\n const transition = transitions[index];\n if (!transition.guard) return transition; // Unconditional fallback branch.\n try {\n if (transition.guard(event, currentState.context)) {\n return transition;\n }\n } catch (error) {\n this.logger_.error('findTransition__', 'guard_failed', error, {\n state: currentState.name,\n eventType: event.type,\n index,\n });\n // Treated as guard === false: continue evaluating the next branch.\n }\n }\n\n return undefined;\n }\n\n /**\n * Sequentially executes a list of synchronous effects (side-effects).\n * Errors are caught and logged without stopping the FSM.\n *\n * @param event The event that triggered these effects.\n * @param context The context at the time of execution.\n * @param effects A single effect or an array of effects.\n */\n private executeEffects__(\n event: TEvent,\n context: Readonly<TContext>,\n effects?: SingleOrArray<Effect<TEvent, TContext>>,\n ): void {\n if (!effects) {\n DEV_MODE && this.logger_.logMethod?.('executeEffects__.skipped');\n return;\n }\n\n DEV_MODE && this.logger_.logMethod?.('executeEffects__');\n\n if (!Array.isArray(effects)) {\n try {\n effects(event, context);\n } catch (error) {\n this.logger_.error('executeEffects__', 'effect_failed', error, {\n event,\n context,\n });\n }\n return;\n }\n\n // else if effects is an array\n\n for (let index = 0; index < effects.length; index++) {\n const effect = effects[index];\n try {\n effect(event, context);\n } catch (error) {\n this.logger_.error('executeEffects__', 'effect_failed', error, {\n event,\n context,\n index,\n });\n }\n }\n }\n\n /**\n * Applies all assigner functions to the context to produce a new, updated context.\n *\n * This process is atomic (all-or-nothing): if any assigner throws, the original\n * context is returned and all updates are discarded.\n *\n * @param event The event that triggered the transition.\n * @param context The current context.\n * @param assigners A single assigner or an array of assigners.\n * @returns The new, updated context, or the original context if any assigner fails.\n */\n private applyAssigners__(\n event: TEvent,\n context: TContext,\n assigners?: SingleOrArray<Assigner<TEvent, TContext>>,\n ): TContext {\n if (!assigners) {\n DEV_MODE && this.logger_.logMethod?.('applyAssigners__.skipped');\n return context;\n }\n\n DEV_MODE && this.logger_.logMethod?.('applyAssigners__');\n\n if (!Array.isArray(assigners)) {\n try {\n return assigners(event, context) ?? context;\n } catch (error) {\n this.logger_.error('applyAssigners__', 'assigner_failed_atomic', error, {event, context});\n }\n return context;\n }\n\n // else if assigners is an array\n\n try {\n let accContext = context;\n for (let index = 0; index < assigners.length; index++) {\n const assigner = assigners[index];\n const nextContext = assigner(event, accContext);\n if (nextContext) {\n accContext = nextContext;\n }\n }\n return accContext;\n } catch (error) {\n this.logger_.error('applyAssigners__', 'assigner_failed_atomic', error, {event, context});\n // On ANY failure, discard all changes and return the original context.\n return context;\n }\n }\n\n /**\n * Starts the FSM by executing the entry effects and spawning the actors of the\n * initial (or rehydrated) state, using the synthetic `{type: '__init__'}` event.\n */\n protected start_(): void {\n if (this.destroyed__) return;\n DEV_MODE && this.logger_.logMethod?.('start_');\n const currentState = this.stateSignal__.get();\n const initEvent = {type: '__init__'} as unknown as TEvent;\n this.executeEffects__(initEvent, currentState.context, this.config_.states[currentState.name]?.entry);\n this.spawnActors__(initEvent, currentState.context, this.config_.states[currentState.name]?.actor);\n this.processing__ = false; // Allow processing of dispatched events after the initial setup is complete.\n if (this.mailbox__.length > 0) {\n this.processMailbox__(); // Process any events that were dispatched during the initial setup.\n }\n }\n\n /**\n * Spawns all configured actors for the entered state.\n */\n private spawnActors__(\n event: TEvent,\n context: Readonly<TContext>,\n actors?: SingleOrArray<Actor<TEvent, TContext>>,\n ): void {\n if (!actors) {\n DEV_MODE && this.logger_.logMethod?.('spawnActors__.skipped');\n return;\n }\n\n DEV_MODE && this.logger_.logMethod?.('spawnActors__');\n\n if (!Array.isArray(actors)) {\n try {\n const cleanup = actors(context, this.dispatch);\n if (typeof cleanup === 'function') {\n this.activeActorCleanups__.push(cleanup);\n }\n } catch (error) {\n this.logger_.error('spawnActors__', 'actor_failed', error, {event, context});\n }\n return;\n }\n\n // else if actors is an array\n\n for (let index = 0; index < actors.length; index++) {\n const actor = actors[index];\n try {\n const cleanup = actor(context, this.dispatch);\n if (typeof cleanup === 'function') {\n this.activeActorCleanups__.push(cleanup);\n }\n } catch (error) {\n this.logger_.error('spawnActors__', 'actor_failed', error, {event, context, index});\n }\n }\n }\n\n /**\n * Cleans up (destroys) all currently active state actors in REVERSE (LIFO) spawn order — standard resource-release semantics.\n */\n private cleanupActors__(): void {\n DEV_MODE && this.logger_.logMethodArgs?.('cleanupActors__', {count: this.activeActorCleanups__.length});\n for (let index = this.activeActorCleanups__.length - 1; index >= 0; index--) {\n try {\n this.activeActorCleanups__[index]();\n } catch (error) {\n this.logger_.error('cleanupActors__', 'cleanup_failed', error, {index});\n }\n }\n this.activeActorCleanups__.length = 0;\n }\n\n /**\n * Destroys the service, cleaning up actors, the mailbox, and owned signals to\n * prevent memory leaks. Idempotent — safe to call multiple times.\n *\n * @param destroyState If `true` (default), also destroys the state signal, preventing any future subscriptions or updates. Set to `false` to preserve the last state value for late subscribers even after destruction.\n */\n public destroy(destroyState = true): void {\n if (this.destroyed__) return;\n DEV_MODE && this.logger_.logMethod?.('destroy');\n this.destroyed__ = true;\n this.mailbox__.length = 0;\n this.cleanupActors__();\n if (destroyState) {\n this.stateSignal__.destroy();\n }\n }\n}\n",
|
|
6
6
|
"import {FsmService} from './fsm-service.js';\n\nimport type {MachineEvent, StateMachineConfig} from './type.js';\n\n/**\n * A simple and clean factory function for creating an `FsmService` instance.\n * This is the recommended way to instantiate a new state machine.\n *\n * @template TState - The union type of all possible states.\n * @template TEvent - The union type of all possible events.\n * @template TContext - The type of the machine's context.\n *\n * @param config - The machine's configuration object.\n * @returns A new, ready-to-use instance of `FsmService`.\n *\n * @example\n * ```ts\n * import {createFsmService} from '@alwatr/fsm';\n * import type {StateMachineConfig} from '@alwatr/fsm';\n *\n * // 1. Define types\n * type LightContext = {brightness: number};\n * type LightState = 'on' | 'off';\n * type LightEvent = {type: 'TOGGLE'} | {type: 'SET_BRIGHTNESS'; level: number};\n *\n * // 2. Config the state machine\n * const lightMachineConfig: StateMachineConfig<LightState, LightEvent, LightContext> = {\n * name: 'light-switch',\n * initial: 'off',\n * context: {brightness: 0},\n * states: {\n * off: {\n * on: {\n * TOGGLE: {\n * target: 'on',\n * assigners: [({context}) => ({...context, brightness: 100})],\n * },\n * },\n * },\n * on: {\n * on: {\n * TOGGLE: {target: 'off', assigners: [({context}) => ({...context, brightness: 0})]},\n * SET_BRIGHTNESS: {assigners: [({context, event}) => ({...context, brightness: event.level})]},\n * },\n * },\n * },\n * };\n *\n * // 3. Create the service\n * const lightService = createFsmService(lightMachineConfig);\n *\n * // 4. Use it in your application\n * lightService.stateSignal.subscribe((state) => {\n * console.log(`Light is ${state.name} with brightness ${state.context.brightness}`);\n * });\n *\n * lightService.dispatch({type: 'TOGGLE'}); // Light is on with brightness 100\n *\n * lightService.dispatch({type: 'SET_BRIGHTNESS', level: 50}); // Light is on with brightness 50\n *\n * // 5. Cleanup\n * // lightService.destroy();\n * ```\n */\nexport function createFsmService<\n TState extends string,\n TEvent extends MachineEvent,\n TContext extends Record<string, unknown> = Record<string, never>,\n>(config: StateMachineConfig<TState, TEvent, TContext>): FsmService<TState, TEvent, TContext> {\n return new FsmService(config);\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";AAAA,uBAAQ,uBACR,yBAAQ,sBAER,sCACE,uBACA,uBAiBK,MAAM,CAIX,CAgCqB,QA/BF,QAGH,YAKC,UAAsB,CAAC,EAMhC,aAAe,GAGf,YAAc,GAOL,sBAAwC,CAAC,EAEzC,cAIjB,WAAW,CACU,EACnB,EACA,CAFmB,eAGnB,KAAK,QAAU,EAAa,OAAO,KAAK,QAAQ,MAAM,
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "21C5CE2D42EDADD164756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alwatr/fsm",
|
|
3
|
-
"version": "9.33.
|
|
3
|
+
"version": "9.33.1",
|
|
4
4
|
"description": "A tiny, type-safe, declarative, and reactive finite state machine (FSM) library for modern TypeScript applications, built on top of Alwatr Signals.",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
|
|
@@ -15,36 +15,39 @@
|
|
|
15
15
|
"exports": {
|
|
16
16
|
".": {
|
|
17
17
|
"types": "./dist/main.d.ts",
|
|
18
|
+
"development": "./dist/dev/main.js",
|
|
18
19
|
"import": "./dist/main.js",
|
|
19
20
|
"default": "./dist/main.js"
|
|
20
21
|
}
|
|
21
22
|
},
|
|
22
23
|
"sideEffects": false,
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@alwatr/delay": "9.33.
|
|
25
|
-
"@alwatr/logger": "9.33.
|
|
26
|
-
"@alwatr/signal": "9.33.
|
|
25
|
+
"@alwatr/delay": "9.33.1",
|
|
26
|
+
"@alwatr/logger": "9.33.1",
|
|
27
|
+
"@alwatr/signal": "9.33.1"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
|
-
"@alwatr/nano-build": "9.
|
|
30
|
+
"@alwatr/nano-build": "9.33.1",
|
|
30
31
|
"@alwatr/standard": "9.33.0",
|
|
31
|
-
"@alwatr/type-helper": "9.
|
|
32
|
+
"@alwatr/type-helper": "9.33.1",
|
|
32
33
|
"typescript": "^6.0.3"
|
|
33
34
|
},
|
|
34
35
|
"scripts": {
|
|
35
36
|
"b": "bun run build",
|
|
36
37
|
"build": "bun run build:ts && bun run build:es",
|
|
37
|
-
"build:es": "
|
|
38
|
+
"build:es": "bun run build:es:dev && bun run build:es:prod",
|
|
39
|
+
"build:es:dev": "nano-build --preset=module --outdir=dist/dev src/main.ts",
|
|
40
|
+
"build:es:prod": "NODE_ENV=production nano-build --preset=module --outdir=dist src/main.ts",
|
|
38
41
|
"build:ts": "tsc --build",
|
|
39
42
|
"cl": "bun run clean",
|
|
40
43
|
"clean": "rm -rfv dist *.tsbuildinfo",
|
|
41
44
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
42
45
|
"lint": "eslint src/ --ext .ts",
|
|
43
46
|
"t": "bun run test",
|
|
44
|
-
"test": "
|
|
47
|
+
"test": "bun test",
|
|
45
48
|
"w": "bun run watch",
|
|
46
49
|
"watch": "bun run watch:ts & bun run watch:es",
|
|
47
|
-
"watch:es": "bun run build:es --watch",
|
|
50
|
+
"watch:es": "bun run build:es:dev --watch",
|
|
48
51
|
"watch:ts": "bun run build:ts --watch --preserveWatchOutput"
|
|
49
52
|
},
|
|
50
53
|
"files": [
|
|
@@ -66,5 +69,5 @@
|
|
|
66
69
|
"state",
|
|
67
70
|
"typescript"
|
|
68
71
|
],
|
|
69
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "84fba5b7b428188be17aaaaf062b0b7eaae96555"
|
|
70
73
|
}
|
package/src/fsm-service.ts
CHANGED
|
@@ -60,7 +60,7 @@ export class FsmService<
|
|
|
60
60
|
stateSignal?: StateSignal<MachineState<TState, TContext>> | PersistentStateSignal<MachineState<TState, TContext>>,
|
|
61
61
|
) {
|
|
62
62
|
this.logger_ = createLogger(`fsm:${this.config_.name}`);
|
|
63
|
-
this.logger_.logMethodArgs?.('constructor', config_);
|
|
63
|
+
DEV_MODE && this.logger_.logMethodArgs?.('constructor', config_);
|
|
64
64
|
|
|
65
65
|
const initialValue: MachineState<TState, TContext> = {
|
|
66
66
|
name: config_.initial,
|
|
@@ -114,7 +114,7 @@ export class FsmService<
|
|
|
114
114
|
* @param event The event to process.
|
|
115
115
|
*/
|
|
116
116
|
public readonly dispatch = (event: TEvent): void => {
|
|
117
|
-
this.logger_.logMethodArgs?.('dispatch', {event});
|
|
117
|
+
DEV_MODE && this.logger_.logMethodArgs?.('dispatch', {event});
|
|
118
118
|
this.mailbox__.push(event);
|
|
119
119
|
this.processMailbox__();
|
|
120
120
|
};
|
|
@@ -123,9 +123,9 @@ export class FsmService<
|
|
|
123
123
|
// RTC guard: an active loop is already draining the mailbox; it will pick
|
|
124
124
|
// this event up after the current transition finishes.
|
|
125
125
|
if (this.processing__) return;
|
|
126
|
-
this.logger_.logMethod?.('processMailbox__');
|
|
126
|
+
DEV_MODE && this.logger_.logMethod?.('processMailbox__');
|
|
127
127
|
if (this.destroyed__) {
|
|
128
|
-
this.logger_.incident?.('dispatch', 'dispatch_after_destroy'
|
|
128
|
+
DEV_MODE && this.logger_.incident?.('dispatch', 'dispatch_after_destroy');
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
131
|
this.processing__ = true;
|
|
@@ -149,15 +149,16 @@ export class FsmService<
|
|
|
149
149
|
*/
|
|
150
150
|
private processTransition__(event: TEvent): void {
|
|
151
151
|
const currentState = this.stateSignal__.get();
|
|
152
|
-
this.logger_.logMethodArgs?.('processTransition__', {state: currentState.name, event});
|
|
152
|
+
DEV_MODE && this.logger_.logMethodArgs?.('processTransition__', {state: currentState.name, event});
|
|
153
153
|
|
|
154
154
|
const transition = this.findTransition__(event, currentState);
|
|
155
155
|
|
|
156
156
|
if (!transition) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
DEV_MODE
|
|
158
|
+
&& this.logger_.incident?.('processTransition__', 'ignored_event', 'No valid transition found for event', {
|
|
159
|
+
state: currentState.name,
|
|
160
|
+
event,
|
|
161
|
+
});
|
|
161
162
|
return; // Event ignored, no transition occurs.
|
|
162
163
|
}
|
|
163
164
|
|
|
@@ -198,7 +199,7 @@ export class FsmService<
|
|
|
198
199
|
event: TEvent,
|
|
199
200
|
currentState: MachineState<TState, TContext>,
|
|
200
201
|
): Transition<TState, TEvent, TContext> | undefined {
|
|
201
|
-
this.logger_.logMethod?.('findTransition__');
|
|
202
|
+
DEV_MODE && this.logger_.logMethod?.('findTransition__');
|
|
202
203
|
|
|
203
204
|
const currentStateConfig = this.config_.states[currentState.name];
|
|
204
205
|
const transitions = currentStateConfig?.on?.[event.type as TEvent['type']] as
|
|
@@ -258,11 +259,11 @@ export class FsmService<
|
|
|
258
259
|
effects?: SingleOrArray<Effect<TEvent, TContext>>,
|
|
259
260
|
): void {
|
|
260
261
|
if (!effects) {
|
|
261
|
-
this.logger_.logMethod?.('executeEffects__.skipped');
|
|
262
|
+
DEV_MODE && this.logger_.logMethod?.('executeEffects__.skipped');
|
|
262
263
|
return;
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
this.logger_.logMethod?.('executeEffects__');
|
|
266
|
+
DEV_MODE && this.logger_.logMethod?.('executeEffects__');
|
|
266
267
|
|
|
267
268
|
if (!Array.isArray(effects)) {
|
|
268
269
|
try {
|
|
@@ -309,11 +310,11 @@ export class FsmService<
|
|
|
309
310
|
assigners?: SingleOrArray<Assigner<TEvent, TContext>>,
|
|
310
311
|
): TContext {
|
|
311
312
|
if (!assigners) {
|
|
312
|
-
this.logger_.logMethod?.('applyAssigners__.skipped');
|
|
313
|
+
DEV_MODE && this.logger_.logMethod?.('applyAssigners__.skipped');
|
|
313
314
|
return context;
|
|
314
315
|
}
|
|
315
316
|
|
|
316
|
-
this.logger_.logMethod?.('applyAssigners__');
|
|
317
|
+
DEV_MODE && this.logger_.logMethod?.('applyAssigners__');
|
|
317
318
|
|
|
318
319
|
if (!Array.isArray(assigners)) {
|
|
319
320
|
try {
|
|
@@ -349,7 +350,7 @@ export class FsmService<
|
|
|
349
350
|
*/
|
|
350
351
|
protected start_(): void {
|
|
351
352
|
if (this.destroyed__) return;
|
|
352
|
-
this.logger_.logMethod?.('start_');
|
|
353
|
+
DEV_MODE && this.logger_.logMethod?.('start_');
|
|
353
354
|
const currentState = this.stateSignal__.get();
|
|
354
355
|
const initEvent = {type: '__init__'} as unknown as TEvent;
|
|
355
356
|
this.executeEffects__(initEvent, currentState.context, this.config_.states[currentState.name]?.entry);
|
|
@@ -369,11 +370,11 @@ export class FsmService<
|
|
|
369
370
|
actors?: SingleOrArray<Actor<TEvent, TContext>>,
|
|
370
371
|
): void {
|
|
371
372
|
if (!actors) {
|
|
372
|
-
this.logger_.logMethod?.('spawnActors__.skipped');
|
|
373
|
+
DEV_MODE && this.logger_.logMethod?.('spawnActors__.skipped');
|
|
373
374
|
return;
|
|
374
375
|
}
|
|
375
376
|
|
|
376
|
-
this.logger_.logMethod?.('spawnActors__');
|
|
377
|
+
DEV_MODE && this.logger_.logMethod?.('spawnActors__');
|
|
377
378
|
|
|
378
379
|
if (!Array.isArray(actors)) {
|
|
379
380
|
try {
|
|
@@ -406,7 +407,7 @@ export class FsmService<
|
|
|
406
407
|
* Cleans up (destroys) all currently active state actors in REVERSE (LIFO) spawn order — standard resource-release semantics.
|
|
407
408
|
*/
|
|
408
409
|
private cleanupActors__(): void {
|
|
409
|
-
this.logger_.logMethodArgs?.('cleanupActors__', {count: this.activeActorCleanups__.length});
|
|
410
|
+
DEV_MODE && this.logger_.logMethodArgs?.('cleanupActors__', {count: this.activeActorCleanups__.length});
|
|
410
411
|
for (let index = this.activeActorCleanups__.length - 1; index >= 0; index--) {
|
|
411
412
|
try {
|
|
412
413
|
this.activeActorCleanups__[index]();
|
|
@@ -425,7 +426,7 @@ export class FsmService<
|
|
|
425
426
|
*/
|
|
426
427
|
public destroy(destroyState = true): void {
|
|
427
428
|
if (this.destroyed__) return;
|
|
428
|
-
this.logger_.logMethod?.('destroy');
|
|
429
|
+
DEV_MODE && this.logger_.logMethod?.('destroy');
|
|
429
430
|
this.destroyed__ = true;
|
|
430
431
|
this.mailbox__.length = 0;
|
|
431
432
|
this.cleanupActors__();
|