Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@futdevpro/fsm-dynamo

Package Overview
Dependencies
Maintainers
3
Versions
305
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@futdevpro/fsm-dynamo - npm Package Compare versions

Comparing version
1.15.11
to
1.15.12
+19
build/_modules/sta...els/state-machine-config.interface.d.ts
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 @@ }