@alwatr/fsm 6.0.2 → 6.1.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/CHANGELOG.md +27 -0
- package/dist/facade.d.ts +1 -1
- package/dist/facade.d.ts.map +1 -1
- package/dist/fsm-service.d.ts +4 -3
- package/dist/fsm-service.d.ts.map +1 -1
- package/dist/main.cjs +3 -3
- package/dist/main.cjs.map +4 -4
- package/dist/main.mjs +3 -3
- package/dist/main.mjs.map +3 -3
- package/dist/type.d.ts +24 -7
- package/dist/type.d.ts.map +1 -1
- package/package.json +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [6.1.1](https://github.com/Alwatr/flux/compare/v6.1.0...v6.1.1) (2025-11-12)
|
|
7
|
+
|
|
8
|
+
### 🐛 Bug Fixes
|
|
9
|
+
|
|
10
|
+
* update defaultValue to initialValue in createFsmService for clarity ([f54c61c](https://github.com/Alwatr/flux/commit/f54c61c4c1a7d2f476f3cedd480a9c31a4eac29f))
|
|
11
|
+
|
|
12
|
+
### 🔗 Dependencies update
|
|
13
|
+
|
|
14
|
+
* update dependencies and devDependencies versions across packages ([ab923fa](https://github.com/Alwatr/flux/commit/ab923fa8ec7f504a3ce59e0ec944d05d361f60be))
|
|
15
|
+
|
|
16
|
+
## [6.1.0](https://github.com/Alwatr/flux/compare/v6.0.2...v6.1.0) (2025-09-22)
|
|
17
|
+
|
|
18
|
+
### ✨ Features
|
|
19
|
+
|
|
20
|
+
* enhance createFsmService to support persistent state signals ([9e46b3b](https://github.com/Alwatr/flux/commit/9e46b3b7da52df6b342b92a2468f0198c1d69c1e))
|
|
21
|
+
|
|
22
|
+
### 🐛 Bug Fixes
|
|
23
|
+
|
|
24
|
+
* update FsmService to use JsonObject for context and remove state signal creation ([5fbb6a6](https://github.com/Alwatr/flux/commit/5fbb6a662625bd4da9fa0d1428c5d683631c56f8))
|
|
25
|
+
* update storageKey handling in PersistentStateSignal and createFsmService for improved state management ([42e764f](https://github.com/Alwatr/flux/commit/42e764f58a2f804c6082a46bfb96eb678a49c22a))
|
|
26
|
+
* update type definitions to use JsonObject for context in MachineState, Assigner, Effect, Condition, Transition, and StateMachineConfig ([e07e991](https://github.com/Alwatr/flux/commit/e07e9914e944b35a08a1c079053675a66183644a))
|
|
27
|
+
|
|
28
|
+
### 🔗 Dependencies update
|
|
29
|
+
|
|
30
|
+
* update dependencies to latest versions across packages ([97bd715](https://github.com/Alwatr/flux/commit/97bd71555912053f8b2ba6ad0578b74bf7f1c1d3))
|
|
31
|
+
* update package dependencies for @alwatr/yarn-upgrade, @alwatr/logger, and other packages ([96b56e7](https://github.com/Alwatr/flux/commit/96b56e75360411bed73ce84acb870db6153f8917))
|
|
32
|
+
|
|
6
33
|
## [6.0.2](https://github.com/Alwatr/flux/compare/v6.0.1...v6.0.2) (2025-09-21)
|
|
7
34
|
|
|
8
35
|
### 🔗 Dependencies update
|
package/dist/facade.d.ts
CHANGED
|
@@ -60,5 +60,5 @@ import type { MachineEvent, StateMachineConfig } from './type.js';
|
|
|
60
60
|
* // lightService.destroy();
|
|
61
61
|
* ```
|
|
62
62
|
*/
|
|
63
|
-
export declare function createFsmService<TState extends string, TEvent extends MachineEvent, TContext extends
|
|
63
|
+
export declare function createFsmService<TState extends string, TEvent extends MachineEvent, TContext extends JsonObject>(config: StateMachineConfig<TState, TEvent, TContext>): FsmService<TState, TEvent, TContext>;
|
|
64
64
|
//# sourceMappingURL=facade.d.ts.map
|
package/dist/facade.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facade.d.ts","sourceRoot":"","sources":["../src/facade.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/fsm-service.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type StateSignal, type PersistentStateSignal } from '@alwatr/signal';
|
|
1
2
|
import type { StateMachineConfig, MachineState, MachineEvent } from './type.js';
|
|
2
3
|
/**
|
|
3
4
|
* A generic, encapsulated service that creates, runs, and manages a finite state machine.
|
|
@@ -8,15 +9,15 @@ import type { StateMachineConfig, MachineState, MachineEvent } from './type.js';
|
|
|
8
9
|
* @template TEvent The union type of all possible events.
|
|
9
10
|
* @template TContext The type of the machine's context (extended state).
|
|
10
11
|
*/
|
|
11
|
-
export declare class FsmService<TState extends string, TEvent extends MachineEvent, TContext extends
|
|
12
|
+
export declare class FsmService<TState extends string, TEvent extends MachineEvent, TContext extends JsonObject> {
|
|
12
13
|
protected readonly config_: StateMachineConfig<TState, TEvent, TContext>;
|
|
14
|
+
private readonly stateSignal__;
|
|
13
15
|
protected readonly logger_: import("@alwatr/logger").AlwatrLogger;
|
|
14
16
|
/** The event signal for sending events to the FSM. */
|
|
15
17
|
readonly eventSignal: import("@alwatr/signal").EventSignal<TEvent>;
|
|
16
|
-
private readonly stateSignal__;
|
|
17
18
|
/** The public, read-only state signal. Subscribe to react to state changes. */
|
|
18
19
|
readonly stateSignal: import("@alwatr/signal").IReadonlySignal<MachineState<TState, TContext>>;
|
|
19
|
-
constructor(config_: StateMachineConfig<TState, TEvent, TContext
|
|
20
|
+
constructor(config_: StateMachineConfig<TState, TEvent, TContext>, stateSignal__: StateSignal<MachineState<TState, TContext>> | PersistentStateSignal<MachineState<TState, TContext>>);
|
|
20
21
|
/**
|
|
21
22
|
* The core FSM logic that processes a single event and transitions the machine to a new state.
|
|
22
23
|
* This process is atomic and follows the Run-to-Completion (RTC) model.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fsm-service.d.ts","sourceRoot":"","sources":["../src/fsm-service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fsm-service.d.ts","sourceRoot":"","sources":["../src/fsm-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,KAAK,WAAW,EAAE,KAAK,qBAAqB,EAAC,MAAM,gBAAgB,CAAC;AAE/F,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;IAYnG,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC;IACxE,OAAO,CAAC,QAAQ,CAAC,aAAa;IAZhC,SAAS,CAAC,QAAQ,CAAC,OAAO,wCAA4C;IAEtE,sDAAsD;IACtD,SAAgB,WAAW,+CAExB;IAEH,+EAA+E;IAC/E,SAAgB,WAAW,2EAAmC;gBAGzC,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;IAMrI;;;;;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"}
|
package/dist/main.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** 📦 @alwatr/fsm v6.
|
|
2
|
-
__dev_mode__: console.debug("📦 @alwatr/fsm v6.
|
|
3
|
-
"use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var main_exports={};__export(main_exports,{FsmService:()=>FsmService,createFsmService:()=>createFsmService});module.exports=__toCommonJS(main_exports);var import_logger=require("@alwatr/logger");var import_signal=require("@alwatr/signal");var FsmService=class{constructor(config_){this.config_=config_;this.logger_=(0,import_logger.createLogger)(`fsm:${this.config_.name}`);this.eventSignal=(0,import_signal.createEventSignal)({name:`fsm-event-${this.config_.name}`});this.
|
|
1
|
+
/** 📦 @alwatr/fsm v6.1.1 */
|
|
2
|
+
__dev_mode__: console.debug("📦 @alwatr/fsm v6.1.1");
|
|
3
|
+
"use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var main_exports={};__export(main_exports,{FsmService:()=>FsmService,createFsmService:()=>createFsmService});module.exports=__toCommonJS(main_exports);var import_signal2=require("@alwatr/signal");var import_logger=require("@alwatr/logger");var import_signal=require("@alwatr/signal");var FsmService=class{constructor(config_,stateSignal__){this.config_=config_;this.stateSignal__=stateSignal__;this.logger_=(0,import_logger.createLogger)(`fsm:${this.config_.name}`);this.eventSignal=(0,import_signal.createEventSignal)({name:`fsm-event-${this.config_.name}`});this.stateSignal=this.stateSignal__.asReadonly();this.logger_.logMethodArgs?.("constructor",config_);this.eventSignal.subscribe(this.processTransition__.bind(this),{receivePrevious:false})}async processTransition__(event){const currentState=this.stateSignal__.get();this.logger_.logMethodArgs?.("processTransition__",{state:currentState.name,event});const transition=this.findTransition__(event,currentState.context);if(!transition){this.logger_.incident?.("processTransition__","ignored_event","No valid transition found for event",{state:currentState.name,event});return}const targetStateName=transition.target??currentState.name;if(targetStateName!==currentState.name){void this.executeEffects__(event,currentState.context,this.config_.states[currentState.name]?.exit)}const nextContext=this.applyAssigners__(event,currentState.context,transition.assigners);const nextState={name:targetStateName,context:nextContext};this.stateSignal__.set(nextState);if(nextState.name!==currentState.name){void this.executeEffects__(event,nextState.context,this.config_.states[nextState.name]?.entry)}}findTransition__(event,context){this.logger_.logMethod?.("findTransition__");const currentStateName=this.stateSignal__.get().name;const currentStateConfig=this.config_.states[currentStateName];const transitions=currentStateConfig?.on?.[event.type];if(!transitions)return void 0;const transitionsArray=Array.isArray(transitions)?transitions:[transitions];return transitionsArray.find((transition,index)=>{if(!transition.condition)return true;try{const conditionMet=transition.condition(event,context);this.logger_.logStep?.("findTransition__","check_condition",{state:currentStateName,eventType:event.type,transitionIndex:index,condition:transition.condition.name||"anonymous",result:conditionMet});return conditionMet}catch(error){this.logger_.error("findTransition__","condition_failed",error,{state:currentStateName,eventType:event.type,transitionIndex:index,condition:transition.condition.name||"anonymous"});return false}})}async executeEffects__(event,context,effects){if(!effects){this.logger_.logMethodArgs?.("executeEffects__//skipped",{count:0});return}const effectsArray=Array.isArray(effects)?effects:[effects];this.logger_.logMethodArgs?.("executeEffects__",{count:effectsArray.length});for(const effect of effectsArray){try{const result=await effect(event,context);if(result&&"type"in result){this.logger_.logStep?.("executeEffects__","new_event_from_effect",{effect:effect.name||"anonymous",state:this.stateSignal__.get().name,newEvent:result.type});this.eventSignal.dispatch(result)}}catch(error){this.logger_.error("executeEffects__","effect_failed",error,{effect:effect.name||"anonymous",state:this.stateSignal__.get().name,event,context})}}}applyAssigners__(event,context,assigners){if(!assigners){this.logger_.logMethodArgs?.("applyAssigners__//skipped",{count:0});return context}const assignersArray=Array.isArray(assigners)?assigners:[assigners];this.logger_.logMethodArgs?.("applyAssigners__",{count:assignersArray.length});try{return assignersArray.reduce((accContext,assigner)=>{const partialUpdate=assigner(event,accContext);this.logger_.logMethodFull?.(`event.${event.type}.action.${assigner.name||"anonymous"}`,{event,accContext},partialUpdate);if(typeof partialUpdate==="object"&&partialUpdate!==null){return{...accContext,...partialUpdate}}return accContext},context)}catch(error){this.logger_.error("applyAssigners__","assigner_failed_atomic",error,{event,context});return context}}destroy(){this.logger_.logMethod?.("destroy");this.eventSignal.destroy();this.stateSignal.destroy()}};function createFsmService(config){const initialValue={name:config.initial,context:config.context};const stateSignal=config.persistent?(0,import_signal2.createPersistentStateSignal)({name:`fsm-state-${config.name}`,storageKey:config.persistent.storageKey??config.name,initialValue,schemaVersion:config.persistent.schemaVersion}):(0,import_signal2.createStateSignal)({name:`fsm-state-${config.name}`,initialValue});return new FsmService(config,stateSignal)}0&&(module.exports={FsmService,createFsmService});
|
|
4
4
|
//# sourceMappingURL=main.cjs.map
|
package/dist/main.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/main.ts", "../src/
|
|
4
|
-
"sourcesContent": ["export * from './facade.js';\nexport * from './fsm-service.js';\nexport type * from './type.js';\n", "import {createLogger} from '@alwatr/logger';\nimport {createStateSignal, createEventSignal} 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 Record<string, unknown>> {\n protected readonly logger_ = createLogger(`fsm:${this.config_.name}`);\n\n /** The event signal for sending events to the FSM. */\n public readonly eventSignal = createEventSignal<TEvent>({\n name: `fsm-event-${this.config_.name}`,\n });\n\n private readonly stateSignal__ = createStateSignal<MachineState<TState, TContext>>({\n name: `fsm-state-${this.config_.name}`,\n initialValue: {\n name: this.config_.initial,\n context: this.config_.context,\n },\n });\n\n /** The public, read-only state signal. Subscribe to react to state changes. */\n public readonly stateSignal = this.stateSignal__.asReadonly();\n\n constructor(protected readonly config_: StateMachineConfig<TState, TEvent, TContext>) {\n this.logger_.logMethodArgs?.('constructor', config_);\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", "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: [() => ({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 Record<string, unknown>>(\n config: StateMachineConfig<TState, TEvent, TContext>,\n): FsmService<TState, TEvent, TContext> {\n return new FsmService(config);\n}\n"],
|
|
5
|
-
"mappings": ";;qqBAAA,uJCAA,kBAA2B,0BAC3B,
|
|
6
|
-
"names": []
|
|
3
|
+
"sources": ["../src/main.ts", "../src/facade.ts", "../src/fsm-service.ts"],
|
|
4
|
+
"sourcesContent": ["export * from './facade.js';\nexport * from './fsm-service.js';\nexport type * from './type.js';\n", "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", "import {createLogger} from '@alwatr/logger';\nimport {createEventSignal, type StateSignal, type PersistentStateSignal} 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_ = createLogger(`fsm:${this.config_.name}`);\n\n /** The event signal for sending events to the FSM. */\n public readonly eventSignal = createEventSignal<TEvent>({\n name: `fsm-event-${this.config_.name}`,\n });\n\n /** The public, read-only state signal. Subscribe to react to state changes. */\n public readonly stateSignal = this.stateSignal__.asReadonly();\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_.logMethodArgs?.('constructor', config_);\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
|
+
"mappings": "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",
|
|
6
|
+
"names": ["import_signal"]
|
|
7
7
|
}
|
package/dist/main.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** 📦 @alwatr/fsm v6.
|
|
2
|
-
__dev_mode__: console.debug("📦 @alwatr/fsm v6.
|
|
3
|
-
import{createLogger}from"@alwatr/logger";import{
|
|
1
|
+
/** 📦 @alwatr/fsm v6.1.1 */
|
|
2
|
+
__dev_mode__: console.debug("📦 @alwatr/fsm v6.1.1");
|
|
3
|
+
import{createPersistentStateSignal,createStateSignal}from"@alwatr/signal";import{createLogger}from"@alwatr/logger";import{createEventSignal}from"@alwatr/signal";var FsmService=class{constructor(config_,stateSignal__){this.config_=config_;this.stateSignal__=stateSignal__;this.logger_=createLogger(`fsm:${this.config_.name}`);this.eventSignal=createEventSignal({name:`fsm-event-${this.config_.name}`});this.stateSignal=this.stateSignal__.asReadonly();this.logger_.logMethodArgs?.("constructor",config_);this.eventSignal.subscribe(this.processTransition__.bind(this),{receivePrevious:false})}async processTransition__(event){const currentState=this.stateSignal__.get();this.logger_.logMethodArgs?.("processTransition__",{state:currentState.name,event});const transition=this.findTransition__(event,currentState.context);if(!transition){this.logger_.incident?.("processTransition__","ignored_event","No valid transition found for event",{state:currentState.name,event});return}const targetStateName=transition.target??currentState.name;if(targetStateName!==currentState.name){void this.executeEffects__(event,currentState.context,this.config_.states[currentState.name]?.exit)}const nextContext=this.applyAssigners__(event,currentState.context,transition.assigners);const nextState={name:targetStateName,context:nextContext};this.stateSignal__.set(nextState);if(nextState.name!==currentState.name){void this.executeEffects__(event,nextState.context,this.config_.states[nextState.name]?.entry)}}findTransition__(event,context){this.logger_.logMethod?.("findTransition__");const currentStateName=this.stateSignal__.get().name;const currentStateConfig=this.config_.states[currentStateName];const transitions=currentStateConfig?.on?.[event.type];if(!transitions)return void 0;const transitionsArray=Array.isArray(transitions)?transitions:[transitions];return transitionsArray.find((transition,index)=>{if(!transition.condition)return true;try{const conditionMet=transition.condition(event,context);this.logger_.logStep?.("findTransition__","check_condition",{state:currentStateName,eventType:event.type,transitionIndex:index,condition:transition.condition.name||"anonymous",result:conditionMet});return conditionMet}catch(error){this.logger_.error("findTransition__","condition_failed",error,{state:currentStateName,eventType:event.type,transitionIndex:index,condition:transition.condition.name||"anonymous"});return false}})}async executeEffects__(event,context,effects){if(!effects){this.logger_.logMethodArgs?.("executeEffects__//skipped",{count:0});return}const effectsArray=Array.isArray(effects)?effects:[effects];this.logger_.logMethodArgs?.("executeEffects__",{count:effectsArray.length});for(const effect of effectsArray){try{const result=await effect(event,context);if(result&&"type"in result){this.logger_.logStep?.("executeEffects__","new_event_from_effect",{effect:effect.name||"anonymous",state:this.stateSignal__.get().name,newEvent:result.type});this.eventSignal.dispatch(result)}}catch(error){this.logger_.error("executeEffects__","effect_failed",error,{effect:effect.name||"anonymous",state:this.stateSignal__.get().name,event,context})}}}applyAssigners__(event,context,assigners){if(!assigners){this.logger_.logMethodArgs?.("applyAssigners__//skipped",{count:0});return context}const assignersArray=Array.isArray(assigners)?assigners:[assigners];this.logger_.logMethodArgs?.("applyAssigners__",{count:assignersArray.length});try{return assignersArray.reduce((accContext,assigner)=>{const partialUpdate=assigner(event,accContext);this.logger_.logMethodFull?.(`event.${event.type}.action.${assigner.name||"anonymous"}`,{event,accContext},partialUpdate);if(typeof partialUpdate==="object"&&partialUpdate!==null){return{...accContext,...partialUpdate}}return accContext},context)}catch(error){this.logger_.error("applyAssigners__","assigner_failed_atomic",error,{event,context});return context}}destroy(){this.logger_.logMethod?.("destroy");this.eventSignal.destroy();this.stateSignal.destroy()}};function createFsmService(config){const initialValue={name:config.initial,context:config.context};const stateSignal=config.persistent?createPersistentStateSignal({name:`fsm-state-${config.name}`,storageKey:config.persistent.storageKey??config.name,initialValue,schemaVersion:config.persistent.schemaVersion}):createStateSignal({name:`fsm-state-${config.name}`,initialValue});return new FsmService(config,stateSignal)}export{FsmService,createFsmService};
|
|
4
4
|
//# sourceMappingURL=main.mjs.map
|
package/dist/main.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/
|
|
4
|
-
"sourcesContent": ["import {createLogger} from '@alwatr/logger';\nimport {createStateSignal, createEventSignal} 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 Record<string, unknown>> {\n protected readonly logger_ = createLogger(`fsm:${this.config_.name}`);\n\n /** The event signal for sending events to the FSM. */\n public readonly eventSignal = createEventSignal<TEvent>({\n name: `fsm-event-${this.config_.name}`,\n });\n\n private readonly stateSignal__ = createStateSignal<MachineState<TState, TContext>>({\n name: `fsm-state-${this.config_.name}`,\n initialValue: {\n name: this.config_.initial,\n context: this.config_.context,\n },\n });\n\n /** The public, read-only state signal. Subscribe to react to state changes. */\n public readonly stateSignal = this.stateSignal__.asReadonly();\n\n constructor(protected readonly config_: StateMachineConfig<TState, TEvent, TContext>) {\n this.logger_.logMethodArgs?.('constructor', config_);\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", "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: [() => ({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 Record<string, unknown>>(\n config: StateMachineConfig<TState, TEvent, TContext>,\n): FsmService<TState, TEvent, TContext> {\n return new FsmService(config);\n}\n"],
|
|
5
|
-
"mappings": ";;AAAA,OAAQ,iBAAmB,iBAC3B,OAAQ,
|
|
3
|
+
"sources": ["../src/facade.ts", "../src/fsm-service.ts"],
|
|
4
|
+
"sourcesContent": ["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", "import {createLogger} from '@alwatr/logger';\nimport {createEventSignal, type StateSignal, type PersistentStateSignal} 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_ = createLogger(`fsm:${this.config_.name}`);\n\n /** The event signal for sending events to the FSM. */\n public readonly eventSignal = createEventSignal<TEvent>({\n name: `fsm-event-${this.config_.name}`,\n });\n\n /** The public, read-only state signal. Subscribe to react to state changes. */\n public readonly stateSignal = this.stateSignal__.asReadonly();\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_.logMethodArgs?.('constructor', config_);\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
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/type.d.ts
CHANGED
|
@@ -6,12 +6,12 @@ import type { SignalConfig } from '@alwatr/signal';
|
|
|
6
6
|
* @template TState The union type of the finite state values.
|
|
7
7
|
* @template TContext The type of the context object (extended state).
|
|
8
8
|
*/
|
|
9
|
-
export
|
|
9
|
+
export type MachineState<TState extends string, TContext extends JsonObject> = {
|
|
10
10
|
/** The current finite state value. */
|
|
11
11
|
readonly name: TState;
|
|
12
12
|
/** The context (extended state) of the machine, holding quantitative data. */
|
|
13
13
|
readonly context: TContext;
|
|
14
|
-
}
|
|
14
|
+
};
|
|
15
15
|
/**
|
|
16
16
|
* Represents an event that can be sent to the state machine.
|
|
17
17
|
* It must have a `type` property, which acts as a discriminator.
|
|
@@ -32,7 +32,7 @@ export interface MachineEvent<TEventType extends string = string> {
|
|
|
32
32
|
* @template TEvent The type of the event that triggered this assigner.
|
|
33
33
|
* @returns A partial context object to be merged into the machine's context.
|
|
34
34
|
*/
|
|
35
|
-
export type Assigner<TEvent extends MachineEvent, TContext extends
|
|
35
|
+
export type Assigner<TEvent extends MachineEvent, TContext extends JsonObject> = (event: Readonly<TEvent>, context: Readonly<TContext>) => Partial<TContext> | void;
|
|
36
36
|
/**
|
|
37
37
|
* Defines an effect (asynchronous side-effect action) executed on state entry/exit.
|
|
38
38
|
* It can interact with the outside world and can dispatch new events.
|
|
@@ -41,7 +41,7 @@ export type Assigner<TEvent extends MachineEvent, TContext extends Record<string
|
|
|
41
41
|
* @template TEvent The type of the event that triggered this effect.
|
|
42
42
|
* @returns void or a Promise<void>.
|
|
43
43
|
*/
|
|
44
|
-
export type Effect<TEvent extends MachineEvent, TContext extends
|
|
44
|
+
export type Effect<TEvent extends MachineEvent, TContext extends JsonObject> = (event: Readonly<TEvent>, context: Readonly<TContext>) => Awaitable<TEvent | void>;
|
|
45
45
|
/**
|
|
46
46
|
* Defines a conditional guard function for a transition.
|
|
47
47
|
* The transition is only taken if this function returns true.
|
|
@@ -50,7 +50,7 @@ export type Effect<TEvent extends MachineEvent, TContext extends Record<string,
|
|
|
50
50
|
* @template TEvent The type of the event.
|
|
51
51
|
* @returns `true` if the transition should be taken, `false` otherwise.
|
|
52
52
|
*/
|
|
53
|
-
export type Condition<TEvent extends MachineEvent, TContext extends
|
|
53
|
+
export type Condition<TEvent extends MachineEvent, TContext extends JsonObject> = (event: Readonly<TEvent>, context: Readonly<TContext>) => boolean;
|
|
54
54
|
/**
|
|
55
55
|
* Defines a transition for a given state and event. It specifies the target state,
|
|
56
56
|
* actions, and an optional condition.
|
|
@@ -59,7 +59,7 @@ export type Condition<TEvent extends MachineEvent, TContext extends Record<strin
|
|
|
59
59
|
* @template TEvent The type of the event.
|
|
60
60
|
* @template TContext The type of the machine's context.
|
|
61
61
|
*/
|
|
62
|
-
export interface Transition<TState extends string, TEvent extends MachineEvent, TContext extends
|
|
62
|
+
export interface Transition<TState extends string, TEvent extends MachineEvent, TContext extends JsonObject> {
|
|
63
63
|
/** The target state to transition to. If undefined, it's an internal transition. */
|
|
64
64
|
readonly target?: TState;
|
|
65
65
|
/** A condition function that must return true for the transition to occur. */
|
|
@@ -67,6 +67,21 @@ export interface Transition<TState extends string, TEvent extends MachineEvent,
|
|
|
67
67
|
/** An array of assigners to execute. These update context synchronously. */
|
|
68
68
|
readonly assigners?: SingleOrArray<Assigner<TEvent, TContext>>;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Configuration options for persisting the FSM state in localStorage.
|
|
72
|
+
*/
|
|
73
|
+
export interface FsmPersistenceConfig {
|
|
74
|
+
/**
|
|
75
|
+
* The version of the state's data structure (schema).
|
|
76
|
+
* Increment this number whenever you make a breaking change to the state's context shape.
|
|
77
|
+
*/
|
|
78
|
+
schemaVersion: number;
|
|
79
|
+
/**
|
|
80
|
+
* The key under which to store the FSM state in localStorage.
|
|
81
|
+
* @default `signal-name`
|
|
82
|
+
*/
|
|
83
|
+
storageKey?: string;
|
|
84
|
+
}
|
|
70
85
|
/**
|
|
71
86
|
* The declarative configuration object for creating a state machine.
|
|
72
87
|
* This object defines the entire behavior of the machine.
|
|
@@ -75,11 +90,13 @@ export interface Transition<TState extends string, TEvent extends MachineEvent,
|
|
|
75
90
|
* @template TEvent The union type of all possible events.
|
|
76
91
|
* @template TContext The type of the context object.
|
|
77
92
|
*/
|
|
78
|
-
export interface StateMachineConfig<TState extends string, TEvent extends MachineEvent, TContext extends
|
|
93
|
+
export interface StateMachineConfig<TState extends string, TEvent extends MachineEvent, TContext extends JsonObject> extends Pick<SignalConfig, 'name'> {
|
|
79
94
|
/** The initial finite state value. */
|
|
80
95
|
readonly initial: TState;
|
|
81
96
|
/** The initial context (extended state) of the machine. */
|
|
82
97
|
readonly context: TContext;
|
|
98
|
+
/** If provided, the FSM's state will be persisted in localStorage. */
|
|
99
|
+
persistent?: FsmPersistenceConfig;
|
|
83
100
|
/** An object defining all possible states and their transitions. */
|
|
84
101
|
readonly states: {
|
|
85
102
|
readonly [S in TState]?: {
|
package/dist/type.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAGjD;;;;;;GAMG;AACH,MAAM,
|
|
1
|
+
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alwatr/fsm",
|
|
3
3
|
"description": "A tiny, type-safe, declarative, and reactive finite state machine (FSM) library for modern TypeScript applications, built on top of Alwatr Signals.",
|
|
4
|
-
"version": "6.
|
|
4
|
+
"version": "6.1.1",
|
|
5
5
|
"author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
|
|
6
6
|
"bugs": "https://github.com/Alwatr/flux/issues",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@alwatr/logger": "^6.0.
|
|
9
|
-
"@alwatr/signal": "6.
|
|
8
|
+
"@alwatr/logger": "^6.0.10",
|
|
9
|
+
"@alwatr/signal": "6.1.1"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@alwatr/nano-build": "^6.3.
|
|
13
|
-
"@alwatr/prettier-config": "^5.0.
|
|
14
|
-
"@alwatr/tsconfig-base": "^6.0.
|
|
15
|
-
"@alwatr/type-helper": "^6.1.
|
|
16
|
-
"@types/node": "^22.
|
|
17
|
-
"jest": "^30.
|
|
18
|
-
"typescript": "^5.9.
|
|
12
|
+
"@alwatr/nano-build": "^6.3.6",
|
|
13
|
+
"@alwatr/prettier-config": "^5.0.5",
|
|
14
|
+
"@alwatr/tsconfig-base": "^6.0.3",
|
|
15
|
+
"@alwatr/type-helper": "^6.1.5",
|
|
16
|
+
"@types/node": "^22.19.1",
|
|
17
|
+
"jest": "^30.2.0",
|
|
18
|
+
"typescript": "^5.9.3"
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
|
21
21
|
".": {
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"sideEffects": false,
|
|
70
70
|
"type": "module",
|
|
71
71
|
"types": "./dist/main.d.ts",
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "af2f09113ac8c6ce53ba697ff9f690955008a132"
|
|
73
73
|
}
|