diff --git a/README.md b/README.md index 9b748eae..6d9044aa 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,26 @@ -# Alwatr Signal +# Alwatr Flux - Elegant State Management and Event System ## Introduction -Elegant powerful event system for handle global signals and states base on observable design pattern, written in tiny TypeScript module. +Alwatr Flux empowers your applications with elegant and powerful state management and event handling capabilities. Built on the observable design pattern, Flux provides a lightweight yet robust foundation for managing global signals and states. -Every signal has own value and can be used as a advance **state management** like redux and recoil without the complexities and unnecessary facilities of those libraries. +**Key Features:** -Contains the following packages: +- **Intuitive State Management:** Embrace Flux as an advanced alternative to Redux or Recoil, minus the complexities and unnecessary overhead. Each signal maintains its own value, offering seamless state control. +- **Finite-State Machines (FSM):** Leverage observables to gracefully manage invocations and state transitions within your finite-state machines. +- **Server Context Management:** Effortlessly handle server-side context with Flux's elegant context manager, ensuring optimal organization and control. +- ... -1. [Finite-state machines (FSM)](./packages/logger): Managing invocations finite-state machines base on observable signal pattern. -2. [Server Context](./packages/server-context): Elegant powerful server-context manager base on alwatr signal. -3. [Signal](./packages/signal): Elegant powerful event system for handle global signals and states. - - +## Usage -## Installation +Refer to the individual package READMEs for comprehensive usage instructions and examples. -```bash -npm install @alwatr/module-name -``` +## Contributing -## Usage - -Follow each package's README for usage instructions. +Contributions are welcome! Please consult the CONTRIBUTING guidelines for detailed information on how to get involved. ## License [MIT](./LICENSE) + +``` diff --git a/package.json b/package.json index 0f701704..7049e6e2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "alwatr-signal", - "description": "Elegant powerful event system for handle global signals and states base on observable design pattern, written in tiny TypeScript module.", - "repository": "https://github.com/Alwatr/signal", + "name": "alwatr-flux", + "description": "Alwatr Flux empowers your applications with elegant and powerful state management and event handling capabilities. Built on the observable design pattern, Flux provides a lightweight yet robust foundation for managing global signals and states.", + "repository": "https://github.com/Alwatr/flux", "author": "S. Ali Mihandoost (https://ali.mihandoost.com)", "license": "MIT", "type": "module", @@ -60,5 +60,5 @@ "prettier": "^3.3.3", "typescript": "^5.6.2" }, - "packageManager": "yarn@4.4.1" + "packageManager": "yarn@4.5.0" } diff --git a/packages/fsm/package.json b/packages/fsm/package.json index 228eaaf3..b88379e9 100644 --- a/packages/fsm/package.json +++ b/packages/fsm/package.json @@ -61,12 +61,14 @@ }, "dependencies": { "@alwatr/logger": "^3.2.12", + "@alwatr/polyfill-has-own": "1.0.8", "@alwatr/signal": "workspace:^" }, "devDependencies": { "@alwatr/nano-build": "^1.3.8", "@alwatr/prettier-config": "^1.0.4", "@alwatr/tsconfig-base": "^1.2.0", + "@alwatr/type-helper": "^1.2.5", "@types/node": "^22.5.5", "jest": "^29.7.0", "typescript": "^5.6.2" diff --git a/packages/fsm/src/base.ts b/packages/fsm/src/base.ts index d03c8f6f..b1257b5e 100644 --- a/packages/fsm/src/base.ts +++ b/packages/fsm/src/base.ts @@ -1,61 +1,64 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import {definePackage} from '@alwatr/logger'; +import '@alwatr/polyfill-has-own'; import {AlwatrObservable} from '@alwatr/signal'; import type {ActionName, ActionRecord, StateEventDetail, StateRecord} from './type.js'; -import type {MaybePromise} from '@alwatr/type'; +import type {} from '@alwatr/nano-build'; +import type {MaybePromise} from '@alwatr/type-helper'; -definePackage('fsm', '2.x'); +definePackage('@alwatr/signal', __package_version__); /** * Finite State Machine Base Class */ export abstract class FiniteStateMachineBase extends AlwatrObservable { - /** - * Current state - */ - protected get _state(): S { - return this._getData()!; - } - /** * States and transitions config. */ - protected _stateRecord: StateRecord = {}; + protected stateRecord_: StateRecord = {}; /** * Bind actions name to class methods */ - protected _actionRecord: ActionRecord = {}; + protected actionRecord_: ActionRecord = {}; - protected _initialState: S; + protected initialState_: S; + + protected override data_: S; constructor(config: {name: string; loggerPrefix?: string; initialState: S}) { config.loggerPrefix ??= 'fsm'; super(config); - this._initialState = config.initialState; - this._reset(); + this.data_ = this.initialState_ = config.initialState; + } + + /** + * Reset machine to initial state. + */ + protected resetToInitialState_(): void { + this.logger_.logMethod?.('resetToInitialState_'); + this.data_ = this.initialState_; } /** * Transition condition. */ - protected _shouldTransition(_eventDetail: StateEventDetail): MaybePromise { - this._logger.logMethodFull?.('_shouldTransition', _eventDetail, true); + protected shouldTransition_(_eventDetail: StateEventDetail): MaybePromise { + this.logger_.logMethodFull?.('shouldTransition_', _eventDetail, true); return true; } /** * Transition finite state machine instance to new state. */ - protected async _transition(event: E): Promise { - const fromState = this._state; - const toState = this._stateRecord[fromState]?.[event] ?? this._stateRecord._all?.[event]; + protected async transition_(event: E): Promise { + const fromState = this.data_; + const toState = this.stateRecord_[fromState]?.[event] ?? this.stateRecord_._all?.[event]; - this._logger.logMethodArgs?.('_transition', {fromState, event, toState}); + this.logger_.logMethodArgs?.('transition_', {fromState, event, toState}); if (toState == null) { - this._logger.incident?.('transition', 'invalid_target_state', { + this.logger_.incident?.('transition', 'invalid_target_state', { fromState, event, }); @@ -64,51 +67,45 @@ export abstract class FiniteStateMachineBase const eventDetail: StateEventDetail = {from: fromState, event, to: toState}; - if ((await this._shouldTransition(eventDetail)) !== true) return; + if ((await this.shouldTransition_(eventDetail)) !== true) return; - this._notify(toState); + this.notify_(toState); - this._transitioned(eventDetail); + this.postTransition__(eventDetail); } /** * Execute all actions for current state. */ - protected async _transitioned(eventDetail: StateEventDetail): Promise { - this._logger.logMethodArgs?.('_transitioned', eventDetail); + protected async postTransition__(eventDetail: StateEventDetail): Promise { + this.logger_.logMethodArgs?.('_transitioned', eventDetail); - await this._$execAction(`_on_${eventDetail.event}`, eventDetail); + await this.execAction__(`_on_${eventDetail.event}`, eventDetail); if (eventDetail.from !== eventDetail.to) { - await this._$execAction(`_on_state_exit`, eventDetail); - await this._$execAction(`_on_${eventDetail.from}_exit`, eventDetail); - await this._$execAction(`_on_state_enter`, eventDetail); - await this._$execAction(`_on_${eventDetail.to}_enter`, eventDetail); + await this.execAction__(`_on_state_exit`, eventDetail); + await this.execAction__(`_on_${eventDetail.from}_exit`, eventDetail); + await this.execAction__(`_on_state_enter`, eventDetail); + await this.execAction__(`_on_${eventDetail.to}_enter`, eventDetail); } - if (`_on_${eventDetail.from}_${eventDetail.event}` in this) { - this._$execAction(`_on_${eventDetail.from}_${eventDetail.event}`, eventDetail); + if (Object.hasOwn(this, `_on_${eventDetail.from}_${eventDetail.event}`)) { + this.execAction__(`_on_${eventDetail.from}_${eventDetail.event}`, eventDetail); } else { - this._$execAction(`_on_all_${eventDetail.event}`, eventDetail); + // The action `all_eventName` is executed only if the action `fromState_eventName` is not defined. + this.execAction__(`_on_all_${eventDetail.event}`, eventDetail); } } /** * Execute action name if defined in _actionRecord. */ - protected _$execAction(name: ActionName, eventDetail: StateEventDetail): MaybePromise { - const actionFn = this._actionRecord[name]; + protected execAction__(name: ActionName, eventDetail: StateEventDetail): MaybePromise { + const actionFn = this.actionRecord_[name]; if (typeof actionFn === 'function') { - this._logger.logMethodArgs?.('_$execAction', name); - return this._actionRecord[name]?.call(this, eventDetail); + this.logger_.logMethodArgs?.('_$execAction', name); + return actionFn.call(this, eventDetail); } } - - /** - * Reset machine to initial state. - */ - protected _reset(): void { - this._$data = this._initialState; - } } diff --git a/packages/fsm/src/fsm.ts b/packages/fsm/src/fsm.ts index 924301ee..4493374a 100644 --- a/packages/fsm/src/fsm.ts +++ b/packages/fsm/src/fsm.ts @@ -7,14 +7,14 @@ export abstract class FiniteStateMachine ext /** * Current state. */ - get state(): S { - return super._state; + getState(): S { + return this.data_; } /** * Transition finite state machine instance to new state. */ transition(event: E): void { - super._transition(event); + this.transition_(event); } } diff --git a/packages/fsm/src/index.ts b/packages/fsm/src/main.ts similarity index 100% rename from packages/fsm/src/index.ts rename to packages/fsm/src/main.ts diff --git a/packages/fsm/src/type.ts b/packages/fsm/src/type.ts index 577f47d6..ace36eb9 100644 --- a/packages/fsm/src/type.ts +++ b/packages/fsm/src/type.ts @@ -1,4 +1,4 @@ -import type {MaybePromise} from '@alwatr/type'; +import type {MaybePromise} from '@alwatr/type-helper'; export interface StateEventDetail { from: S; diff --git a/packages/server-context/src/index.ts b/packages/server-context/src/main.ts similarity index 100% rename from packages/server-context/src/index.ts rename to packages/server-context/src/main.ts diff --git a/packages/server-context/src/server-request.ts b/packages/server-context/src/server-request.ts index 630dfb9c..04c6a6ad 100644 --- a/packages/server-context/src/server-request.ts +++ b/packages/server-context/src/server-request.ts @@ -2,9 +2,10 @@ import {fetch} from '@alwatr/fetch'; import {ActionRecord, FiniteStateMachineBase, StateRecord} from '@alwatr/fsm'; import {definePackage} from '@alwatr/logger'; -import type {FetchOptions} from '@alwatr/fetch/type.js'; +import type {FetchOptions} from '@alwatr/fetch'; +import type {} from '@alwatr/nano-build'; -definePackage('server-context', '2.x'); +definePackage('@alwatr/signal', __package_version__); export interface ServerRequestConfig extends Partial { name: string; diff --git a/packages/signal/src/context.ts b/packages/signal/src/context.ts index 5092cbdc..5885faf0 100644 --- a/packages/signal/src/context.ts +++ b/packages/signal/src/context.ts @@ -1,9 +1,11 @@ import {AlwatrObservable} from './observable.js'; +import type { Dictionary } from '@alwatr/type-helper'; + /** - * Alwatr context signal. + * Alwatr Context. */ -export class AlwatrContextSignal extends AlwatrObservable { +export class AlwatrContext extends AlwatrObservable { constructor(config: {name: string; loggerPrefix?: string}) { config.loggerPrefix ??= 'context-signal'; super(config); @@ -15,15 +17,15 @@ export class AlwatrContextSignal extends AlwatrObservable { * Return undefined if context not set before or expired. */ getValue(): T | undefined { - return super._getData(); + return this.data_; } /** * Set context value and notify all subscribers. */ setValue(value: T): void { - this._logger.logMethodArgs?.('setValue', {value}); - super._notify(value); + this.logger_.logMethodArgs?.('setValue', {value}); + super.notify_(value); } /** @@ -32,13 +34,13 @@ export class AlwatrContextSignal extends AlwatrObservable { * `receivePrevious` in new subscribers not work until new context changes. */ expire(): void { - super._clear(); + super.clearData_(); } /** * Get the value of the next context changes. */ untilChange(): Promise { - return super._untilNewNotify(); + return super.untilNewNotify_(); } } diff --git a/packages/signal/src/index.ts b/packages/signal/src/main.ts similarity index 78% rename from packages/signal/src/index.ts rename to packages/signal/src/main.ts index f4a673fe..7192d120 100644 --- a/packages/signal/src/index.ts +++ b/packages/signal/src/main.ts @@ -2,5 +2,5 @@ export * from './observable.js'; export * from './simple-signal.js'; export * from './signal.js'; export * from './context.js'; -export * from './multithread-context.js'; +// export * from './multithread-context.js'; export type * from './type.js'; diff --git a/packages/signal/src/multithread-context.ts b/packages/signal/src/multithread-context.ts index 83ffd675..d59ccf6b 100644 --- a/packages/signal/src/multithread-context.ts +++ b/packages/signal/src/multithread-context.ts @@ -1,6 +1,6 @@ import {createLogger} from '@alwatr/logger'; -import {AlwatrContextSignal} from './context.js'; +import {AlwatrContext} from './context.js'; interface AlwatrContextChangedMessage { type: 'alwatr_context_changed'; @@ -11,7 +11,7 @@ interface AlwatrContextChangedMessage { /** * Alwatr multithread context signal. */ -export class AlwatrMultithreadContextSignal extends AlwatrContextSignal { +export class AlwatrMultithreadContextSignal extends AlwatrContext { protected static _logger = createLogger(`alwatr/mt-context`); protected static _worker?: Worker; protected static _registry: Record | undefined> = {}; @@ -29,7 +29,7 @@ export class AlwatrMultithreadContextSignal extends AlwatrContextSignal< if (context === undefined) { throw new Error('context_not_define', {cause: 'context not define in this thread yet!'}); } - context._notify(message.payload); + context.notify_(message.payload); } static _postMessage(name: string, payload: unknown): void { @@ -47,11 +47,11 @@ export class AlwatrMultithreadContextSignal extends AlwatrContextSignal< constructor(config: {name: string; loggerPrefix?: string}) { super(config); - if (AlwatrMultithreadContextSignal._registry[this._name] !== undefined) { + if (AlwatrMultithreadContextSignal._registry[this.name_] !== undefined) { throw new Error('context_name_exist'); } - AlwatrMultithreadContextSignal._registry[this._name] = this as AlwatrMultithreadContextSignal; + AlwatrMultithreadContextSignal._registry[this.name_] = this as AlwatrMultithreadContextSignal; } /** @@ -59,6 +59,6 @@ export class AlwatrMultithreadContextSignal extends AlwatrContextSignal< */ override setValue(value: TValue): void { super.setValue(value); - AlwatrMultithreadContextSignal._postMessage(this._name, value); + AlwatrMultithreadContextSignal._postMessage(this.name_, value); } } diff --git a/packages/signal/src/observable.ts b/packages/signal/src/observable.ts index 053a9205..8524558c 100644 --- a/packages/signal/src/observable.ts +++ b/packages/signal/src/observable.ts @@ -1,62 +1,53 @@ import {createLogger, definePackage} from '@alwatr/logger'; import type {SubscribeOptions, ListenerCallback, Observer, SubscribeResult, AlwatrObservableInterface} from './type.js'; +import type {} from '@alwatr/nano-build'; -definePackage('signal', '2.x'); +definePackage('@alwatr/signal', __package_version__); /** * Alwatr base signal. */ export abstract class AlwatrObservable implements AlwatrObservableInterface { - protected _name; - protected _logger; - protected _$data?: T; - protected _$observers: Observer[] = []; + protected name_; + protected logger_; + protected data_?: T; + protected observers__: Observer[] = []; constructor(config: {name: string; loggerPrefix?: string}) { config.loggerPrefix ??= 'signal'; - this._name = config.name; - this._logger = createLogger(`{${config.loggerPrefix}: ${this._name}}`); - this._logger.logMethod?.('constructor'); - } - - /** - * Get data. - * - * Return undefined if signal not notify before or expired. - */ - protected _getData(): T | undefined { - this._logger.logMethodFull?.('_getData', {}, this._$data); - return this._$data; + this.name_ = config.name; + this.logger_ = createLogger(`{${config.loggerPrefix}: ${this.name_}}`); + this.logger_.logMethodArgs?.('new', config); } /** * Execute all observers and remember data. */ - protected _notify(data: T): void { - this._logger.logMethodArgs?.('_notify', data); - this._$data = data; - setTimeout(() => this._$dispatch(data), 0); + protected notify_(data: T): void { + this.logger_.logMethodArgs?.('notify_', data); + this.data_ = data; + setTimeout(() => this.dispatch__(data), 0); } /** * Execute all observers callback. */ - protected _$dispatch(data: T): void { + protected dispatch__(data: T): void { const removeList: Observer[] = []; - for (const listener of this._$observers) { + for (const listener of this.observers__) { if (listener.options.disabled) continue; if (listener.options.once) removeList.push(listener); try { const ret = listener.callback.call(this, data); if (ret instanceof Promise) { - ret.catch((err) => this._logger.error('_$dispatch', 'call_listener_failed', err)); + ret.catch((err) => this.logger_.error('dispatch__', 'call_listener_failed', err)); } } catch (err) { - this._logger.error('_$dispatch', 'call_listener_failed', err); + this.logger_.error('dispatch__', 'call_listener_failed', err); } } @@ -69,15 +60,15 @@ export abstract class AlwatrObservable implements AlwatrObservableInterface, options: SubscribeOptions = {}): SubscribeResult { - this._logger.logMethodArgs?.('subscribe', {options}); + this.logger_.logMethodArgs?.('subscribe', {options}); - const _listenerObject: Observer = { + const listenerObject_: Observer = { callback: listenerCallback, options, }; let callbackExecuted = false; - const data = this._$data; + const data = this.data_; if (data !== undefined && options.receivePrevious === true && options.disabled !== true) { // Run callback for old dispatch signal callbackExecuted = true; @@ -85,11 +76,11 @@ export abstract class AlwatrObservable implements AlwatrObservableInterface this._logger.error('subscribe.receivePrevious', 'call_signal_callback_failed', err)); + ret.catch((err) => this.logger_.error('subscribe.receivePrevious', 'call_signal_callback_failed', err)); } } catch (err) { - this._logger.error('subscribe.receivePrevious', 'call_signal_callback_failed', err); + this.logger_.error('subscribe.receivePrevious', 'call_signal_callback_failed', err); } }, 0); } @@ -97,10 +88,10 @@ export abstract class AlwatrObservable implements AlwatrObservableInterface implements AlwatrObservableInterface): void { - this._logger.logMethod?.('unsubscribe'); - const listenerIndex = this._$observers.findIndex((listener) => listener.callback === listenerCallback); + this.logger_.logMethod?.('unsubscribe'); + const listenerIndex = this.observers__.findIndex((listener) => listener.callback === listenerCallback); if (listenerIndex !== -1) { - void this._$observers.splice(listenerIndex, 1); + void this.observers__.splice(listenerIndex, 1); } } @@ -125,16 +116,16 @@ export abstract class AlwatrObservable implements AlwatrObservableInterface { - this._logger.logMethod?.('_untilNewNotify'); + protected untilNewNotify_(): Promise { + this.logger_.logMethod?.('untilNewNotify_'); return new Promise((resolve) => { this.subscribe(resolve, { once: true, diff --git a/packages/signal/src/signal.ts b/packages/signal/src/signal.ts index 3ee5eb14..d5fd2beb 100644 --- a/packages/signal/src/signal.ts +++ b/packages/signal/src/signal.ts @@ -13,13 +13,13 @@ export class AlwatrSignal extends AlwatrObservable { * Dispatch an event to all listeners. */ notify(detail: T): void { - this._notify(detail); + this.notify_(detail); } /** * Wait until next event. */ untilNewNotify(): Promise { - return super._untilNewNotify(); + return super.untilNewNotify_(); } } diff --git a/packages/signal/src/simple-signal.ts b/packages/signal/src/simple-signal.ts index e2f01f03..572f2430 100644 --- a/packages/signal/src/simple-signal.ts +++ b/packages/signal/src/simple-signal.ts @@ -13,13 +13,13 @@ export class AlwatrSimpleSignal extends AlwatrObservable { * Dispatch an event to all listeners. */ notify(): void { - this._notify(undefined); + this.notify_(undefined); } /** * Wait until next event signal. */ untilNewNotify(): Promise { - return super._untilNewNotify(); + return super.untilNewNotify_(); } } diff --git a/packages/signal/src/type.ts b/packages/signal/src/type.ts index 5127d82e..e8ed8057 100644 --- a/packages/signal/src/type.ts +++ b/packages/signal/src/type.ts @@ -1,4 +1,4 @@ -import type {MaybePromise} from '@alwatr/type'; +import type {MaybePromise} from '@alwatr/type-helper'; /** * Subscribe options type. diff --git a/packages/signal/tsconfig.json b/packages/signal/tsconfig.json index 8a8312fa..9bea7a7a 100644 --- a/packages/signal/tsconfig.json +++ b/packages/signal/tsconfig.json @@ -7,5 +7,5 @@ "composite": true, }, "include": ["src/**/*.ts"], - "references": [{"path": "../api-server"}], + "references": [], } diff --git a/yarn.lock b/yarn.lock index 06096e76..2ef5521d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -45,9 +45,11 @@ __metadata: dependencies: "@alwatr/logger": "npm:^3.2.12" "@alwatr/nano-build": "npm:^1.3.8" + "@alwatr/polyfill-has-own": "npm:1.0.8" "@alwatr/prettier-config": "npm:^1.0.4" "@alwatr/signal": "workspace:^" "@alwatr/tsconfig-base": "npm:^1.2.0" + "@alwatr/type-helper": "npm:^1.2.5" "@types/node": "npm:^22.5.5" jest: "npm:^29.7.0" typescript: "npm:^5.6.2" @@ -89,6 +91,13 @@ __metadata: languageName: node linkType: hard +"@alwatr/polyfill-has-own@npm:1.0.8": + version: 1.0.8 + resolution: "@alwatr/polyfill-has-own@npm:1.0.8" + checksum: 10c0/7599bb5a098ff084f47ae384465303b8be9be19e74db624c34a5cda9484c46de91928b4fdae3b208a12f68c299c8e1a050e8e2ba08f1fa2e68866673342dfc3a + languageName: node + linkType: hard + "@alwatr/prettier-config@npm:^1.0.4": version: 1.0.4 resolution: "@alwatr/prettier-config@npm:1.0.4" @@ -2312,9 +2321,9 @@ __metadata: languageName: node linkType: hard -"alwatr-signal@workspace:.": +"alwatr-flux@workspace:.": version: 0.0.0-use.local - resolution: "alwatr-signal@workspace:." + resolution: "alwatr-flux@workspace:." dependencies: "@alwatr/eslint-config": "npm:^1.2.5" "@alwatr/prettier-config": "npm:^1.0.4"