@futdevpro/fsm-dynamo
Advanced tools
| import { DyFM_StateMachineEvent } from './state-machine-event.interface'; | ||
| import { DyFM_StateTransition } from './state-transition.interface'; | ||
| /** | ||
| * Config-objektum a `DyFM_StateMachine` konstruktorhoz. | ||
| * | ||
| * - `initial`: induloallapot | ||
| * - `transitions`: a megengedett atmenetek listaja (transitionMap) | ||
| * - `persist`: opcionalis aszinkron callback minden sikeres atmenet utan | ||
| * (NEM a state-valtas elott — a state mar uj-erteku amikor a `persist` | ||
| * fut, igy a callback-be erkezo event tartalmazza a `to`-t es a `stateVersion`-t). | ||
| * Throw-ja kivaltja a `'persist-failed'` reason-t es a state automatikusan | ||
| * visszaall a from-ra (atomicity). | ||
| */ | ||
| export interface DyFM_StateMachineConfig<TState extends string, TContext = unknown> { | ||
| initial: TState; | ||
| transitions: DyFM_StateTransition<TState, TContext>[]; | ||
| persist?: (event: DyFM_StateMachineEvent<TState, TContext>) => Promise<void>; | ||
| } | ||
| //# sourceMappingURL=state-machine-config.interface.d.ts.map |
| {"version":3,"file":"state-machine-config.interface.d.ts","sourceRoot":"","sources":["../../../../src/_modules/state-machine/_models/state-machine-config.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAEpE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,uBAAuB,CAAC,MAAM,SAAS,MAAM,EAAE,QAAQ,GAAG,OAAO;IAChF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;IACtD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9E"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| //# sourceMappingURL=state-machine-config.interface.js.map |
| {"version":3,"file":"state-machine-config.interface.js","sourceRoot":"","sources":["../../../../src/_modules/state-machine/_models/state-machine-config.interface.ts"],"names":[],"mappings":""} |
| /** | ||
| * Egy sikeres atmenet utan kibocsatott esemeny. | ||
| * | ||
| * A `DyFM_StateMachine.events$` Observable-en at szubszkribalhato. Az opcionalis | ||
| * `persist` callback is ezt az objektumot kapja meg, igy a konzumens (pl. | ||
| * Mongoose ControlModel) eldontheti milyen mezoket frissit. | ||
| * | ||
| * `stateVersion` minden sikeres atmenet utan inkrementalodik (0 -> 1 -> 2 -> ...). | ||
| * Optimistic-concurrency token-kent hasznalhato a `persist` callback-ben | ||
| * (`$inc: { stateVersion: 1 }` + `findOneAndUpdate({ stateVersion: prevVersion })` | ||
| * mintara) hogy a race conditions detektalhatoak legyenek tobb-folyamatos | ||
| * deploy-okban. | ||
| */ | ||
| export interface DyFM_StateMachineEvent<TState extends string, TContext = unknown> { | ||
| /** Az atmenet elotti state. */ | ||
| from: TState; | ||
| /** Az atmenet utani state. */ | ||
| to: TState; | ||
| /** A frissitett state-version szam (>= 1). */ | ||
| stateVersion: number; | ||
| /** Az opcionalis context, ahogy a `transition()`-be erkezett. */ | ||
| context?: TContext; | ||
| /** Az atmenet idobelyege (ms epoch). */ | ||
| timestamp: number; | ||
| } | ||
| //# sourceMappingURL=state-machine-event.interface.d.ts.map |
| {"version":3,"file":"state-machine-event.interface.d.ts","sourceRoot":"","sources":["../../../../src/_modules/state-machine/_models/state-machine-event.interface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,sBAAsB,CAAC,MAAM,SAAS,MAAM,EAAE,QAAQ,GAAG,OAAO;IAC/E,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;CACnB"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| //# sourceMappingURL=state-machine-event.interface.js.map |
| {"version":3,"file":"state-machine-event.interface.js","sourceRoot":"","sources":["../../../../src/_modules/state-machine/_models/state-machine-event.interface.ts"],"names":[],"mappings":""} |
| import { Observable } from 'rxjs'; | ||
| import { DyFM_StateMachineConfig } from './state-machine-config.interface'; | ||
| import { DyFM_StateMachineEvent } from './state-machine-event.interface'; | ||
| import { DyFM_StateMachineTransition_Result } from './state-transition-result.interface'; | ||
| /** | ||
| * Generic finite state machine (FR-006, BL-20260518-004). | ||
| * | ||
| * Konfiguralhato `initial` state-tel + `transitions[]` listaval. Reaktiv | ||
| * (`state$` BehaviorSubject + `events$` Subject), single-instance mutex-szel | ||
| * vedett, opcionalis aszinkron `persist` callback-kel. | ||
| * | ||
| * **Hasznalat (CCAP MP1-D RAG ingest minta):** | ||
| * ```typescript | ||
| * enum RagIngestState { Idle='idle', Ingesting='ingesting', Embedding='embedding', | ||
| * Ready='ready', Degraded='degraded', Reingesting='reingesting' } | ||
| * | ||
| * const ragFsm = new DyFM_StateMachine<RagIngestState>({ | ||
| * initial: RagIngestState.Idle, | ||
| * transitions: [ | ||
| * { from: RagIngestState.Idle, to: RagIngestState.Ingesting }, | ||
| * { from: RagIngestState.Ingesting, to: RagIngestState.Embedding, | ||
| * guard: ctx => ctx?.pendingChunks === 0 }, | ||
| * { from: RagIngestState.Embedding, to: RagIngestState.Ready, | ||
| * onEnter: () => persistCorpusVersion() }, | ||
| * { from: RagIngestState.Ready, to: RagIngestState.Degraded }, | ||
| * { from: RagIngestState.Degraded, to: RagIngestState.Reingesting }, | ||
| * // Wildcard array: barmely "futasi" allapotbol degraded-re | ||
| * { from: [RagIngestState.Ingesting, RagIngestState.Embedding, | ||
| * RagIngestState.Reingesting], to: RagIngestState.Degraded }, | ||
| * ], | ||
| * persist: async event => await mongoCollection.updateOne( | ||
| * { _id: 'rag-status', stateVersion: event.stateVersion - 1 }, | ||
| * { $set: { state: event.to, stateVersion: event.stateVersion } }, | ||
| * ), | ||
| * }); | ||
| * | ||
| * const result = await ragFsm.transition(RagIngestState.Ingesting); | ||
| * if (result.ok) { console.log('Now in:', result.to, 'v:', result.stateVersion); } | ||
| * ``` | ||
| * | ||
| * **Atomicity:** Az `onEnter` vagy `persist` callback throw-ja eseten a state | ||
| * automatikusan visszaall a from-ra (revert). Ha az `onLeave` throw-ol, a | ||
| * state-valtas mar el sem kezdodik (early-return). | ||
| * | ||
| * **Mutex:** Egy-instance-en belul a `transition()` hivasok sorosak (in-memory | ||
| * Promise-queue). Tobb-instance / tobb-folyamatos race-condition-re az | ||
| * opcionalis `persist` callback `stateVersion`-t hasznalja optimistic-locking-hez. | ||
| */ | ||
| export declare class DyFM_StateMachine<TState extends string, TContext = unknown> { | ||
| /** Aktualis state. */ | ||
| private _state; | ||
| /** Aktualis state-version (0 = initial; minden sikeres atmenet utan +1). */ | ||
| private _stateVersion; | ||
| /** Reactive state-source. */ | ||
| private readonly _state$; | ||
| /** Public reactive state observable. */ | ||
| readonly state$: Observable<TState>; | ||
| /** Reactive event-stream — minden sikeres atmenet utan emittal. */ | ||
| private readonly _events$; | ||
| /** Public reactive event observable. */ | ||
| readonly events$: Observable<DyFM_StateMachineEvent<TState, TContext>>; | ||
| /** Config snapshot. */ | ||
| private readonly _config; | ||
| /** Mutex tail — soros transition() hivasokhoz. */ | ||
| private _mutexTail; | ||
| constructor(config: DyFM_StateMachineConfig<TState, TContext>); | ||
| /** | ||
| * Sync getter — az aktualis state. | ||
| */ | ||
| state(): TState; | ||
| /** | ||
| * Sync getter — az aktualis state-version. | ||
| */ | ||
| stateVersion(): number; | ||
| /** | ||
| * Megnezi hogy van-e olyan transition ami a jelenlegi state-bol a kert | ||
| * `to`-ba vezet, ES (ha van guard) a guard `true`-t adna a kontextusra. | ||
| * | ||
| * NEM fut le onLeave / onEnter / persist callback. | ||
| */ | ||
| canTransition(to: TState, ctx?: TContext): Promise<boolean>; | ||
| /** | ||
| * Atmenet vegrehajtasa a kert `to` state-re. | ||
| * | ||
| * Flow: | ||
| * 1. Mutex-acquire — soros vegrehajtas egy instance-en | ||
| * 2. Transition-keres (`_findTransition`); ha nincs → `invalid-transition` | ||
| * 3. `guard(ctx)` — ha false → `guard-rejected` | ||
| * 4. `onLeave(ctx)` — throw → `on-leave-failed` (state meg NEM valtozott) | ||
| * 5. State + version frissites, `state$.next()` emit | ||
| * 6. `onEnter(ctx)` — throw → revert state + `on-enter-failed` | ||
| * 7. `persist(event)` if configured — throw → revert state + `persist-failed` | ||
| * 8. `events$.next(event)` emit | ||
| * 9. Mutex-release, return ok | ||
| */ | ||
| transition(to: TState, ctx?: TContext): Promise<DyFM_StateMachineTransition_Result<TState>>; | ||
| /** Mutex-protected belso transition logika. */ | ||
| private _transitionInternal; | ||
| /** | ||
| * Lookup: a `transitions[]` listaban olyat keres, aminek `from` matchel a | ||
| * `current`-tel (vagy `'*'` wildcard), ES `to` matchel a `target`-tel. | ||
| * | ||
| * Az elso talalatot adja vissza (a kepes ezert a config-ban a specifikusabb | ||
| * jojjon eloszor a wildcardnel — de a peldakban a wildcard tartalmazza az | ||
| * 'invalidat' is, igy az ordering MUST nem szigoru a tipikus eseteknel). | ||
| */ | ||
| private _findTransition; | ||
| /** | ||
| * Belso helper a sikertelen result epitesere. | ||
| */ | ||
| private _failure; | ||
| } | ||
| //# sourceMappingURL=state-machine.control-model.d.ts.map |
| {"version":3,"file":"state-machine.control-model.d.ts","sourceRoot":"","sources":["../../../../src/_modules/state-machine/_models/state-machine.control-model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,UAAU,EAAW,MAAM,MAAM,CAAC;AAE5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAEzE,OAAO,EAEL,kCAAkC,EACnC,MAAM,qCAAqC,CAAC;AAG7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,qBAAa,iBAAiB,CAAC,MAAM,SAAS,MAAM,EAAE,QAAQ,GAAG,OAAO;IAEtE,sBAAsB;IACtB,OAAO,CAAC,MAAM,CAAS;IAEvB,4EAA4E;IAC5E,OAAO,CAAC,aAAa,CAAa;IAElC,6BAA6B;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IAEpC,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoD;IAE7E,wCAAwC;IACxC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,sBAAsB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAEvE,uBAAuB;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4C;IAEpE,kDAAkD;IAClD,OAAO,CAAC,UAAU,CAAuC;gBAG7C,MAAM,EAAE,uBAAuB,CAAC,MAAM,EAAE,QAAQ,CAAC;IAmB7D;;OAEG;IACH,KAAK,IAAI,MAAM;IAIf;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;;;;OAKG;IACG,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAejE;;;;;;;;;;;;;OAaG;IACG,UAAU,CACd,EAAE,EAAE,MAAM,EACV,GAAG,CAAC,EAAE,QAAQ,GACb,OAAO,CAAC,kCAAkC,CAAC,MAAM,CAAC,CAAC;IAgBtD,+CAA+C;YACjC,mBAAmB;IAsFjC;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAgBvB;;OAEG;IACH,OAAO,CAAC,QAAQ;CAcjB"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.DyFM_StateMachine = void 0; | ||
| const rxjs_1 = require("rxjs"); | ||
| /** | ||
| * Generic finite state machine (FR-006, BL-20260518-004). | ||
| * | ||
| * Konfiguralhato `initial` state-tel + `transitions[]` listaval. Reaktiv | ||
| * (`state$` BehaviorSubject + `events$` Subject), single-instance mutex-szel | ||
| * vedett, opcionalis aszinkron `persist` callback-kel. | ||
| * | ||
| * **Hasznalat (CCAP MP1-D RAG ingest minta):** | ||
| * ```typescript | ||
| * enum RagIngestState { Idle='idle', Ingesting='ingesting', Embedding='embedding', | ||
| * Ready='ready', Degraded='degraded', Reingesting='reingesting' } | ||
| * | ||
| * const ragFsm = new DyFM_StateMachine<RagIngestState>({ | ||
| * initial: RagIngestState.Idle, | ||
| * transitions: [ | ||
| * { from: RagIngestState.Idle, to: RagIngestState.Ingesting }, | ||
| * { from: RagIngestState.Ingesting, to: RagIngestState.Embedding, | ||
| * guard: ctx => ctx?.pendingChunks === 0 }, | ||
| * { from: RagIngestState.Embedding, to: RagIngestState.Ready, | ||
| * onEnter: () => persistCorpusVersion() }, | ||
| * { from: RagIngestState.Ready, to: RagIngestState.Degraded }, | ||
| * { from: RagIngestState.Degraded, to: RagIngestState.Reingesting }, | ||
| * // Wildcard array: barmely "futasi" allapotbol degraded-re | ||
| * { from: [RagIngestState.Ingesting, RagIngestState.Embedding, | ||
| * RagIngestState.Reingesting], to: RagIngestState.Degraded }, | ||
| * ], | ||
| * persist: async event => await mongoCollection.updateOne( | ||
| * { _id: 'rag-status', stateVersion: event.stateVersion - 1 }, | ||
| * { $set: { state: event.to, stateVersion: event.stateVersion } }, | ||
| * ), | ||
| * }); | ||
| * | ||
| * const result = await ragFsm.transition(RagIngestState.Ingesting); | ||
| * if (result.ok) { console.log('Now in:', result.to, 'v:', result.stateVersion); } | ||
| * ``` | ||
| * | ||
| * **Atomicity:** Az `onEnter` vagy `persist` callback throw-ja eseten a state | ||
| * automatikusan visszaall a from-ra (revert). Ha az `onLeave` throw-ol, a | ||
| * state-valtas mar el sem kezdodik (early-return). | ||
| * | ||
| * **Mutex:** Egy-instance-en belul a `transition()` hivasok sorosak (in-memory | ||
| * Promise-queue). Tobb-instance / tobb-folyamatos race-condition-re az | ||
| * opcionalis `persist` callback `stateVersion`-t hasznalja optimistic-locking-hez. | ||
| */ | ||
| class DyFM_StateMachine { | ||
| /** Aktualis state. */ | ||
| _state; | ||
| /** Aktualis state-version (0 = initial; minden sikeres atmenet utan +1). */ | ||
| _stateVersion = 0; | ||
| /** Reactive state-source. */ | ||
| _state$; | ||
| /** Public reactive state observable. */ | ||
| state$; | ||
| /** Reactive event-stream — minden sikeres atmenet utan emittal. */ | ||
| _events$; | ||
| /** Public reactive event observable. */ | ||
| events$; | ||
| /** Config snapshot. */ | ||
| _config; | ||
| /** Mutex tail — soros transition() hivasokhoz. */ | ||
| _mutexTail = Promise.resolve(); | ||
| constructor(config) { | ||
| if (!config || typeof config !== 'object') { | ||
| throw new Error('DyFM_StateMachine: config object required'); | ||
| } | ||
| if (typeof config.initial !== 'string') { | ||
| throw new Error('DyFM_StateMachine: config.initial must be a string state value'); | ||
| } | ||
| if (!Array.isArray(config.transitions) || config.transitions.length === 0) { | ||
| throw new Error('DyFM_StateMachine: config.transitions must be a non-empty array'); | ||
| } | ||
| this._config = config; | ||
| this._state = config.initial; | ||
| this._state$ = new rxjs_1.BehaviorSubject(this._state); | ||
| this.state$ = this._state$.asObservable(); | ||
| this._events$ = new rxjs_1.Subject(); | ||
| this.events$ = this._events$.asObservable(); | ||
| } | ||
| /** | ||
| * Sync getter — az aktualis state. | ||
| */ | ||
| state() { | ||
| return this._state; | ||
| } | ||
| /** | ||
| * Sync getter — az aktualis state-version. | ||
| */ | ||
| stateVersion() { | ||
| return this._stateVersion; | ||
| } | ||
| /** | ||
| * Megnezi hogy van-e olyan transition ami a jelenlegi state-bol a kert | ||
| * `to`-ba vezet, ES (ha van guard) a guard `true`-t adna a kontextusra. | ||
| * | ||
| * NEM fut le onLeave / onEnter / persist callback. | ||
| */ | ||
| async canTransition(to, ctx) { | ||
| const transition = this._findTransition(this._state, to); | ||
| if (!transition) { | ||
| return false; | ||
| } | ||
| if (transition.guard) { | ||
| try { | ||
| const guardResult = await transition.guard(ctx); | ||
| return guardResult === true; | ||
| } | ||
| catch { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| /** | ||
| * Atmenet vegrehajtasa a kert `to` state-re. | ||
| * | ||
| * Flow: | ||
| * 1. Mutex-acquire — soros vegrehajtas egy instance-en | ||
| * 2. Transition-keres (`_findTransition`); ha nincs → `invalid-transition` | ||
| * 3. `guard(ctx)` — ha false → `guard-rejected` | ||
| * 4. `onLeave(ctx)` — throw → `on-leave-failed` (state meg NEM valtozott) | ||
| * 5. State + version frissites, `state$.next()` emit | ||
| * 6. `onEnter(ctx)` — throw → revert state + `on-enter-failed` | ||
| * 7. `persist(event)` if configured — throw → revert state + `persist-failed` | ||
| * 8. `events$.next(event)` emit | ||
| * 9. Mutex-release, return ok | ||
| */ | ||
| async transition(to, ctx) { | ||
| const release = { resolve: () => { } }; | ||
| const myTurn = new Promise((res) => { | ||
| release.resolve = res; | ||
| }); | ||
| const prevTail = this._mutexTail; | ||
| this._mutexTail = myTurn; | ||
| try { | ||
| await prevTail; | ||
| return await this._transitionInternal(to, ctx); | ||
| } | ||
| finally { | ||
| release.resolve(); | ||
| } | ||
| } | ||
| /** Mutex-protected belso transition logika. */ | ||
| async _transitionInternal(to, ctx) { | ||
| const from = this._state; | ||
| // 1. Transition lookup | ||
| const transition = this._findTransition(from, to); | ||
| if (!transition) { | ||
| return this._failure(from, to, 'invalid-transition'); | ||
| } | ||
| // 2. Guard check | ||
| if (transition.guard) { | ||
| let guardResult; | ||
| try { | ||
| guardResult = await transition.guard(ctx); | ||
| } | ||
| catch (err) { | ||
| return this._failure(from, to, 'guard-rejected', err); | ||
| } | ||
| if (guardResult !== true) { | ||
| return this._failure(from, to, 'guard-rejected'); | ||
| } | ||
| } | ||
| // 3. onLeave (pre-change) | ||
| if (transition.onLeave) { | ||
| try { | ||
| await transition.onLeave(ctx); | ||
| } | ||
| catch (err) { | ||
| return this._failure(from, to, 'on-leave-failed', err); | ||
| } | ||
| } | ||
| // 4. State + version transition (the actual change) | ||
| const newVersion = this._stateVersion + 1; | ||
| this._state = to; | ||
| this._stateVersion = newVersion; | ||
| this._state$.next(this._state); | ||
| // 5. onEnter (post-change, pre-persist) | ||
| if (transition.onEnter) { | ||
| try { | ||
| await transition.onEnter(ctx); | ||
| } | ||
| catch (err) { | ||
| // Revert | ||
| this._state = from; | ||
| this._stateVersion = newVersion - 1; | ||
| this._state$.next(this._state); | ||
| return this._failure(from, to, 'on-enter-failed', err); | ||
| } | ||
| } | ||
| // 6. Persist (post-state-change, post-onEnter) | ||
| const event = { | ||
| from: from, | ||
| to: to, | ||
| stateVersion: newVersion, | ||
| context: ctx, | ||
| timestamp: Date.now(), | ||
| }; | ||
| if (this._config.persist) { | ||
| try { | ||
| await this._config.persist(event); | ||
| } | ||
| catch (err) { | ||
| // Revert | ||
| this._state = from; | ||
| this._stateVersion = newVersion - 1; | ||
| this._state$.next(this._state); | ||
| return this._failure(from, to, 'persist-failed', err); | ||
| } | ||
| } | ||
| // 7. Emit event | ||
| this._events$.next(event); | ||
| return { | ||
| ok: true, | ||
| from: from, | ||
| to: to, | ||
| stateVersion: newVersion, | ||
| }; | ||
| } | ||
| /** | ||
| * Lookup: a `transitions[]` listaban olyat keres, aminek `from` matchel a | ||
| * `current`-tel (vagy `'*'` wildcard), ES `to` matchel a `target`-tel. | ||
| * | ||
| * Az elso talalatot adja vissza (a kepes ezert a config-ban a specifikusabb | ||
| * jojjon eloszor a wildcardnel — de a peldakban a wildcard tartalmazza az | ||
| * 'invalidat' is, igy az ordering MUST nem szigoru a tipikus eseteknel). | ||
| */ | ||
| _findTransition(current, target) { | ||
| for (const t of this._config.transitions) { | ||
| if (t.to !== target) { | ||
| continue; | ||
| } | ||
| if (t.from === '*') { | ||
| return t; | ||
| } | ||
| if (Array.isArray(t.from)) { | ||
| if (t.from.includes(current)) { | ||
| return t; | ||
| } | ||
| } | ||
| else if (t.from === current) { | ||
| return t; | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * Belso helper a sikertelen result epitesere. | ||
| */ | ||
| _failure(from, attemptedTo, reason, error) { | ||
| return { | ||
| ok: false, | ||
| reason: reason, | ||
| currentState: this._state, | ||
| attemptedTo: attemptedTo, | ||
| error: error, | ||
| }; | ||
| } | ||
| } | ||
| exports.DyFM_StateMachine = DyFM_StateMachine; | ||
| //# sourceMappingURL=state-machine.control-model.js.map |
| {"version":3,"file":"state-machine.control-model.js","sourceRoot":"","sources":["../../../../src/_modules/state-machine/_models/state-machine.control-model.ts"],"names":[],"mappings":"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"} |
| import { DyFM_Error } from '../../../_models/control-models/error.control-model'; | ||
| /** | ||
| * Failure reason-ok a `DyFM_StateMachineTransition_Result.reason` mezohoz. | ||
| * | ||
| * - `invalid-transition`: nincs olyan transition a config-ban ami a jelenlegi | ||
| * state-bol a kert `to`-ba vezet. | ||
| * - `guard-rejected`: a transition `guard` callback-je `false`-t adott. | ||
| * - `on-leave-failed`: a transition `onLeave` callback-je throw-olt; state NEM valtozott. | ||
| * - `on-enter-failed`: az `onEnter` callback throw-olt az uj state beallitasa utan; state revert-elve a from-ra. | ||
| * - `persist-failed`: a `persist` callback throw-olt; state revert-elve a from-ra. | ||
| */ | ||
| export type DyFM_StateMachineTransition_FailureReason = 'invalid-transition' | 'guard-rejected' | 'on-leave-failed' | 'on-enter-failed' | 'persist-failed'; | ||
| /** | ||
| * Sikeres atmenet eredmenye. | ||
| */ | ||
| export interface DyFM_StateMachineTransition_SuccessResult<TState extends string> { | ||
| ok: true; | ||
| from: TState; | ||
| to: TState; | ||
| /** A frissitett state-version szam (>= 1). */ | ||
| stateVersion: number; | ||
| } | ||
| /** | ||
| * Sikertelen atmenet eredmenye. | ||
| */ | ||
| export interface DyFM_StateMachineTransition_FailureResult<TState extends string> { | ||
| ok: false; | ||
| reason: DyFM_StateMachineTransition_FailureReason; | ||
| /** A jelenlegi state amikor a sikertelenseg torent. */ | ||
| currentState: TState; | ||
| /** A megkiserelt cel-state. */ | ||
| attemptedTo: TState; | ||
| /** Opcionalis hiba ha az atmenet error-ral bukott (pl. on-enter throw, persist throw). */ | ||
| error?: DyFM_Error | Error; | ||
| } | ||
| /** | ||
| * Discriminated union — `ok` flag-gel branchol. | ||
| */ | ||
| export type DyFM_StateMachineTransition_Result<TState extends string> = DyFM_StateMachineTransition_SuccessResult<TState> | DyFM_StateMachineTransition_FailureResult<TState>; | ||
| //# sourceMappingURL=state-transition-result.interface.d.ts.map |
| {"version":3,"file":"state-transition-result.interface.d.ts","sourceRoot":"","sources":["../../../../src/_modules/state-machine/_models/state-transition-result.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,qDAAqD,CAAC;AAEjF;;;;;;;;;GASG;AACH,MAAM,MAAM,yCAAyC,GACjD,oBAAoB,GACpB,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,gBAAgB,CAAC;AAGrB;;GAEG;AACH,MAAM,WAAW,yCAAyC,CAAC,MAAM,SAAS,MAAM;IAC9E,EAAE,EAAE,IAAI,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,YAAY,EAAE,MAAM,CAAC;CACtB;AAGD;;GAEG;AACH,MAAM,WAAW,yCAAyC,CAAC,MAAM,SAAS,MAAM;IAC9E,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,yCAAyC,CAAC;IAClD,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,UAAU,GAAG,KAAK,CAAC;CAC5B;AAGD;;GAEG;AACH,MAAM,MAAM,kCAAkC,CAAC,MAAM,SAAS,MAAM,IAChE,yCAAyC,CAAC,MAAM,CAAC,GACjD,yCAAyC,CAAC,MAAM,CAAC,CAAC"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| //# sourceMappingURL=state-transition-result.interface.js.map |
| {"version":3,"file":"state-transition-result.interface.js","sourceRoot":"","sources":["../../../../src/_modules/state-machine/_models/state-transition-result.interface.ts"],"names":[],"mappings":""} |
| /** | ||
| * Egyetlen atmenet (transition) leiras a `DyFM_StateMachine` transitionMap-jeben. | ||
| * | ||
| * `from` lehet: | ||
| * - egy konkret state (TState) | ||
| * - tobb state array-je | ||
| * - `'*'` wildcard (barmely jelenlegi state) | ||
| * | ||
| * `guard` opcionalis pre-check: ha visszater `false` (vagy resolved-`false` Promise), | ||
| * az atmenet visszautasitva `'guard-rejected'` reason-nel. | ||
| * | ||
| * `onLeave` opcionalis side-effect ami a state-valtas ELOTT fut. Throw-ja | ||
| * `'on-leave-failed'` reason-t valt ki. | ||
| * | ||
| * `onEnter` opcionalis side-effect ami az uj state beallitasa UTAN, de a | ||
| * `persist` callback elott fut. Throw-ja `'on-enter-failed'` reason-t valt ki | ||
| * — ilyenkor a state visszaall (revert). | ||
| */ | ||
| export interface DyFM_StateTransition<TState extends string, TContext = unknown> { | ||
| /** Forras state(ek). Wildcard `'*'` = barmilyen jelenlegi state. */ | ||
| from: TState | TState[] | '*'; | ||
| /** Cel state. */ | ||
| to: TState; | ||
| /** Optional pre-check; ha `false`-t ad, az atmenet elutasitva. */ | ||
| guard?: (ctx?: TContext) => boolean | Promise<boolean>; | ||
| /** Optional side-effect a state-valtas ELOTT. */ | ||
| onLeave?: (ctx?: TContext) => Promise<void>; | ||
| /** Optional side-effect a state-valtas UTAN, a `persist` elott. */ | ||
| onEnter?: (ctx?: TContext) => Promise<void>; | ||
| } | ||
| //# sourceMappingURL=state-transition.interface.d.ts.map |
| {"version":3,"file":"state-transition.interface.d.ts","sourceRoot":"","sources":["../../../../src/_modules/state-machine/_models/state-transition.interface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,oBAAoB,CAAC,MAAM,SAAS,MAAM,EAAE,QAAQ,GAAG,OAAO;IAC7E,oEAAoE;IACpE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,GAAG,CAAC;IAC9B,iBAAiB;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,kEAAkE;IAClE,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD,iDAAiD;IACjD,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,mEAAmE;IACnE,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| //# sourceMappingURL=state-transition.interface.js.map |
| {"version":3,"file":"state-transition.interface.js","sourceRoot":"","sources":["../../../../src/_modules/state-machine/_models/state-transition.interface.ts"],"names":[],"mappings":""} |
| export * from './_models/state-machine.control-model'; | ||
| export * from './_models/state-machine-config.interface'; | ||
| export * from './_models/state-machine-event.interface'; | ||
| export * from './_models/state-transition.interface'; | ||
| export * from './_models/state-transition-result.interface'; | ||
| //# sourceMappingURL=index.d.ts.map |
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/_modules/state-machine/index.ts"],"names":[],"mappings":"AACA,cAAc,uCAAuC,CAAC;AACtD,cAAc,0CAA0C,CAAC;AACzD,cAAc,yCAAyC,CAAC;AACxD,cAAc,sCAAsC,CAAC;AACrD,cAAc,6CAA6C,CAAC"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| const tslib_1 = require("tslib"); | ||
| // MODELS | ||
| tslib_1.__exportStar(require("./_models/state-machine.control-model"), exports); | ||
| tslib_1.__exportStar(require("./_models/state-machine-config.interface"), exports); | ||
| tslib_1.__exportStar(require("./_models/state-machine-event.interface"), exports); | ||
| tslib_1.__exportStar(require("./_models/state-transition.interface"), exports); | ||
| tslib_1.__exportStar(require("./_models/state-transition-result.interface"), exports); | ||
| //# sourceMappingURL=index.js.map |
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/_modules/state-machine/index.ts"],"names":[],"mappings":";;;AAAA,SAAS;AACT,gFAAsD;AACtD,mFAAyD;AACzD,kFAAwD;AACxD,+EAAqD;AACrD,sFAA4D"} |
| import { DyFM_StateMachineEvent } from './state-machine-event.interface'; | ||
| import { DyFM_StateTransition } from './state-transition.interface'; | ||
| /** | ||
| * Config-objektum a `DyFM_StateMachine` konstruktorhoz. | ||
| * | ||
| * - `initial`: induloallapot | ||
| * - `transitions`: a megengedett atmenetek listaja (transitionMap) | ||
| * - `persist`: opcionalis aszinkron callback minden sikeres atmenet utan | ||
| * (NEM a state-valtas elott — a state mar uj-erteku amikor a `persist` | ||
| * fut, igy a callback-be erkezo event tartalmazza a `to`-t es a `stateVersion`-t). | ||
| * Throw-ja kivaltja a `'persist-failed'` reason-t es a state automatikusan | ||
| * visszaall a from-ra (atomicity). | ||
| */ | ||
| export interface DyFM_StateMachineConfig<TState extends string, TContext = unknown> { | ||
| initial: TState; | ||
| transitions: DyFM_StateTransition<TState, TContext>[]; | ||
| persist?: (event: DyFM_StateMachineEvent<TState, TContext>) => Promise<void>; | ||
| } |
| /** | ||
| * Egy sikeres atmenet utan kibocsatott esemeny. | ||
| * | ||
| * A `DyFM_StateMachine.events$` Observable-en at szubszkribalhato. Az opcionalis | ||
| * `persist` callback is ezt az objektumot kapja meg, igy a konzumens (pl. | ||
| * Mongoose ControlModel) eldontheti milyen mezoket frissit. | ||
| * | ||
| * `stateVersion` minden sikeres atmenet utan inkrementalodik (0 -> 1 -> 2 -> ...). | ||
| * Optimistic-concurrency token-kent hasznalhato a `persist` callback-ben | ||
| * (`$inc: { stateVersion: 1 }` + `findOneAndUpdate({ stateVersion: prevVersion })` | ||
| * mintara) hogy a race conditions detektalhatoak legyenek tobb-folyamatos | ||
| * deploy-okban. | ||
| */ | ||
| export interface DyFM_StateMachineEvent<TState extends string, TContext = unknown> { | ||
| /** Az atmenet elotti state. */ | ||
| from: TState; | ||
| /** Az atmenet utani state. */ | ||
| to: TState; | ||
| /** A frissitett state-version szam (>= 1). */ | ||
| stateVersion: number; | ||
| /** Az opcionalis context, ahogy a `transition()`-be erkezett. */ | ||
| context?: TContext; | ||
| /** Az atmenet idobelyege (ms epoch). */ | ||
| timestamp: number; | ||
| } |
| import { firstValueFrom, take, toArray } from 'rxjs'; | ||
| import { DyFM_StateMachine } from './state-machine.control-model'; | ||
| import { DyFM_StateMachineEvent } from './state-machine-event.interface'; | ||
| import { | ||
| DyFM_StateMachineTransition_FailureResult, | ||
| DyFM_StateMachineTransition_Result, | ||
| DyFM_StateMachineTransition_SuccessResult, | ||
| } from './state-transition-result.interface'; | ||
| /** Test state-enum a RAG ingest mintabol. */ | ||
| enum TestRagState { | ||
| Idle = 'idle', | ||
| Ingesting = 'ingesting', | ||
| Embedding = 'embedding', | ||
| Ready = 'ready', | ||
| Degraded = 'degraded', | ||
| Reingesting = 'reingesting', | ||
| } | ||
| /** Default test-config helper. */ | ||
| const buildBasicConfig = () => ({ | ||
| initial: TestRagState.Idle, | ||
| transitions: [ | ||
| { from: TestRagState.Idle, to: TestRagState.Ingesting }, | ||
| { from: TestRagState.Ingesting, to: TestRagState.Embedding }, | ||
| { from: TestRagState.Embedding, to: TestRagState.Ready }, | ||
| { from: TestRagState.Ready, to: TestRagState.Degraded }, | ||
| { from: TestRagState.Degraded, to: TestRagState.Reingesting }, | ||
| { from: TestRagState.Reingesting, to: TestRagState.Ready }, | ||
| // Wildcard array → degraded from any "working" state | ||
| { | ||
| from: [TestRagState.Ingesting, TestRagState.Embedding, TestRagState.Reingesting] as TestRagState[], | ||
| to: TestRagState.Degraded, | ||
| }, | ||
| ], | ||
| }); | ||
| describe('| DyFM_StateMachine | constructor + sync getters', () => { | ||
| it('| initial state is set from config.initial', () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>(buildBasicConfig()); | ||
| expect(sm.state()).toBe(TestRagState.Idle); | ||
| expect(sm.stateVersion()).toBe(0); | ||
| }); | ||
| it('| throws on missing config', () => { | ||
| expect(() => new DyFM_StateMachine<TestRagState>(undefined as any)).toThrowError(/config object required/); | ||
| }); | ||
| it('| throws on missing config.initial', () => { | ||
| expect(() => new DyFM_StateMachine<TestRagState>({ transitions: [] } as any)).toThrowError(/initial must be a string/); | ||
| }); | ||
| it('| throws on empty transitions array', () => { | ||
| expect(() => new DyFM_StateMachine<TestRagState>({ initial: TestRagState.Idle, transitions: [] })).toThrowError(/non-empty array/); | ||
| }); | ||
| }); | ||
| describe('| DyFM_StateMachine | state$ observable', () => { | ||
| it('| emits initial value on subscribe (BehaviorSubject pattern)', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>(buildBasicConfig()); | ||
| const firstValue: TestRagState = await firstValueFrom(sm.state$); | ||
| expect(firstValue).toBe(TestRagState.Idle); | ||
| }); | ||
| it('| emits new state after successful transition', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>(buildBasicConfig()); | ||
| const next$: Promise<TestRagState[]> = firstValueFrom(sm.state$.pipe(take(2), toArray())); | ||
| await sm.transition(TestRagState.Ingesting); | ||
| const emitted: TestRagState[] = await next$; | ||
| expect(emitted).toEqual([TestRagState.Idle, TestRagState.Ingesting]); | ||
| }); | ||
| }); | ||
| describe('| DyFM_StateMachine | canTransition', () => { | ||
| it('| true on valid transition path', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>(buildBasicConfig()); | ||
| expect(await sm.canTransition(TestRagState.Ingesting)).toBe(true); | ||
| }); | ||
| it('| false on invalid transition (no matching from→to)', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>(buildBasicConfig()); | ||
| // Idle → Ready is NOT in the transition map | ||
| expect(await sm.canTransition(TestRagState.Ready)).toBe(false); | ||
| }); | ||
| it('| false when guard returns false', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState, { allow: boolean }>({ | ||
| initial: TestRagState.Idle, | ||
| transitions: [ | ||
| { from: TestRagState.Idle, to: TestRagState.Ingesting, guard: (ctx) => ctx?.allow === true }, | ||
| ], | ||
| }); | ||
| expect(await sm.canTransition(TestRagState.Ingesting, { allow: false })).toBe(false); | ||
| expect(await sm.canTransition(TestRagState.Ingesting, { allow: true })).toBe(true); | ||
| }); | ||
| }); | ||
| describe('| DyFM_StateMachine | transition — happy paths', () => { | ||
| it('| basic ok flow: state changes, stateVersion++, ok result', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>(buildBasicConfig()); | ||
| const result: DyFM_StateMachineTransition_Result<TestRagState> = await sm.transition(TestRagState.Ingesting); | ||
| expect(result.ok).toBe(true); | ||
| if (result.ok) { | ||
| expect(result.from).toBe(TestRagState.Idle); | ||
| expect(result.to).toBe(TestRagState.Ingesting); | ||
| expect(result.stateVersion).toBe(1); | ||
| } | ||
| expect(sm.state()).toBe(TestRagState.Ingesting); | ||
| expect(sm.stateVersion()).toBe(1); | ||
| }); | ||
| it('| full happy-path through ingest → ready', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>(buildBasicConfig()); | ||
| await sm.transition(TestRagState.Ingesting); | ||
| await sm.transition(TestRagState.Embedding); | ||
| const r3: DyFM_StateMachineTransition_Result<TestRagState> = await sm.transition(TestRagState.Ready); | ||
| expect(r3.ok).toBe(true); | ||
| expect(sm.state()).toBe(TestRagState.Ready); | ||
| expect(sm.stateVersion()).toBe(3); | ||
| }); | ||
| it('| wildcard `*` from matches any current state', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>({ | ||
| initial: TestRagState.Idle, | ||
| transitions: [ | ||
| { from: '*', to: TestRagState.Degraded }, | ||
| { from: TestRagState.Idle, to: TestRagState.Ingesting }, | ||
| ], | ||
| }); | ||
| // Idle → Degraded via wildcard | ||
| const r1 = await sm.transition(TestRagState.Degraded); | ||
| expect(r1.ok).toBe(true); | ||
| expect(sm.state()).toBe(TestRagState.Degraded); | ||
| }); | ||
| it('| array-from matches multiple sources', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>(buildBasicConfig()); | ||
| await sm.transition(TestRagState.Ingesting); | ||
| // [Ingesting, Embedding, Reingesting] → Degraded | ||
| const r: DyFM_StateMachineTransition_Result<TestRagState> = await sm.transition(TestRagState.Degraded); | ||
| expect(r.ok).toBe(true); | ||
| expect(sm.state()).toBe(TestRagState.Degraded); | ||
| }); | ||
| }); | ||
| describe('| DyFM_StateMachine | transition — failure paths', () => { | ||
| it('| invalid-transition reason when no matching transition', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>(buildBasicConfig()); | ||
| // Idle → Ready is not in the map | ||
| const result: DyFM_StateMachineTransition_Result<TestRagState> = await sm.transition(TestRagState.Ready); | ||
| expect(result.ok).toBe(false); | ||
| const f: DyFM_StateMachineTransition_FailureResult<TestRagState> = result as DyFM_StateMachineTransition_FailureResult<TestRagState>; | ||
| expect(f.reason).toBe('invalid-transition'); | ||
| expect(f.currentState).toBe(TestRagState.Idle); | ||
| expect(f.attemptedTo).toBe(TestRagState.Ready); | ||
| expect(sm.state()).toBe(TestRagState.Idle); | ||
| expect(sm.stateVersion()).toBe(0); | ||
| }); | ||
| it('| guard-rejected reason when guard returns false; state unchanged', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState, { allow: boolean }>({ | ||
| initial: TestRagState.Idle, | ||
| transitions: [ | ||
| { from: TestRagState.Idle, to: TestRagState.Ingesting, guard: (ctx) => ctx?.allow === true }, | ||
| ], | ||
| }); | ||
| const result = await sm.transition(TestRagState.Ingesting, { allow: false }); | ||
| expect(result.ok).toBe(false); | ||
| const f: DyFM_StateMachineTransition_FailureResult<TestRagState> = result as DyFM_StateMachineTransition_FailureResult<TestRagState>; | ||
| expect(f.reason).toBe('guard-rejected'); | ||
| expect(sm.state()).toBe(TestRagState.Idle); | ||
| expect(sm.stateVersion()).toBe(0); | ||
| }); | ||
| it('| onLeave called BEFORE state change', async () => { | ||
| let stateAtOnLeave: TestRagState | null = null; | ||
| const sm = new DyFM_StateMachine<TestRagState>({ | ||
| initial: TestRagState.Idle, | ||
| transitions: [ | ||
| { | ||
| from: TestRagState.Idle, | ||
| to: TestRagState.Ingesting, | ||
| onLeave: async () => { stateAtOnLeave = sm.state(); }, | ||
| }, | ||
| ], | ||
| }); | ||
| await sm.transition(TestRagState.Ingesting); | ||
| expect(stateAtOnLeave).toBe(TestRagState.Idle); | ||
| expect(sm.state()).toBe(TestRagState.Ingesting); | ||
| }); | ||
| it('| onEnter called AFTER state change, BEFORE persist', async () => { | ||
| let stateAtOnEnter: TestRagState | null = null; | ||
| let stateAtPersist: TestRagState | null = null; | ||
| const sm = new DyFM_StateMachine<TestRagState>({ | ||
| initial: TestRagState.Idle, | ||
| transitions: [ | ||
| { | ||
| from: TestRagState.Idle, | ||
| to: TestRagState.Ingesting, | ||
| onEnter: async () => { stateAtOnEnter = sm.state(); }, | ||
| }, | ||
| ], | ||
| persist: async () => { stateAtPersist = sm.state(); }, | ||
| }); | ||
| await sm.transition(TestRagState.Ingesting); | ||
| expect(stateAtOnEnter).toBe(TestRagState.Ingesting); | ||
| expect(stateAtPersist).toBe(TestRagState.Ingesting); | ||
| }); | ||
| it('| onEnter throw → state reverts + on-enter-failed reason', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>({ | ||
| initial: TestRagState.Idle, | ||
| transitions: [ | ||
| { | ||
| from: TestRagState.Idle, | ||
| to: TestRagState.Ingesting, | ||
| onEnter: async () => { throw new Error('boom-onEnter'); }, | ||
| }, | ||
| ], | ||
| }); | ||
| const result: DyFM_StateMachineTransition_Result<TestRagState> = await sm.transition(TestRagState.Ingesting); | ||
| expect(result.ok).toBe(false); | ||
| const f1: DyFM_StateMachineTransition_FailureResult<TestRagState> = result as DyFM_StateMachineTransition_FailureResult<TestRagState>; | ||
| expect(f1.reason).toBe('on-enter-failed'); | ||
| expect(String((f1.error as Error)?.message ?? '')).toContain('boom-onEnter'); | ||
| expect(sm.state()).toBe(TestRagState.Idle); // reverted | ||
| expect(sm.stateVersion()).toBe(0); // reverted | ||
| }); | ||
| it('| persist throw → state reverts + persist-failed reason', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>({ | ||
| initial: TestRagState.Idle, | ||
| transitions: [ | ||
| { from: TestRagState.Idle, to: TestRagState.Ingesting }, | ||
| ], | ||
| persist: async () => { throw new Error('boom-persist'); }, | ||
| }); | ||
| const result = await sm.transition(TestRagState.Ingesting); | ||
| expect(result.ok).toBe(false); | ||
| const f2: DyFM_StateMachineTransition_FailureResult<TestRagState> = result as DyFM_StateMachineTransition_FailureResult<TestRagState>; | ||
| expect(f2.reason).toBe('persist-failed'); | ||
| expect(String((f2.error as Error)?.message ?? '')).toContain('boom-persist'); | ||
| expect(sm.state()).toBe(TestRagState.Idle); // reverted | ||
| expect(sm.stateVersion()).toBe(0); | ||
| }); | ||
| it('| onLeave throw → state NOT changed + on-leave-failed reason', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>({ | ||
| initial: TestRagState.Idle, | ||
| transitions: [ | ||
| { | ||
| from: TestRagState.Idle, | ||
| to: TestRagState.Ingesting, | ||
| onLeave: async () => { throw new Error('boom-onLeave'); }, | ||
| }, | ||
| ], | ||
| }); | ||
| const result = await sm.transition(TestRagState.Ingesting); | ||
| expect(result.ok).toBe(false); | ||
| const f3: DyFM_StateMachineTransition_FailureResult<TestRagState> = result as DyFM_StateMachineTransition_FailureResult<TestRagState>; | ||
| expect(f3.reason).toBe('on-leave-failed'); | ||
| expect(sm.state()).toBe(TestRagState.Idle); // never changed | ||
| expect(sm.stateVersion()).toBe(0); | ||
| }); | ||
| }); | ||
| describe('| DyFM_StateMachine | events$ + mutex', () => { | ||
| it('| events$ emits on successful transition', async () => { | ||
| const sm = new DyFM_StateMachine<TestRagState>(buildBasicConfig()); | ||
| const event$: Promise<DyFM_StateMachineEvent<TestRagState>> = firstValueFrom(sm.events$); | ||
| await sm.transition(TestRagState.Ingesting); | ||
| const event = await event$; | ||
| expect(event.from).toBe(TestRagState.Idle); | ||
| expect(event.to).toBe(TestRagState.Ingesting); | ||
| expect(event.stateVersion).toBe(1); | ||
| expect(typeof event.timestamp).toBe('number'); | ||
| }); | ||
| it('| concurrent transitions are serialized (mutex)', async () => { | ||
| let inflightCount: number = 0; | ||
| let maxInflight: number = 0; | ||
| const sm = new DyFM_StateMachine<TestRagState>({ | ||
| initial: TestRagState.Idle, | ||
| transitions: [ | ||
| { from: TestRagState.Idle, to: TestRagState.Ingesting, | ||
| onEnter: async (): Promise<void> => { | ||
| inflightCount++; | ||
| if (inflightCount > maxInflight) { maxInflight = inflightCount; } | ||
| await new Promise((r): void => { setTimeout(r, 10); }); | ||
| inflightCount--; | ||
| }, | ||
| }, | ||
| { from: TestRagState.Ingesting, to: TestRagState.Idle }, | ||
| ], | ||
| }); | ||
| // Fire 3 transitions concurrently — they must serialize | ||
| await Promise.all([ | ||
| sm.transition(TestRagState.Ingesting), | ||
| sm.transition(TestRagState.Idle), | ||
| sm.transition(TestRagState.Ingesting), | ||
| ]); | ||
| expect(maxInflight).toBe(1); | ||
| expect(sm.stateVersion()).toBe(3); | ||
| }); | ||
| }); |
| import { BehaviorSubject, Observable, Subject } from 'rxjs'; | ||
| import { DyFM_StateMachineConfig } from './state-machine-config.interface'; | ||
| import { DyFM_StateMachineEvent } from './state-machine-event.interface'; | ||
| import { DyFM_StateTransition } from './state-transition.interface'; | ||
| import { | ||
| DyFM_StateMachineTransition_FailureReason, | ||
| DyFM_StateMachineTransition_Result, | ||
| } from './state-transition-result.interface'; | ||
| /** | ||
| * Generic finite state machine (FR-006, BL-20260518-004). | ||
| * | ||
| * Konfiguralhato `initial` state-tel + `transitions[]` listaval. Reaktiv | ||
| * (`state$` BehaviorSubject + `events$` Subject), single-instance mutex-szel | ||
| * vedett, opcionalis aszinkron `persist` callback-kel. | ||
| * | ||
| * **Hasznalat (CCAP MP1-D RAG ingest minta):** | ||
| * ```typescript | ||
| * enum RagIngestState { Idle='idle', Ingesting='ingesting', Embedding='embedding', | ||
| * Ready='ready', Degraded='degraded', Reingesting='reingesting' } | ||
| * | ||
| * const ragFsm = new DyFM_StateMachine<RagIngestState>({ | ||
| * initial: RagIngestState.Idle, | ||
| * transitions: [ | ||
| * { from: RagIngestState.Idle, to: RagIngestState.Ingesting }, | ||
| * { from: RagIngestState.Ingesting, to: RagIngestState.Embedding, | ||
| * guard: ctx => ctx?.pendingChunks === 0 }, | ||
| * { from: RagIngestState.Embedding, to: RagIngestState.Ready, | ||
| * onEnter: () => persistCorpusVersion() }, | ||
| * { from: RagIngestState.Ready, to: RagIngestState.Degraded }, | ||
| * { from: RagIngestState.Degraded, to: RagIngestState.Reingesting }, | ||
| * // Wildcard array: barmely "futasi" allapotbol degraded-re | ||
| * { from: [RagIngestState.Ingesting, RagIngestState.Embedding, | ||
| * RagIngestState.Reingesting], to: RagIngestState.Degraded }, | ||
| * ], | ||
| * persist: async event => await mongoCollection.updateOne( | ||
| * { _id: 'rag-status', stateVersion: event.stateVersion - 1 }, | ||
| * { $set: { state: event.to, stateVersion: event.stateVersion } }, | ||
| * ), | ||
| * }); | ||
| * | ||
| * const result = await ragFsm.transition(RagIngestState.Ingesting); | ||
| * if (result.ok) { console.log('Now in:', result.to, 'v:', result.stateVersion); } | ||
| * ``` | ||
| * | ||
| * **Atomicity:** Az `onEnter` vagy `persist` callback throw-ja eseten a state | ||
| * automatikusan visszaall a from-ra (revert). Ha az `onLeave` throw-ol, a | ||
| * state-valtas mar el sem kezdodik (early-return). | ||
| * | ||
| * **Mutex:** Egy-instance-en belul a `transition()` hivasok sorosak (in-memory | ||
| * Promise-queue). Tobb-instance / tobb-folyamatos race-condition-re az | ||
| * opcionalis `persist` callback `stateVersion`-t hasznalja optimistic-locking-hez. | ||
| */ | ||
| export class DyFM_StateMachine<TState extends string, TContext = unknown> { | ||
| /** Aktualis state. */ | ||
| private _state: TState; | ||
| /** Aktualis state-version (0 = initial; minden sikeres atmenet utan +1). */ | ||
| private _stateVersion: number = 0; | ||
| /** Reactive state-source. */ | ||
| private readonly _state$: BehaviorSubject<TState>; | ||
| /** Public reactive state observable. */ | ||
| readonly state$: Observable<TState>; | ||
| /** Reactive event-stream — minden sikeres atmenet utan emittal. */ | ||
| private readonly _events$: Subject<DyFM_StateMachineEvent<TState, TContext>>; | ||
| /** Public reactive event observable. */ | ||
| readonly events$: Observable<DyFM_StateMachineEvent<TState, TContext>>; | ||
| /** Config snapshot. */ | ||
| private readonly _config: DyFM_StateMachineConfig<TState, TContext>; | ||
| /** Mutex tail — soros transition() hivasokhoz. */ | ||
| private _mutexTail: Promise<unknown> = Promise.resolve(); | ||
| constructor(config: DyFM_StateMachineConfig<TState, TContext>) { | ||
| if (!config || typeof config !== 'object') { | ||
| throw new Error('DyFM_StateMachine: config object required'); | ||
| } | ||
| if (typeof config.initial !== 'string') { | ||
| throw new Error('DyFM_StateMachine: config.initial must be a string state value'); | ||
| } | ||
| if (!Array.isArray(config.transitions) || config.transitions.length === 0) { | ||
| throw new Error('DyFM_StateMachine: config.transitions must be a non-empty array'); | ||
| } | ||
| this._config = config; | ||
| this._state = config.initial; | ||
| this._state$ = new BehaviorSubject<TState>(this._state); | ||
| this.state$ = this._state$.asObservable(); | ||
| this._events$ = new Subject<DyFM_StateMachineEvent<TState, TContext>>(); | ||
| this.events$ = this._events$.asObservable(); | ||
| } | ||
| /** | ||
| * Sync getter — az aktualis state. | ||
| */ | ||
| state(): TState { | ||
| return this._state; | ||
| } | ||
| /** | ||
| * Sync getter — az aktualis state-version. | ||
| */ | ||
| stateVersion(): number { | ||
| return this._stateVersion; | ||
| } | ||
| /** | ||
| * Megnezi hogy van-e olyan transition ami a jelenlegi state-bol a kert | ||
| * `to`-ba vezet, ES (ha van guard) a guard `true`-t adna a kontextusra. | ||
| * | ||
| * NEM fut le onLeave / onEnter / persist callback. | ||
| */ | ||
| async canTransition(to: TState, ctx?: TContext): Promise<boolean> { | ||
| const transition: DyFM_StateTransition<TState, TContext> | undefined = | ||
| this._findTransition(this._state, to); | ||
| if (!transition) { return false; } | ||
| if (transition.guard) { | ||
| try { | ||
| const guardResult: boolean = await transition.guard(ctx); | ||
| return guardResult === true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| /** | ||
| * Atmenet vegrehajtasa a kert `to` state-re. | ||
| * | ||
| * Flow: | ||
| * 1. Mutex-acquire — soros vegrehajtas egy instance-en | ||
| * 2. Transition-keres (`_findTransition`); ha nincs → `invalid-transition` | ||
| * 3. `guard(ctx)` — ha false → `guard-rejected` | ||
| * 4. `onLeave(ctx)` — throw → `on-leave-failed` (state meg NEM valtozott) | ||
| * 5. State + version frissites, `state$.next()` emit | ||
| * 6. `onEnter(ctx)` — throw → revert state + `on-enter-failed` | ||
| * 7. `persist(event)` if configured — throw → revert state + `persist-failed` | ||
| * 8. `events$.next(event)` emit | ||
| * 9. Mutex-release, return ok | ||
| */ | ||
| async transition( | ||
| to: TState, | ||
| ctx?: TContext, | ||
| ): Promise<DyFM_StateMachineTransition_Result<TState>> { | ||
| const release: { resolve: () => void } = { resolve: (): void => { /* noop */ } }; | ||
| const myTurn: Promise<void> = new Promise<void>((res: () => void): void => { | ||
| release.resolve = res; | ||
| }); | ||
| const prevTail: Promise<unknown> = this._mutexTail; | ||
| this._mutexTail = myTurn; | ||
| try { | ||
| await prevTail; | ||
| return await this._transitionInternal(to, ctx); | ||
| } finally { | ||
| release.resolve(); | ||
| } | ||
| } | ||
| /** Mutex-protected belso transition logika. */ | ||
| private async _transitionInternal( | ||
| to: TState, | ||
| ctx?: TContext, | ||
| ): Promise<DyFM_StateMachineTransition_Result<TState>> { | ||
| const from: TState = this._state; | ||
| // 1. Transition lookup | ||
| const transition: DyFM_StateTransition<TState, TContext> | undefined = | ||
| this._findTransition(from, to); | ||
| if (!transition) { | ||
| return this._failure(from, to, 'invalid-transition'); | ||
| } | ||
| // 2. Guard check | ||
| if (transition.guard) { | ||
| let guardResult: boolean; | ||
| try { | ||
| guardResult = await transition.guard(ctx); | ||
| } catch (err: unknown) { | ||
| return this._failure(from, to, 'guard-rejected', err as Error); | ||
| } | ||
| if (guardResult !== true) { | ||
| return this._failure(from, to, 'guard-rejected'); | ||
| } | ||
| } | ||
| // 3. onLeave (pre-change) | ||
| if (transition.onLeave) { | ||
| try { | ||
| await transition.onLeave(ctx); | ||
| } catch (err: unknown) { | ||
| return this._failure(from, to, 'on-leave-failed', err as Error); | ||
| } | ||
| } | ||
| // 4. State + version transition (the actual change) | ||
| const newVersion: number = this._stateVersion + 1; | ||
| this._state = to; | ||
| this._stateVersion = newVersion; | ||
| this._state$.next(this._state); | ||
| // 5. onEnter (post-change, pre-persist) | ||
| if (transition.onEnter) { | ||
| try { | ||
| await transition.onEnter(ctx); | ||
| } catch (err: unknown) { | ||
| // Revert | ||
| this._state = from; | ||
| this._stateVersion = newVersion - 1; | ||
| this._state$.next(this._state); | ||
| return this._failure(from, to, 'on-enter-failed', err as Error); | ||
| } | ||
| } | ||
| // 6. Persist (post-state-change, post-onEnter) | ||
| const event: DyFM_StateMachineEvent<TState, TContext> = { | ||
| from: from, | ||
| to: to, | ||
| stateVersion: newVersion, | ||
| context: ctx, | ||
| timestamp: Date.now(), | ||
| }; | ||
| if (this._config.persist) { | ||
| try { | ||
| await this._config.persist(event); | ||
| } catch (err: unknown) { | ||
| // Revert | ||
| this._state = from; | ||
| this._stateVersion = newVersion - 1; | ||
| this._state$.next(this._state); | ||
| return this._failure(from, to, 'persist-failed', err as Error); | ||
| } | ||
| } | ||
| // 7. Emit event | ||
| this._events$.next(event); | ||
| return { | ||
| ok: true, | ||
| from: from, | ||
| to: to, | ||
| stateVersion: newVersion, | ||
| }; | ||
| } | ||
| /** | ||
| * Lookup: a `transitions[]` listaban olyat keres, aminek `from` matchel a | ||
| * `current`-tel (vagy `'*'` wildcard), ES `to` matchel a `target`-tel. | ||
| * | ||
| * Az elso talalatot adja vissza (a kepes ezert a config-ban a specifikusabb | ||
| * jojjon eloszor a wildcardnel — de a peldakban a wildcard tartalmazza az | ||
| * 'invalidat' is, igy az ordering MUST nem szigoru a tipikus eseteknel). | ||
| */ | ||
| private _findTransition( | ||
| current: TState, | ||
| target: TState, | ||
| ): DyFM_StateTransition<TState, TContext> | undefined { | ||
| for (const t of this._config.transitions) { | ||
| if (t.to !== target) { continue; } | ||
| if (t.from === '*') { return t; } | ||
| if (Array.isArray(t.from)) { | ||
| if (t.from.includes(current)) { return t; } | ||
| } else if (t.from === current) { | ||
| return t; | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * Belso helper a sikertelen result epitesere. | ||
| */ | ||
| private _failure( | ||
| from: TState, | ||
| attemptedTo: TState, | ||
| reason: DyFM_StateMachineTransition_FailureReason, | ||
| error?: Error, | ||
| ): DyFM_StateMachineTransition_Result<TState> { | ||
| return { | ||
| ok: false, | ||
| reason: reason, | ||
| currentState: this._state, | ||
| attemptedTo: attemptedTo, | ||
| error: error, | ||
| }; | ||
| } | ||
| } |
| import { DyFM_Error } from '../../../_models/control-models/error.control-model'; | ||
| /** | ||
| * Failure reason-ok a `DyFM_StateMachineTransition_Result.reason` mezohoz. | ||
| * | ||
| * - `invalid-transition`: nincs olyan transition a config-ban ami a jelenlegi | ||
| * state-bol a kert `to`-ba vezet. | ||
| * - `guard-rejected`: a transition `guard` callback-je `false`-t adott. | ||
| * - `on-leave-failed`: a transition `onLeave` callback-je throw-olt; state NEM valtozott. | ||
| * - `on-enter-failed`: az `onEnter` callback throw-olt az uj state beallitasa utan; state revert-elve a from-ra. | ||
| * - `persist-failed`: a `persist` callback throw-olt; state revert-elve a from-ra. | ||
| */ | ||
| export type DyFM_StateMachineTransition_FailureReason = | ||
| | 'invalid-transition' | ||
| | 'guard-rejected' | ||
| | 'on-leave-failed' | ||
| | 'on-enter-failed' | ||
| | 'persist-failed'; | ||
| /** | ||
| * Sikeres atmenet eredmenye. | ||
| */ | ||
| export interface DyFM_StateMachineTransition_SuccessResult<TState extends string> { | ||
| ok: true; | ||
| from: TState; | ||
| to: TState; | ||
| /** A frissitett state-version szam (>= 1). */ | ||
| stateVersion: number; | ||
| } | ||
| /** | ||
| * Sikertelen atmenet eredmenye. | ||
| */ | ||
| export interface DyFM_StateMachineTransition_FailureResult<TState extends string> { | ||
| ok: false; | ||
| reason: DyFM_StateMachineTransition_FailureReason; | ||
| /** A jelenlegi state amikor a sikertelenseg torent. */ | ||
| currentState: TState; | ||
| /** A megkiserelt cel-state. */ | ||
| attemptedTo: TState; | ||
| /** Opcionalis hiba ha az atmenet error-ral bukott (pl. on-enter throw, persist throw). */ | ||
| error?: DyFM_Error | Error; | ||
| } | ||
| /** | ||
| * Discriminated union — `ok` flag-gel branchol. | ||
| */ | ||
| export type DyFM_StateMachineTransition_Result<TState extends string> = | ||
| | DyFM_StateMachineTransition_SuccessResult<TState> | ||
| | DyFM_StateMachineTransition_FailureResult<TState>; |
| /** | ||
| * Egyetlen atmenet (transition) leiras a `DyFM_StateMachine` transitionMap-jeben. | ||
| * | ||
| * `from` lehet: | ||
| * - egy konkret state (TState) | ||
| * - tobb state array-je | ||
| * - `'*'` wildcard (barmely jelenlegi state) | ||
| * | ||
| * `guard` opcionalis pre-check: ha visszater `false` (vagy resolved-`false` Promise), | ||
| * az atmenet visszautasitva `'guard-rejected'` reason-nel. | ||
| * | ||
| * `onLeave` opcionalis side-effect ami a state-valtas ELOTT fut. Throw-ja | ||
| * `'on-leave-failed'` reason-t valt ki. | ||
| * | ||
| * `onEnter` opcionalis side-effect ami az uj state beallitasa UTAN, de a | ||
| * `persist` callback elott fut. Throw-ja `'on-enter-failed'` reason-t valt ki | ||
| * — ilyenkor a state visszaall (revert). | ||
| */ | ||
| export interface DyFM_StateTransition<TState extends string, TContext = unknown> { | ||
| /** Forras state(ek). Wildcard `'*'` = barmilyen jelenlegi state. */ | ||
| from: TState | TState[] | '*'; | ||
| /** Cel state. */ | ||
| to: TState; | ||
| /** Optional pre-check; ha `false`-t ad, az atmenet elutasitva. */ | ||
| guard?: (ctx?: TContext) => boolean | Promise<boolean>; | ||
| /** Optional side-effect a state-valtas ELOTT. */ | ||
| onLeave?: (ctx?: TContext) => Promise<void>; | ||
| /** Optional side-effect a state-valtas UTAN, a `persist` elott. */ | ||
| onEnter?: (ctx?: TContext) => Promise<void>; | ||
| } |
| // MODELS | ||
| export * from './_models/state-machine.control-model'; | ||
| export * from './_models/state-machine-config.interface'; | ||
| export * from './_models/state-machine-event.interface'; | ||
| export * from './_models/state-transition.interface'; | ||
| export * from './_models/state-transition-result.interface'; |
+10
-1
| { | ||
| "name": "@futdevpro/fsm-dynamo", | ||
| "version": "01.15.11", | ||
| "version": "01.15.12", | ||
| "description": "Full Stack Model Collection for Dynamic (NodeJS-Typescript) Framework called Dynamo, by Future Development Ltd.", | ||
@@ -174,2 +174,8 @@ "DyBu_settings": { | ||
| "typings": "./build/_modules/data-handler/index.d.ts" | ||
| }, | ||
| "./state-machine": { | ||
| "default": "./build/_modules/state-machine/index.js", | ||
| "module": "./build/_modules/state-machine/index.js", | ||
| "types": "./build/_modules/state-machine/index.d.ts", | ||
| "typings": "./build/_modules/state-machine/index.d.ts" | ||
| } | ||
@@ -235,2 +241,5 @@ }, | ||
| "build/_modules/data-handler/index.d.ts" | ||
| ], | ||
| "state-machine": [ | ||
| "build/_modules/state-machine/index.d.ts" | ||
| ] | ||
@@ -237,0 +246,0 @@ } |
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
3655973
1.67%1380
2.3%117807
1.01%