@unlaxer/tramli 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,7 +68,10 @@ export declare class FromBuilder<S extends string> {
68
68
  private readonly fromState;
69
69
  constructor(builder: Builder<S>, fromState: S);
70
70
  auto(to: S, processor: StateProcessor<S>): Builder<S>;
71
- external(to: S, guard: TransitionGuard<S>, processor?: StateProcessor<S>): Builder<S>;
71
+ external(to: S, guard: TransitionGuard<S>, processorOrOptions?: StateProcessor<S> | {
72
+ processor?: StateProcessor<S>;
73
+ timeout?: number;
74
+ }): Builder<S>;
72
75
  branch(branch: BranchProcessor<S>): BranchBuilder<S>;
73
76
  subFlow(subFlowDef: FlowDefinition<any>): SubFlowBuilder<S>;
74
77
  }
@@ -434,10 +434,20 @@ class FromBuilder {
434
434
  });
435
435
  return this.builder;
436
436
  }
437
- external(to, guard, processor) {
437
+ external(to, guard, processorOrOptions) {
438
+ let processor;
439
+ let timeout;
440
+ if (processorOrOptions && 'process' in processorOrOptions) {
441
+ processor = processorOrOptions;
442
+ }
443
+ else if (processorOrOptions) {
444
+ const opts = processorOrOptions;
445
+ processor = opts.processor;
446
+ timeout = opts.timeout;
447
+ }
438
448
  this.builder.addTransition({
439
449
  from: this.fromState, to, type: 'external', processor,
440
- guard, branch: undefined, branchTargets: new Map(),
450
+ guard, branch: undefined, branchTargets: new Map(), timeout,
441
451
  });
442
452
  return this.builder;
443
453
  }
@@ -1,6 +1,8 @@
1
1
  import type { FlowDefinition } from './flow-definition.js';
2
2
  import { FlowInstance } from './flow-instance.js';
3
3
  import type { InMemoryFlowStore } from './in-memory-flow-store.js';
4
+ /** Default max auto-chain depth. Override via constructor options. */
5
+ export declare const DEFAULT_MAX_CHAIN_DEPTH = 10;
4
6
  /** Log entry types for tramli's pluggable logger API. */
5
7
  export interface TransitionLogEntry {
6
8
  flowId: string;
@@ -24,11 +26,13 @@ export interface ErrorLogEntry {
24
26
  export declare class FlowEngine {
25
27
  private readonly store;
26
28
  private readonly strictMode;
29
+ private readonly maxChainDepth;
27
30
  private transitionLogger?;
28
31
  private stateLogger?;
29
32
  private errorLogger?;
30
33
  constructor(store: InMemoryFlowStore, options?: {
31
34
  strictMode?: boolean;
35
+ maxChainDepth?: number;
32
36
  });
33
37
  setTransitionLogger(logger: ((entry: TransitionLogEntry) => void) | null): void;
34
38
  setStateLogger(logger: ((entry: StateLogEntry) => void) | null): void;
@@ -1,19 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FlowEngine = void 0;
3
+ exports.FlowEngine = exports.DEFAULT_MAX_CHAIN_DEPTH = void 0;
4
4
  const flow_context_js_1 = require("./flow-context");
5
5
  const flow_instance_js_1 = require("./flow-instance");
6
6
  const flow_error_js_1 = require("./flow-error");
7
- const MAX_CHAIN_DEPTH = 10;
7
+ /** Default max auto-chain depth. Override via constructor options. */
8
+ exports.DEFAULT_MAX_CHAIN_DEPTH = 10;
8
9
  class FlowEngine {
9
10
  store;
10
11
  strictMode;
12
+ maxChainDepth;
11
13
  transitionLogger;
12
14
  stateLogger;
13
15
  errorLogger;
14
16
  constructor(store, options) {
15
17
  this.store = store;
16
18
  this.strictMode = options?.strictMode ?? false;
19
+ this.maxChainDepth = options?.maxChainDepth ?? exports.DEFAULT_MAX_CHAIN_DEPTH;
17
20
  }
18
21
  setTransitionLogger(logger) {
19
22
  this.transitionLogger = logger ?? undefined;
@@ -65,6 +68,15 @@ class FlowEngine {
65
68
  const transition = definition.externalFrom(currentState);
66
69
  if (!transition)
67
70
  throw flow_error_js_1.FlowError.invalidTransition(currentState, currentState);
71
+ // Per-state timeout check
72
+ if (transition.timeout != null) {
73
+ const deadline = new Date(flow.stateEnteredAt.getTime() + transition.timeout);
74
+ if (new Date() > deadline) {
75
+ flow.complete('EXPIRED');
76
+ this.store.save(flow);
77
+ return flow;
78
+ }
79
+ }
68
80
  const guard = transition.guard;
69
81
  if (guard) {
70
82
  const output = await guard.validate(flow.context);
@@ -116,7 +128,7 @@ class FlowEngine {
116
128
  }
117
129
  async executeAutoChain(flow) {
118
130
  let depth = 0;
119
- while (depth < MAX_CHAIN_DEPTH) {
131
+ while (depth < this.maxChainDepth) {
120
132
  const current = flow.currentState;
121
133
  if (flow.definition.stateConfig[current].terminal) {
122
134
  flow.complete(current);
@@ -168,7 +180,7 @@ class FlowEngine {
168
180
  }
169
181
  depth++;
170
182
  }
171
- if (depth >= MAX_CHAIN_DEPTH)
183
+ if (depth >= this.maxChainDepth)
172
184
  throw flow_error_js_1.FlowError.maxChainDepth();
173
185
  }
174
186
  async executeSubFlow(parentFlow, subFlowTransition) {
@@ -1,10 +1,15 @@
1
+ export type FlowErrorType = 'BUSINESS' | 'SYSTEM' | 'RETRYABLE' | 'FATAL';
1
2
  export declare class FlowError extends Error {
2
3
  readonly code: string;
4
+ /** Error type classification for retry/recovery strategy. */
5
+ errorType?: FlowErrorType;
3
6
  /** Types that were available in context when the error occurred. */
4
7
  availableTypes?: Set<string>;
5
8
  /** Types that were expected but missing (if applicable). */
6
9
  missingTypes?: Set<string>;
7
10
  constructor(code: string, message: string);
11
+ /** Attach error type classification. */
12
+ withErrorType(type: FlowErrorType): this;
8
13
  /** Attach context snapshot to this error. */
9
14
  withContextSnapshot(available: Set<string>, missing: Set<string>): this;
10
15
  static invalidTransition(from: string, to: string): FlowError;
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FlowError = void 0;
4
4
  class FlowError extends Error {
5
5
  code;
6
+ /** Error type classification for retry/recovery strategy. */
7
+ errorType;
6
8
  /** Types that were available in context when the error occurred. */
7
9
  availableTypes;
8
10
  /** Types that were expected but missing (if applicable). */
@@ -12,6 +14,11 @@ class FlowError extends Error {
12
14
  this.code = code;
13
15
  this.name = 'FlowError';
14
16
  }
17
+ /** Attach error type classification. */
18
+ withErrorType(type) {
19
+ this.errorType = type;
20
+ return this;
21
+ }
15
22
  /** Attach context snapshot to this error. */
16
23
  withContextSnapshot(available, missing) {
17
24
  this.availableTypes = available;
@@ -13,6 +13,7 @@ export declare class FlowInstance<S extends string> {
13
13
  private _exitState;
14
14
  private _activeSubFlow;
15
15
  private _lastError;
16
+ private _stateEnteredAt;
16
17
  constructor(id: string, sessionId: string, definition: FlowDefinition<S>, context: FlowContext, currentState: S, expiresAt: Date);
17
18
  /**
18
19
  * Restore a FlowInstance from persisted state.
@@ -39,6 +40,7 @@ export declare class FlowInstance<S extends string> {
39
40
  waitingFor(): string[];
40
41
  /** Return a copy with the given version. For FlowStore optimistic locking. */
41
42
  withVersion(newVersion: number): FlowInstance<S>;
43
+ get stateEnteredAt(): Date;
42
44
  /** @internal */ transitionTo(state: S): void;
43
45
  /** @internal */ incrementGuardFailure(): void;
44
46
  /** @internal */ complete(exitState: string): void;
@@ -14,6 +14,7 @@ class FlowInstance {
14
14
  _exitState;
15
15
  _activeSubFlow = null;
16
16
  _lastError = null;
17
+ _stateEnteredAt = new Date();
17
18
  constructor(id, sessionId, definition, context, currentState, expiresAt) {
18
19
  this.id = id;
19
20
  this.sessionId = sessionId;
@@ -98,7 +99,8 @@ class FlowInstance {
98
99
  copy.setActiveSubFlow(this._activeSubFlow);
99
100
  return copy;
100
101
  }
101
- /** @internal */ transitionTo(state) { this._currentState = state; }
102
+ get stateEnteredAt() { return this._stateEnteredAt; }
103
+ /** @internal */ transitionTo(state) { this._currentState = state; this._stateEnteredAt = new Date(); }
102
104
  /** @internal */ incrementGuardFailure() { this._guardFailureCount++; }
103
105
  /** @internal */ complete(exitState) { this._exitState = exitState; }
104
106
  /** @internal */ setVersion(version) { this._version = version; }
@@ -28,6 +28,8 @@ export interface Transition<S extends string> {
28
28
  branchTargets: Map<string, S>;
29
29
  subFlowDefinition?: import('./flow-definition.js').FlowDefinition<any>;
30
30
  exitMappings?: Map<string, S>;
31
+ /** Per-state timeout in milliseconds. If set, resumeAndExecute checks this before guard. */
32
+ timeout?: number;
31
33
  }
32
34
  /**
33
35
  * Processes a state transition.
@@ -68,7 +68,10 @@ export declare class FromBuilder<S extends string> {
68
68
  private readonly fromState;
69
69
  constructor(builder: Builder<S>, fromState: S);
70
70
  auto(to: S, processor: StateProcessor<S>): Builder<S>;
71
- external(to: S, guard: TransitionGuard<S>, processor?: StateProcessor<S>): Builder<S>;
71
+ external(to: S, guard: TransitionGuard<S>, processorOrOptions?: StateProcessor<S> | {
72
+ processor?: StateProcessor<S>;
73
+ timeout?: number;
74
+ }): Builder<S>;
72
75
  branch(branch: BranchProcessor<S>): BranchBuilder<S>;
73
76
  subFlow(subFlowDef: FlowDefinition<any>): SubFlowBuilder<S>;
74
77
  }
@@ -429,10 +429,20 @@ export class FromBuilder {
429
429
  });
430
430
  return this.builder;
431
431
  }
432
- external(to, guard, processor) {
432
+ external(to, guard, processorOrOptions) {
433
+ let processor;
434
+ let timeout;
435
+ if (processorOrOptions && 'process' in processorOrOptions) {
436
+ processor = processorOrOptions;
437
+ }
438
+ else if (processorOrOptions) {
439
+ const opts = processorOrOptions;
440
+ processor = opts.processor;
441
+ timeout = opts.timeout;
442
+ }
433
443
  this.builder.addTransition({
434
444
  from: this.fromState, to, type: 'external', processor,
435
- guard, branch: undefined, branchTargets: new Map(),
445
+ guard, branch: undefined, branchTargets: new Map(), timeout,
436
446
  });
437
447
  return this.builder;
438
448
  }
@@ -1,6 +1,8 @@
1
1
  import type { FlowDefinition } from './flow-definition.js';
2
2
  import { FlowInstance } from './flow-instance.js';
3
3
  import type { InMemoryFlowStore } from './in-memory-flow-store.js';
4
+ /** Default max auto-chain depth. Override via constructor options. */
5
+ export declare const DEFAULT_MAX_CHAIN_DEPTH = 10;
4
6
  /** Log entry types for tramli's pluggable logger API. */
5
7
  export interface TransitionLogEntry {
6
8
  flowId: string;
@@ -24,11 +26,13 @@ export interface ErrorLogEntry {
24
26
  export declare class FlowEngine {
25
27
  private readonly store;
26
28
  private readonly strictMode;
29
+ private readonly maxChainDepth;
27
30
  private transitionLogger?;
28
31
  private stateLogger?;
29
32
  private errorLogger?;
30
33
  constructor(store: InMemoryFlowStore, options?: {
31
34
  strictMode?: boolean;
35
+ maxChainDepth?: number;
32
36
  });
33
37
  setTransitionLogger(logger: ((entry: TransitionLogEntry) => void) | null): void;
34
38
  setStateLogger(logger: ((entry: StateLogEntry) => void) | null): void;
@@ -1,16 +1,19 @@
1
1
  import { FlowContext } from './flow-context.js';
2
2
  import { FlowInstance } from './flow-instance.js';
3
3
  import { FlowError } from './flow-error.js';
4
- const MAX_CHAIN_DEPTH = 10;
4
+ /** Default max auto-chain depth. Override via constructor options. */
5
+ export const DEFAULT_MAX_CHAIN_DEPTH = 10;
5
6
  export class FlowEngine {
6
7
  store;
7
8
  strictMode;
9
+ maxChainDepth;
8
10
  transitionLogger;
9
11
  stateLogger;
10
12
  errorLogger;
11
13
  constructor(store, options) {
12
14
  this.store = store;
13
15
  this.strictMode = options?.strictMode ?? false;
16
+ this.maxChainDepth = options?.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH;
14
17
  }
15
18
  setTransitionLogger(logger) {
16
19
  this.transitionLogger = logger ?? undefined;
@@ -62,6 +65,15 @@ export class FlowEngine {
62
65
  const transition = definition.externalFrom(currentState);
63
66
  if (!transition)
64
67
  throw FlowError.invalidTransition(currentState, currentState);
68
+ // Per-state timeout check
69
+ if (transition.timeout != null) {
70
+ const deadline = new Date(flow.stateEnteredAt.getTime() + transition.timeout);
71
+ if (new Date() > deadline) {
72
+ flow.complete('EXPIRED');
73
+ this.store.save(flow);
74
+ return flow;
75
+ }
76
+ }
65
77
  const guard = transition.guard;
66
78
  if (guard) {
67
79
  const output = await guard.validate(flow.context);
@@ -113,7 +125,7 @@ export class FlowEngine {
113
125
  }
114
126
  async executeAutoChain(flow) {
115
127
  let depth = 0;
116
- while (depth < MAX_CHAIN_DEPTH) {
128
+ while (depth < this.maxChainDepth) {
117
129
  const current = flow.currentState;
118
130
  if (flow.definition.stateConfig[current].terminal) {
119
131
  flow.complete(current);
@@ -165,7 +177,7 @@ export class FlowEngine {
165
177
  }
166
178
  depth++;
167
179
  }
168
- if (depth >= MAX_CHAIN_DEPTH)
180
+ if (depth >= this.maxChainDepth)
169
181
  throw FlowError.maxChainDepth();
170
182
  }
171
183
  async executeSubFlow(parentFlow, subFlowTransition) {
@@ -1,10 +1,15 @@
1
+ export type FlowErrorType = 'BUSINESS' | 'SYSTEM' | 'RETRYABLE' | 'FATAL';
1
2
  export declare class FlowError extends Error {
2
3
  readonly code: string;
4
+ /** Error type classification for retry/recovery strategy. */
5
+ errorType?: FlowErrorType;
3
6
  /** Types that were available in context when the error occurred. */
4
7
  availableTypes?: Set<string>;
5
8
  /** Types that were expected but missing (if applicable). */
6
9
  missingTypes?: Set<string>;
7
10
  constructor(code: string, message: string);
11
+ /** Attach error type classification. */
12
+ withErrorType(type: FlowErrorType): this;
8
13
  /** Attach context snapshot to this error. */
9
14
  withContextSnapshot(available: Set<string>, missing: Set<string>): this;
10
15
  static invalidTransition(from: string, to: string): FlowError;
@@ -1,5 +1,7 @@
1
1
  export class FlowError extends Error {
2
2
  code;
3
+ /** Error type classification for retry/recovery strategy. */
4
+ errorType;
3
5
  /** Types that were available in context when the error occurred. */
4
6
  availableTypes;
5
7
  /** Types that were expected but missing (if applicable). */
@@ -9,6 +11,11 @@ export class FlowError extends Error {
9
11
  this.code = code;
10
12
  this.name = 'FlowError';
11
13
  }
14
+ /** Attach error type classification. */
15
+ withErrorType(type) {
16
+ this.errorType = type;
17
+ return this;
18
+ }
12
19
  /** Attach context snapshot to this error. */
13
20
  withContextSnapshot(available, missing) {
14
21
  this.availableTypes = available;
@@ -13,6 +13,7 @@ export declare class FlowInstance<S extends string> {
13
13
  private _exitState;
14
14
  private _activeSubFlow;
15
15
  private _lastError;
16
+ private _stateEnteredAt;
16
17
  constructor(id: string, sessionId: string, definition: FlowDefinition<S>, context: FlowContext, currentState: S, expiresAt: Date);
17
18
  /**
18
19
  * Restore a FlowInstance from persisted state.
@@ -39,6 +40,7 @@ export declare class FlowInstance<S extends string> {
39
40
  waitingFor(): string[];
40
41
  /** Return a copy with the given version. For FlowStore optimistic locking. */
41
42
  withVersion(newVersion: number): FlowInstance<S>;
43
+ get stateEnteredAt(): Date;
42
44
  /** @internal */ transitionTo(state: S): void;
43
45
  /** @internal */ incrementGuardFailure(): void;
44
46
  /** @internal */ complete(exitState: string): void;
@@ -11,6 +11,7 @@ export class FlowInstance {
11
11
  _exitState;
12
12
  _activeSubFlow = null;
13
13
  _lastError = null;
14
+ _stateEnteredAt = new Date();
14
15
  constructor(id, sessionId, definition, context, currentState, expiresAt) {
15
16
  this.id = id;
16
17
  this.sessionId = sessionId;
@@ -95,7 +96,8 @@ export class FlowInstance {
95
96
  copy.setActiveSubFlow(this._activeSubFlow);
96
97
  return copy;
97
98
  }
98
- /** @internal */ transitionTo(state) { this._currentState = state; }
99
+ get stateEnteredAt() { return this._stateEnteredAt; }
100
+ /** @internal */ transitionTo(state) { this._currentState = state; this._stateEnteredAt = new Date(); }
99
101
  /** @internal */ incrementGuardFailure() { this._guardFailureCount++; }
100
102
  /** @internal */ complete(exitState) { this._exitState = exitState; }
101
103
  /** @internal */ setVersion(version) { this._version = version; }
@@ -28,6 +28,8 @@ export interface Transition<S extends string> {
28
28
  branchTargets: Map<string, S>;
29
29
  subFlowDefinition?: import('./flow-definition.js').FlowDefinition<any>;
30
30
  exitMappings?: Map<string, S>;
31
+ /** Per-state timeout in milliseconds. If set, resumeAndExecute checks this before guard. */
32
+ timeout?: number;
31
33
  }
32
34
  /**
33
35
  * Processes a state transition.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unlaxer/tramli",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "Constrained flow engine — state machines that prevent invalid transitions at build time",
5
5
  "type": "module",
6
6
  "exports": {