@alwatr/fsm 9.13.0 → 9.16.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/facade.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { JsonObject } from '@alwatr/type-helper';
1
2
  import { FsmService } from './fsm-service.js';
2
3
  import type { MachineEvent, StateMachineConfig } from './type.js';
3
4
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"facade.d.ts","sourceRoot":"","sources":["../src/facade.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAE5C,OAAO,KAAK,EAAC,YAAY,EAAgB,kBAAkB,EAAC,MAAM,WAAW,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,SAAS,YAAY,EAAE,QAAQ,SAAS,UAAU,EAC9G,MAAM,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,GACnD,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAmBtC"}
1
+ {"version":3,"file":"facade.d.ts","sourceRoot":"","sources":["../src/facade.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,qBAAqB,CAAC;AAGpD,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAE5C,OAAO,KAAK,EAAC,YAAY,EAAgB,kBAAkB,EAAC,MAAM,WAAW,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,SAAS,YAAY,EAAE,QAAQ,SAAS,UAAU,EAC9G,MAAM,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,GACnD,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAoBtC"}
@@ -1,3 +1,4 @@
1
+ import type { JsonObject } from '@alwatr/type-helper';
1
2
  import { type AlwatrLogger } from '@alwatr/logger';
2
3
  import { type StateSignal, type PersistentStateSignal, EventSignal, type IReadonlySignal } from '@alwatr/signal';
3
4
  import type { StateMachineConfig, MachineState, MachineEvent } from './type.js';
@@ -1 +1 @@
1
- {"version":3,"file":"fsm-service.d.ts","sourceRoot":"","sources":["../src/fsm-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EAAqB,KAAK,WAAW,EAAE,KAAK,qBAAqB,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEpI,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,YAAY,EAAgC,MAAM,WAAW,CAAC;AAE9G;;;;;;;;GAQG;AACH,qBAAa,UAAU,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,SAAS,YAAY,EAAE,QAAQ,SAAS,UAAU;IAUnG,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC;IACxE,OAAO,CAAC,QAAQ,CAAC,aAAa;IAVhC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAEzC,sDAAsD;IACtD,SAAgB,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAEjD,+EAA+E;IAC/E,SAAgB,WAAW,EAAE,eAAe,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;gBAGxD,OAAO,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EACvD,aAAa,EAAE,WAAW,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,GAAG,qBAAqB,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAYrI;;;;;OAKG;YACW,mBAAmB;IAuCjC;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAwCxB;;;;;;;OAOG;YACW,gBAAgB;IAqC9B;;;;;;;;;OASG;IACH,OAAO,CAAC,gBAAgB;IAkCxB;;;OAGG;IACI,OAAO,IAAI,IAAI;CAKvB"}
1
+ {"version":3,"file":"fsm-service.d.ts","sourceRoot":"","sources":["../src/fsm-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAgB,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAe,KAAK,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,WAAW,EACX,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAC,kBAAkB,EAAE,YAAY,EAAE,YAAY,EAA+B,MAAM,WAAW,CAAC;AAE5G;;;;;;;;GAQG;AACH,qBAAa,UAAU,CAAC,MAAM,SAAS,MAAM,EAAE,MAAM,SAAS,YAAY,EAAE,QAAQ,SAAS,UAAU;IAUnG,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC;IACxE,OAAO,CAAC,QAAQ,CAAC,aAAa;IAVhC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAEzC,sDAAsD;IACtD,SAAgB,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAEjD,+EAA+E;IAC/E,SAAgB,WAAW,EAAE,eAAe,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;gBAGxD,OAAO,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EACvD,aAAa,EAC1B,WAAW,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,GAC3C,qBAAqB,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAY3D;;;;;OAKG;YACW,mBAAmB;IAuCjC;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IA0CxB;;;;;;;OAOG;YACW,gBAAgB;IAoC9B;;;;;;;;;OASG;IACH,OAAO,CAAC,gBAAgB;IAyCxB;;;OAGG;IACI,OAAO,IAAI,IAAI;CAKvB"}
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
- /* 📦 @alwatr/fsm v9.13.0 */
1
+ /* 📦 @alwatr/fsm v9.16.0 */
2
2
  import{createPersistentStateSignal as D,createStateSignal as V}from"@alwatr/signal";import{createLogger as k}from"@alwatr/logger";import{createEventSignal as P}from"@alwatr/signal";class Z{config_;stateSignal__;logger_;eventSignal;stateSignal;constructor(q,z){this.config_=q;this.stateSignal__=z;this.logger_=k(`fsm:${this.config_.name}`),this.logger_.logMethodArgs?.("constructor",q),this.stateSignal=this.stateSignal__.asReadonly(),this.eventSignal=P({name:`fsm-event-${this.config_.name}`}),this.eventSignal.subscribe(this.processTransition__.bind(this),{receivePrevious:!1})}async processTransition__(q){let z=this.stateSignal__.get();this.logger_.logMethodArgs?.("processTransition__",{state:z.name,event:q});let J=this.findTransition__(q,z.context);if(!J){this.logger_.incident?.("processTransition__","ignored_event","No valid transition found for event",{state:z.name,event:q});return}let W=J.target??z.name;if(W!==z.name)this.executeEffects__(q,z.context,this.config_.states[z.name]?.exit);let K=this.applyAssigners__(q,z.context,J.assigners),Q={name:W,context:K};if(this.stateSignal__.set(Q),Q.name!==z.name)this.executeEffects__(q,Q.context,this.config_.states[Q.name]?.entry)}findTransition__(q,z){this.logger_.logMethod?.("findTransition__");let J=this.stateSignal__.get().name,K=this.config_.states[J]?.on?.[q.type];if(!K)return;return(Array.isArray(K)?K:[K]).find((X,j)=>{if(!X.condition)return!0;try{let Y=X.condition(q,z);return this.logger_.logStep?.("findTransition__","check_condition",{state:J,eventType:q.type,transitionIndex:j,condition:X.condition.name||"anonymous",result:Y}),Y}catch(Y){return this.logger_.error("findTransition__","condition_failed",Y,{state:J,eventType:q.type,transitionIndex:j,condition:X.condition.name||"anonymous"}),!1}})}async executeEffects__(q,z,J){if(!J){this.logger_.logMethodArgs?.("executeEffects__//skipped",{count:0});return}let W=Array.isArray(J)?J:[J];this.logger_.logMethodArgs?.("executeEffects__",{count:W.length});for(let K of W)try{let Q=await K(q,z);if(Q&&"type"in Q)this.logger_.logStep?.("executeEffects__","new_event_from_effect",{effect:K.name||"anonymous",state:this.stateSignal__.get().name,newEvent:Q.type}),this.eventSignal.dispatch(Q)}catch(Q){this.logger_.error("executeEffects__","effect_failed",Q,{effect:K.name||"anonymous",state:this.stateSignal__.get().name,event:q,context:z})}}applyAssigners__(q,z,J){if(!J)return this.logger_.logMethodArgs?.("applyAssigners__//skipped",{count:0}),z;let W=Array.isArray(J)?J:[J];this.logger_.logMethodArgs?.("applyAssigners__",{count:W.length});try{return W.reduce((K,Q)=>{let X=Q(q,K);if(this.logger_.logMethodFull?.(`event.${q.type}.action.${Q.name||"anonymous"}`,{event:q,accContext:K},X),typeof X==="object"&&X!==null)return{...K,...X};return K},z)}catch(K){return this.logger_.error("applyAssigners__","assigner_failed_atomic",K,{event:q,context:z}),z}}destroy(){this.logger_.logMethod?.("destroy"),this.eventSignal.destroy(),this.stateSignal.destroy()}}function F(q){let z={name:q.initial,context:q.context},J=q.persistent?D({name:`fsm-state-${q.name}`,storageKey:q.persistent.storageKey??q.name,initialValue:z,schemaVersion:q.persistent.schemaVersion}):V({name:`fsm-state-${q.name}`,initialValue:z});return new Z(q,J)}export{F as createFsmService,Z as FsmService};
3
3
 
4
- //# debugId=1738FDF620FFD65864756E2164756E21
4
+ //# debugId=F288BF6145D20DC764756E2164756E21
5
5
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -2,10 +2,10 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/facade.ts", "../src/fsm-service.ts"],
4
4
  "sourcesContent": [
5
- "import {createPersistentStateSignal, createStateSignal} from '@alwatr/signal';\n\nimport {FsmService} from './fsm-service.js';\n\nimport type {MachineEvent, MachineState, 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: [() => ({brightness: 100})],\n * },\n * },\n * },\n * on: {\n * on: {\n * TOGGLE: {target: 'off', assigners: [() => ({brightness: 0})]},\n * SET_BRIGHTNESS: {assigners: [(event) => ({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.eventSignal.dispatch({type: 'TOGGLE'}); // Light is on with brightness 100\n *\n * lightService.eventSignal.dispatch({type: 'SET_BRIGHTNESS', level: 50}); // Light is on with brightness 50\n *\n * // 5. Cleanup\n * // lightService.destroy();\n * ```\n */\nexport function createFsmService<TState extends string, TEvent extends MachineEvent, TContext extends JsonObject>(\n config: StateMachineConfig<TState, TEvent, TContext>,\n): FsmService<TState, TEvent, TContext> {\n const initialValue: MachineState<TState, TContext> = {\n name: config.initial,\n context: config.context,\n };\n\n const stateSignal = 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: initialValue,\n });\n\n return new FsmService(config, stateSignal);\n}\n",
6
- "import { createLogger, type AlwatrLogger } from '@alwatr/logger';\nimport { createEventSignal, type StateSignal, type PersistentStateSignal, EventSignal, type IReadonlySignal } from '@alwatr/signal';\n\nimport type { StateMachineConfig, MachineState, MachineEvent, Transition, Effect, Assigner } 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<TState extends string, TEvent extends MachineEvent, TContext extends JsonObject> {\n protected readonly logger_: AlwatrLogger;\n\n /** The event signal for sending events to the FSM. */\n public readonly eventSignal: EventSignal<TEvent>;\n\n /** The public, read-only state signal. Subscribe to react to state changes. */\n public readonly stateSignal: IReadonlySignal<MachineState<TState, TContext>>;\n\n constructor(\n protected readonly config_: StateMachineConfig<TState, TEvent, TContext>,\n private readonly 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 this.stateSignal = this.stateSignal__.asReadonly();\n this.eventSignal = createEventSignal<TEvent>({\n name: `fsm-event-${this.config_.name}`,\n });\n this.eventSignal.subscribe(this.processTransition__.bind(this), { receivePrevious: false });\n }\n\n /**\n * The core FSM logic that processes a single event and transitions the machine to a new state.\n * This process is atomic and follows the Run-to-Completion (RTC) model.\n *\n * @param event The event to process.\n */\n private async processTransition__(event: TEvent): Promise<void> {\n const currentState = this.stateSignal__.get();\n this.logger_.logMethodArgs?.('processTransition__', { state: currentState.name, event });\n\n const transition = this.findTransition__(event, currentState.context);\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\n // 1. Execute exit effects of the current state if transitioning to a new state.\n if (targetStateName !== currentState.name) {\n void this.executeEffects__(event, currentState.context, this.config_.states[currentState.name]?.exit);\n }\n\n // 2. Apply assigners to compute the next context. This is a pure function.\n const nextContext = this.applyAssigners__(event, currentState.context, transition.assigners);\n\n // 3. Create the final next state object.\n const nextState: MachineState<TState, TContext> = {\n name: targetStateName,\n context: nextContext,\n };\n\n // 4. Set the new state, notifying all subscribers.\n this.stateSignal__.set(nextState);\n\n // 5. Execute entry effects of the new state if a transition occurred.\n if (nextState.name !== currentState.name) {\n void this.executeEffects__(event, nextState.context, this.config_.states[nextState.name]?.entry);\n }\n }\n\n /**\n * Finds the first valid transition for the given event and context by evaluating conditions.\n *\n * @param event The triggering event.\n * @param context The current machine context.\n * @returns The first matching transition or `undefined` if none are found.\n */\n private findTransition__(event: TEvent, context: Readonly<TContext>): Transition<TState, TEvent, TContext> | undefined {\n this.logger_.logMethod?.('findTransition__');\n\n const currentStateName = this.stateSignal__.get().name;\n const currentStateConfig = this.config_.states[currentStateName];\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 // Normalize to an array to handle both single and multiple transitions uniformly.\n const transitionsArray = Array.isArray(transitions) ? transitions : [transitions];\n\n return transitionsArray.find((transition, index) => {\n if (!transition.condition) return true; // A transition without a condition is always valid.\n\n try {\n const conditionMet = transition.condition(event, context);\n this.logger_.logStep?.('findTransition__', 'check_condition', {\n state: currentStateName,\n eventType: event.type,\n transitionIndex: index,\n condition: transition.condition.name || 'anonymous',\n result: conditionMet,\n });\n return conditionMet;\n }\n catch (error) {\n this.logger_.error('findTransition__', 'condition_failed', error, {\n state: currentStateName,\n eventType: event.type,\n transitionIndex: index,\n condition: transition.condition.name || 'anonymous',\n });\n return false; // Treat a failing condition as not met.\n }\n });\n }\n\n /**\n * Sequentially executes a list of 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 async executeEffects__(\n event: TEvent,\n context: Readonly<TContext>,\n effects?: SingleOrArray<Effect<TEvent, TContext>>,\n ): Promise<void> {\n if (!effects) {\n this.logger_.logMethodArgs?.('executeEffects__//skipped', { count: 0 });\n return;\n }\n const effectsArray: Effect<TEvent, TContext>[] = Array.isArray(effects) ? effects : [effects];\n\n this.logger_.logMethodArgs?.('executeEffects__', { count: effectsArray.length });\n\n for (const effect of effectsArray) {\n try {\n const result = await effect(event, context);\n // If an effect returns a new event, dispatch it to be processed next.\n if (result && 'type' in result) {\n this.logger_.logStep?.('executeEffects__', 'new_event_from_effect', {\n effect: effect.name || 'anonymous',\n state: this.stateSignal__.get().name,\n newEvent: result.type,\n });\n this.eventSignal.dispatch(result);\n }\n }\n catch (error) {\n this.logger_.error('executeEffects__', 'effect_failed', error, {\n effect: effect.name || 'anonymous',\n state: this.stateSignal__.get().name,\n event,\n context,\n });\n }\n }\n }\n\n /**\n * Applies all assigner functions to the context to produce a new, updated context.\n * This process is atomic (all-or-nothing). If any assigner fails, 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__(event: TEvent, context: Readonly<TContext>, assigners?: SingleOrArray<Assigner<TEvent, TContext>>): TContext {\n if (!assigners) {\n this.logger_.logMethodArgs?.('applyAssigners__//skipped', { count: 0 });\n return context;\n }\n\n const assignersArray: Assigner<TEvent, TContext>[] = Array.isArray(assigners) ? assigners : [assigners];\n\n this.logger_.logMethodArgs?.('applyAssigners__', { count: assignersArray.length });\n\n try {\n // The entire reduce operation is wrapped in a single try/catch block\n // to ensure atomic updates.\n return assignersArray.reduce((accContext, assigner) => {\n const partialUpdate = assigner(event, accContext);\n this.logger_.logMethodFull?.(`event.${event.type}.action.${assigner.name || 'anonymous'}`, { event, accContext }, partialUpdate);\n if (typeof partialUpdate === 'object' && partialUpdate !== null) {\n // The next assigner receives the updated context from the previous one.\n return { ...accContext, ...partialUpdate };\n }\n // If an assigner returns nothing, pass the accumulated context along.\n return accContext;\n }, context);\n }\n catch (error) {\n this.logger_.error('applyAssigners__', 'assigner_failed_atomic', error, {\n event,\n context, // Log the original context for debugging.\n });\n // On ANY failure, discard all changes and return the original context.\n return context;\n }\n }\n\n /**\n * Destroys the service, cleaning up all internal signals and subscriptions\n * to prevent memory leaks.\n */\n public destroy(): void {\n this.logger_.logMethod?.('destroy');\n this.eventSignal.destroy();\n this.stateSignal.destroy();\n }\n}\n"
5
+ "import type {JsonObject} from '@alwatr/type-helper';\nimport {createPersistentStateSignal, createStateSignal} from '@alwatr/signal';\n\nimport {FsmService} from './fsm-service.js';\n\nimport type {MachineEvent, MachineState, 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: [() => ({brightness: 100})],\n * },\n * },\n * },\n * on: {\n * on: {\n * TOGGLE: {target: 'off', assigners: [() => ({brightness: 0})]},\n * SET_BRIGHTNESS: {assigners: [(event) => ({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.eventSignal.dispatch({type: 'TOGGLE'}); // Light is on with brightness 100\n *\n * lightService.eventSignal.dispatch({type: 'SET_BRIGHTNESS', level: 50}); // Light is on with brightness 50\n *\n * // 5. Cleanup\n * // lightService.destroy();\n * ```\n */\nexport function createFsmService<TState extends string, TEvent extends MachineEvent, TContext extends JsonObject>(\n config: StateMachineConfig<TState, TEvent, TContext>,\n): FsmService<TState, TEvent, TContext> {\n const initialValue: MachineState<TState, TContext> = {\n name: config.initial,\n context: config.context,\n };\n\n const 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: initialValue,\n });\n\n return new FsmService(config, stateSignal);\n}\n",
6
+ "import type {JsonObject, SingleOrArray} from '@alwatr/type-helper';\nimport {createLogger, type AlwatrLogger} from '@alwatr/logger';\nimport {\n createEventSignal,\n type StateSignal,\n type PersistentStateSignal,\n EventSignal,\n type IReadonlySignal,\n} from '@alwatr/signal';\n\nimport type {StateMachineConfig, MachineState, MachineEvent, Transition, Effect, Assigner} 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<TState extends string, TEvent extends MachineEvent, TContext extends JsonObject> {\n protected readonly logger_: AlwatrLogger;\n\n /** The event signal for sending events to the FSM. */\n public readonly eventSignal: EventSignal<TEvent>;\n\n /** The public, read-only state signal. Subscribe to react to state changes. */\n public readonly stateSignal: IReadonlySignal<MachineState<TState, TContext>>;\n\n constructor(\n protected readonly config_: StateMachineConfig<TState, TEvent, TContext>,\n private readonly stateSignal__:\n | StateSignal<MachineState<TState, TContext>>\n | PersistentStateSignal<MachineState<TState, TContext>>,\n ) {\n this.logger_ = createLogger(`fsm:${this.config_.name}`);\n this.logger_.logMethodArgs?.('constructor', config_);\n\n this.stateSignal = this.stateSignal__.asReadonly();\n this.eventSignal = createEventSignal<TEvent>({\n name: `fsm-event-${this.config_.name}`,\n });\n this.eventSignal.subscribe(this.processTransition__.bind(this), {receivePrevious: false});\n }\n\n /**\n * The core FSM logic that processes a single event and transitions the machine to a new state.\n * This process is atomic and follows the Run-to-Completion (RTC) model.\n *\n * @param event The event to process.\n */\n private async processTransition__(event: TEvent): Promise<void> {\n const currentState = this.stateSignal__.get();\n this.logger_.logMethodArgs?.('processTransition__', {state: currentState.name, event});\n\n const transition = this.findTransition__(event, currentState.context);\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\n // 1. Execute exit effects of the current state if transitioning to a new state.\n if (targetStateName !== currentState.name) {\n void this.executeEffects__(event, currentState.context, this.config_.states[currentState.name]?.exit);\n }\n\n // 2. Apply assigners to compute the next context. This is a pure function.\n const nextContext = this.applyAssigners__(event, currentState.context, transition.assigners);\n\n // 3. Create the final next state object.\n const nextState: MachineState<TState, TContext> = {\n name: targetStateName,\n context: nextContext,\n };\n\n // 4. Set the new state, notifying all subscribers.\n this.stateSignal__.set(nextState);\n\n // 5. Execute entry effects of the new state if a transition occurred.\n if (nextState.name !== currentState.name) {\n void this.executeEffects__(event, nextState.context, this.config_.states[nextState.name]?.entry);\n }\n }\n\n /**\n * Finds the first valid transition for the given event and context by evaluating conditions.\n *\n * @param event The triggering event.\n * @param context The current machine context.\n * @returns The first matching transition or `undefined` if none are found.\n */\n private findTransition__(\n event: TEvent,\n context: Readonly<TContext>,\n ): Transition<TState, TEvent, TContext> | undefined {\n this.logger_.logMethod?.('findTransition__');\n\n const currentStateName = this.stateSignal__.get().name;\n const currentStateConfig = this.config_.states[currentStateName];\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 // Normalize to an array to handle both single and multiple transitions uniformly.\n const transitionsArray = Array.isArray(transitions) ? transitions : [transitions];\n\n return transitionsArray.find((transition, index) => {\n if (!transition.condition) return true; // A transition without a condition is always valid.\n\n try {\n const conditionMet = transition.condition(event, context);\n this.logger_.logStep?.('findTransition__', 'check_condition', {\n state: currentStateName,\n eventType: event.type,\n transitionIndex: index,\n condition: transition.condition.name || 'anonymous',\n result: conditionMet,\n });\n return conditionMet;\n } catch (error) {\n this.logger_.error('findTransition__', 'condition_failed', error, {\n state: currentStateName,\n eventType: event.type,\n transitionIndex: index,\n condition: transition.condition.name || 'anonymous',\n });\n return false; // Treat a failing condition as not met.\n }\n });\n }\n\n /**\n * Sequentially executes a list of 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 async executeEffects__(\n event: TEvent,\n context: Readonly<TContext>,\n effects?: SingleOrArray<Effect<TEvent, TContext>>,\n ): Promise<void> {\n if (!effects) {\n this.logger_.logMethodArgs?.('executeEffects__//skipped', {count: 0});\n return;\n }\n const effectsArray: Effect<TEvent, TContext>[] = Array.isArray(effects) ? effects : [effects];\n\n this.logger_.logMethodArgs?.('executeEffects__', {count: effectsArray.length});\n\n for (const effect of effectsArray) {\n try {\n const result = await effect(event, context);\n // If an effect returns a new event, dispatch it to be processed next.\n if (result && 'type' in result) {\n this.logger_.logStep?.('executeEffects__', 'new_event_from_effect', {\n effect: effect.name || 'anonymous',\n state: this.stateSignal__.get().name,\n newEvent: result.type,\n });\n this.eventSignal.dispatch(result);\n }\n } catch (error) {\n this.logger_.error('executeEffects__', 'effect_failed', error, {\n effect: effect.name || 'anonymous',\n state: this.stateSignal__.get().name,\n event,\n context,\n });\n }\n }\n }\n\n /**\n * Applies all assigner functions to the context to produce a new, updated context.\n * This process is atomic (all-or-nothing). If any assigner fails, 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: Readonly<TContext>,\n assigners?: SingleOrArray<Assigner<TEvent, TContext>>,\n ): TContext {\n if (!assigners) {\n this.logger_.logMethodArgs?.('applyAssigners__//skipped', {count: 0});\n return context;\n }\n\n const assignersArray: Assigner<TEvent, TContext>[] = Array.isArray(assigners) ? assigners : [assigners];\n\n this.logger_.logMethodArgs?.('applyAssigners__', {count: assignersArray.length});\n\n try {\n // The entire reduce operation is wrapped in a single try/catch block\n // to ensure atomic updates.\n return assignersArray.reduce((accContext, assigner) => {\n const partialUpdate = assigner(event, accContext);\n this.logger_.logMethodFull?.(\n `event.${event.type}.action.${assigner.name || 'anonymous'}`,\n {event, accContext},\n partialUpdate,\n );\n if (typeof partialUpdate === 'object' && partialUpdate !== null) {\n // The next assigner receives the updated context from the previous one.\n return {...accContext, ...partialUpdate};\n }\n // If an assigner returns nothing, pass the accumulated context along.\n return accContext;\n }, context);\n } catch (error) {\n this.logger_.error('applyAssigners__', 'assigner_failed_atomic', error, {\n event,\n context, // Log the original context for debugging.\n });\n // On ANY failure, discard all changes and return the original context.\n return context;\n }\n }\n\n /**\n * Destroys the service, cleaning up all internal signals and subscriptions\n * to prevent memory leaks.\n */\n public destroy(): void {\n this.logger_.logMethod?.('destroy');\n this.eventSignal.destroy();\n this.stateSignal.destroy();\n }\n}\n"
7
7
  ],
8
- "mappings": "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",
9
- "debugId": "1738FDF620FFD65864756E2164756E21",
8
+ "mappings": "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",
9
+ "debugId": "F288BF6145D20DC764756E2164756E21",
10
10
  "names": []
11
11
  }
package/dist/type.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Awaitable, JsonObject, SingleOrArray } from '@alwatr/type-helper';
1
2
  import type { SignalConfig } from '@alwatr/signal';
2
3
  /**
3
4
  * Represents the state of a state machine, including its current finite state value
@@ -1 +1 @@
1
- {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alwatr/fsm",
3
- "version": "9.13.0",
3
+ "version": "9.16.0",
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)",
@@ -21,13 +21,13 @@
21
21
  },
22
22
  "sideEffects": false,
23
23
  "dependencies": {
24
- "@alwatr/logger": "9.11.2",
25
- "@alwatr/signal": "9.13.0"
24
+ "@alwatr/logger": "9.16.0",
25
+ "@alwatr/signal": "9.16.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@alwatr/nano-build": "9.10.1",
29
- "@alwatr/standard": "9.11.2",
30
- "@alwatr/type-helper": "9.11.2",
28
+ "@alwatr/nano-build": "9.14.0",
29
+ "@alwatr/standard": "9.16.0",
30
+ "@alwatr/type-helper": "9.14.0",
31
31
  "typescript": "^6.0.3"
32
32
  },
33
33
  "scripts": {
@@ -65,5 +65,5 @@
65
65
  "state",
66
66
  "typescript"
67
67
  ],
68
- "gitHead": "da284d23e3173d589fa69376e51a098c5e89649d"
68
+ "gitHead": "c210044f6e8ab444ec2f9e600f095761cbd279bd"
69
69
  }
package/src/facade.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type {JsonObject} from '@alwatr/type-helper';
1
2
  import {createPersistentStateSignal, createStateSignal} from '@alwatr/signal';
2
3
 
3
4
  import {FsmService} from './fsm-service.js';
@@ -72,17 +73,18 @@ export function createFsmService<TState extends string, TEvent extends MachineEv
72
73
  context: config.context,
73
74
  };
74
75
 
75
- const stateSignal = config.persistent
76
- ? createPersistentStateSignal<MachineState<TState, TContext>>({
77
- name: `fsm-state-${config.name}`,
78
- storageKey: config.persistent.storageKey ?? config.name,
79
- initialValue,
80
- schemaVersion: config.persistent.schemaVersion,
81
- })
76
+ const stateSignal =
77
+ config.persistent ?
78
+ createPersistentStateSignal<MachineState<TState, TContext>>({
79
+ name: `fsm-state-${config.name}`,
80
+ storageKey: config.persistent.storageKey ?? config.name,
81
+ initialValue,
82
+ schemaVersion: config.persistent.schemaVersion,
83
+ })
82
84
  : createStateSignal<MachineState<TState, TContext>>({
83
- name: `fsm-state-${config.name}`,
84
- initialValue: initialValue,
85
- });
85
+ name: `fsm-state-${config.name}`,
86
+ initialValue: initialValue,
87
+ });
86
88
 
87
89
  return new FsmService(config, stateSignal);
88
90
  }
@@ -1,7 +1,14 @@
1
- import { createLogger, type AlwatrLogger } from '@alwatr/logger';
2
- import { createEventSignal, type StateSignal, type PersistentStateSignal, EventSignal, type IReadonlySignal } from '@alwatr/signal';
3
-
4
- import type { StateMachineConfig, MachineState, MachineEvent, Transition, Effect, Assigner } from './type.js';
1
+ import type {JsonObject, SingleOrArray} from '@alwatr/type-helper';
2
+ import {createLogger, type AlwatrLogger} from '@alwatr/logger';
3
+ import {
4
+ createEventSignal,
5
+ type StateSignal,
6
+ type PersistentStateSignal,
7
+ EventSignal,
8
+ type IReadonlySignal,
9
+ } from '@alwatr/signal';
10
+
11
+ import type {StateMachineConfig, MachineState, MachineEvent, Transition, Effect, Assigner} from './type.js';
5
12
 
6
13
  /**
7
14
  * A generic, encapsulated service that creates, runs, and manages a finite state machine.
@@ -23,7 +30,9 @@ export class FsmService<TState extends string, TEvent extends MachineEvent, TCon
23
30
 
24
31
  constructor(
25
32
  protected readonly config_: StateMachineConfig<TState, TEvent, TContext>,
26
- private readonly stateSignal__: StateSignal<MachineState<TState, TContext>> | PersistentStateSignal<MachineState<TState, TContext>>,
33
+ private readonly stateSignal__:
34
+ | StateSignal<MachineState<TState, TContext>>
35
+ | PersistentStateSignal<MachineState<TState, TContext>>,
27
36
  ) {
28
37
  this.logger_ = createLogger(`fsm:${this.config_.name}`);
29
38
  this.logger_.logMethodArgs?.('constructor', config_);
@@ -32,7 +41,7 @@ export class FsmService<TState extends string, TEvent extends MachineEvent, TCon
32
41
  this.eventSignal = createEventSignal<TEvent>({
33
42
  name: `fsm-event-${this.config_.name}`,
34
43
  });
35
- this.eventSignal.subscribe(this.processTransition__.bind(this), { receivePrevious: false });
44
+ this.eventSignal.subscribe(this.processTransition__.bind(this), {receivePrevious: false});
36
45
  }
37
46
 
38
47
  /**
@@ -43,7 +52,7 @@ export class FsmService<TState extends string, TEvent extends MachineEvent, TCon
43
52
  */
44
53
  private async processTransition__(event: TEvent): Promise<void> {
45
54
  const currentState = this.stateSignal__.get();
46
- this.logger_.logMethodArgs?.('processTransition__', { state: currentState.name, event });
55
+ this.logger_.logMethodArgs?.('processTransition__', {state: currentState.name, event});
47
56
 
48
57
  const transition = this.findTransition__(event, currentState.context);
49
58
 
@@ -87,7 +96,10 @@ export class FsmService<TState extends string, TEvent extends MachineEvent, TCon
87
96
  * @param context The current machine context.
88
97
  * @returns The first matching transition or `undefined` if none are found.
89
98
  */
90
- private findTransition__(event: TEvent, context: Readonly<TContext>): Transition<TState, TEvent, TContext> | undefined {
99
+ private findTransition__(
100
+ event: TEvent,
101
+ context: Readonly<TContext>,
102
+ ): Transition<TState, TEvent, TContext> | undefined {
91
103
  this.logger_.logMethod?.('findTransition__');
92
104
 
93
105
  const currentStateName = this.stateSignal__.get().name;
@@ -114,8 +126,7 @@ export class FsmService<TState extends string, TEvent extends MachineEvent, TCon
114
126
  result: conditionMet,
115
127
  });
116
128
  return conditionMet;
117
- }
118
- catch (error) {
129
+ } catch (error) {
119
130
  this.logger_.error('findTransition__', 'condition_failed', error, {
120
131
  state: currentStateName,
121
132
  eventType: event.type,
@@ -141,12 +152,12 @@ export class FsmService<TState extends string, TEvent extends MachineEvent, TCon
141
152
  effects?: SingleOrArray<Effect<TEvent, TContext>>,
142
153
  ): Promise<void> {
143
154
  if (!effects) {
144
- this.logger_.logMethodArgs?.('executeEffects__//skipped', { count: 0 });
155
+ this.logger_.logMethodArgs?.('executeEffects__//skipped', {count: 0});
145
156
  return;
146
157
  }
147
158
  const effectsArray: Effect<TEvent, TContext>[] = Array.isArray(effects) ? effects : [effects];
148
159
 
149
- this.logger_.logMethodArgs?.('executeEffects__', { count: effectsArray.length });
160
+ this.logger_.logMethodArgs?.('executeEffects__', {count: effectsArray.length});
150
161
 
151
162
  for (const effect of effectsArray) {
152
163
  try {
@@ -160,8 +171,7 @@ export class FsmService<TState extends string, TEvent extends MachineEvent, TCon
160
171
  });
161
172
  this.eventSignal.dispatch(result);
162
173
  }
163
- }
164
- catch (error) {
174
+ } catch (error) {
165
175
  this.logger_.error('executeEffects__', 'effect_failed', error, {
166
176
  effect: effect.name || 'anonymous',
167
177
  state: this.stateSignal__.get().name,
@@ -182,31 +192,38 @@ export class FsmService<TState extends string, TEvent extends MachineEvent, TCon
182
192
  * @param assigners A single assigner or an array of assigners.
183
193
  * @returns The new, updated context, or the original context if any assigner fails.
184
194
  */
185
- private applyAssigners__(event: TEvent, context: Readonly<TContext>, assigners?: SingleOrArray<Assigner<TEvent, TContext>>): TContext {
195
+ private applyAssigners__(
196
+ event: TEvent,
197
+ context: Readonly<TContext>,
198
+ assigners?: SingleOrArray<Assigner<TEvent, TContext>>,
199
+ ): TContext {
186
200
  if (!assigners) {
187
- this.logger_.logMethodArgs?.('applyAssigners__//skipped', { count: 0 });
201
+ this.logger_.logMethodArgs?.('applyAssigners__//skipped', {count: 0});
188
202
  return context;
189
203
  }
190
204
 
191
205
  const assignersArray: Assigner<TEvent, TContext>[] = Array.isArray(assigners) ? assigners : [assigners];
192
206
 
193
- this.logger_.logMethodArgs?.('applyAssigners__', { count: assignersArray.length });
207
+ this.logger_.logMethodArgs?.('applyAssigners__', {count: assignersArray.length});
194
208
 
195
209
  try {
196
210
  // The entire reduce operation is wrapped in a single try/catch block
197
211
  // to ensure atomic updates.
198
212
  return assignersArray.reduce((accContext, assigner) => {
199
213
  const partialUpdate = assigner(event, accContext);
200
- this.logger_.logMethodFull?.(`event.${event.type}.action.${assigner.name || 'anonymous'}`, { event, accContext }, partialUpdate);
214
+ this.logger_.logMethodFull?.(
215
+ `event.${event.type}.action.${assigner.name || 'anonymous'}`,
216
+ {event, accContext},
217
+ partialUpdate,
218
+ );
201
219
  if (typeof partialUpdate === 'object' && partialUpdate !== null) {
202
220
  // The next assigner receives the updated context from the previous one.
203
- return { ...accContext, ...partialUpdate };
221
+ return {...accContext, ...partialUpdate};
204
222
  }
205
223
  // If an assigner returns nothing, pass the accumulated context along.
206
224
  return accContext;
207
225
  }, context);
208
- }
209
- catch (error) {
226
+ } catch (error) {
210
227
  this.logger_.error('applyAssigners__', 'assigner_failed_atomic', error, {
211
228
  event,
212
229
  context, // Log the original context for debugging.
package/src/type.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type {Awaitable, JsonObject, SingleOrArray} from '@alwatr/type-helper';
1
2
  import type {SignalConfig} from '@alwatr/signal';
2
3
 
3
4
  /**
@@ -110,8 +111,11 @@ export interface FsmPersistenceConfig {
110
111
  * @template TEvent The union type of all possible events.
111
112
  * @template TContext The type of the context object.
112
113
  */
113
- export interface StateMachineConfig<TState extends string, TEvent extends MachineEvent, TContext extends JsonObject>
114
- extends Pick<SignalConfig, 'name'> {
114
+ export interface StateMachineConfig<
115
+ TState extends string,
116
+ TEvent extends MachineEvent,
117
+ TContext extends JsonObject,
118
+ > extends Pick<SignalConfig, 'name'> {
115
119
  /** The initial finite state value. */
116
120
  readonly initial: TState;
117
121