@unlaxer/tramli 1.5.3 → 1.7.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.
@@ -3,11 +3,15 @@ import { FlowInstance } from './flow-instance.js';
3
3
  import type { InMemoryFlowStore } from './in-memory-flow-store.js';
4
4
  export declare class FlowEngine {
5
5
  private readonly store;
6
- constructor(store: InMemoryFlowStore);
6
+ private readonly strictMode;
7
+ constructor(store: InMemoryFlowStore, options?: {
8
+ strictMode?: boolean;
9
+ });
7
10
  startFlow<S extends string>(definition: FlowDefinition<S>, sessionId: string, initialData: Map<string, unknown>): Promise<FlowInstance<S>>;
8
11
  resumeAndExecute<S extends string>(flowId: string, definition: FlowDefinition<S>, externalData?: Map<string, unknown>): Promise<FlowInstance<S>>;
9
12
  private executeAutoChain;
10
13
  private executeSubFlow;
11
14
  private resumeSubFlow;
15
+ private verifyProduces;
12
16
  private handleError;
13
17
  }
@@ -7,8 +7,10 @@ const flow_error_js_1 = require("./flow-error");
7
7
  const MAX_CHAIN_DEPTH = 10;
8
8
  class FlowEngine {
9
9
  store;
10
- constructor(store) {
10
+ strictMode;
11
+ constructor(store, options) {
11
12
  this.store = store;
13
+ this.strictMode = options?.strictMode ?? false;
12
14
  }
13
15
  async startFlow(definition, sessionId, initialData) {
14
16
  const flowId = crypto.randomUUID();
@@ -26,7 +28,7 @@ class FlowEngine {
26
28
  return flow;
27
29
  }
28
30
  async resumeAndExecute(flowId, definition, externalData) {
29
- const flow = this.store.loadForUpdate(flowId);
31
+ const flow = this.store.loadForUpdate(flowId, definition);
30
32
  if (!flow)
31
33
  throw new flow_error_js_1.FlowError('FLOW_NOT_FOUND', `Flow ${flowId} not found or already completed`);
32
34
  if (externalData) {
@@ -63,9 +65,9 @@ class FlowEngine {
63
65
  flow.transitionTo(transition.to);
64
66
  this.store.recordTransition(flow.id, from, transition.to, guard.name, flow.context);
65
67
  }
66
- catch {
68
+ catch (e) {
67
69
  flow.context.restoreFrom(backup);
68
- this.handleError(flow, currentState);
70
+ this.handleError(flow, currentState, e instanceof Error ? e : new Error(String(e)));
69
71
  this.store.save(flow);
70
72
  return flow;
71
73
  }
@@ -119,8 +121,10 @@ class FlowEngine {
119
121
  const backup = flow.context.snapshot();
120
122
  try {
121
123
  if (autoOrBranch.type === 'auto') {
122
- if (autoOrBranch.processor)
124
+ if (autoOrBranch.processor) {
123
125
  await autoOrBranch.processor.process(flow.context);
126
+ this.verifyProduces(autoOrBranch.processor, flow.context);
127
+ }
124
128
  const from = flow.currentState;
125
129
  flow.transitionTo(autoOrBranch.to);
126
130
  this.store.recordTransition(flow.id, from, autoOrBranch.to, autoOrBranch.processor?.name ?? 'auto', flow.context);
@@ -140,9 +144,9 @@ class FlowEngine {
140
144
  this.store.recordTransition(flow.id, from, target, `${branch.name}:${label}`, flow.context);
141
145
  }
142
146
  }
143
- catch {
147
+ catch (e) {
144
148
  flow.context.restoreFrom(backup);
145
- this.handleError(flow, flow.currentState);
149
+ this.handleError(flow, flow.currentState, e instanceof Error ? e : new Error(String(e)));
146
150
  return;
147
151
  }
148
152
  depth++;
@@ -225,7 +229,18 @@ class FlowEngine {
225
229
  this.store.save(parentFlow);
226
230
  return parentFlow;
227
231
  }
228
- handleError(flow, fromState) {
232
+ verifyProduces(processor, ctx) {
233
+ if (!this.strictMode)
234
+ return;
235
+ for (const prod of processor.produces) {
236
+ if (!ctx.has(prod)) {
237
+ throw new flow_error_js_1.FlowError('PRODUCES_VIOLATION', `Processor '${processor.name}' declares produces ${prod} but did not put it in context (strictMode)`);
238
+ }
239
+ }
240
+ }
241
+ handleError(flow, fromState, cause) {
242
+ if (cause)
243
+ flow.setLastError(`${cause.constructor.name}: ${cause.message}`);
229
244
  const errorTarget = flow.definition.errorTransitions.get(fromState);
230
245
  if (errorTarget) {
231
246
  const from = flow.currentState;
@@ -12,6 +12,7 @@ export declare class FlowInstance<S extends string> {
12
12
  readonly expiresAt: Date;
13
13
  private _exitState;
14
14
  private _activeSubFlow;
15
+ private _lastError;
15
16
  constructor(id: string, sessionId: string, definition: FlowDefinition<S>, context: FlowContext, currentState: S, expiresAt: Date);
16
17
  /**
17
18
  * Restore a FlowInstance from persisted state.
@@ -24,6 +25,8 @@ export declare class FlowInstance<S extends string> {
24
25
  get exitState(): string | null;
25
26
  get isCompleted(): boolean;
26
27
  get activeSubFlow(): FlowInstance<any> | null;
28
+ /** Last error message (set when a processor throws and error transition fires). */
29
+ get lastError(): string | null;
27
30
  /** State path from root to deepest active sub-flow. */
28
31
  statePath(): string[];
29
32
  /** State path as slash-separated string. */
@@ -41,4 +44,5 @@ export declare class FlowInstance<S extends string> {
41
44
  /** @internal */ complete(exitState: string): void;
42
45
  /** @internal */ setVersion(version: number): void;
43
46
  /** @internal */ setActiveSubFlow(sub: FlowInstance<any> | null): void;
47
+ /** @internal */ setLastError(error: string | null): void;
44
48
  }
@@ -13,6 +13,7 @@ class FlowInstance {
13
13
  expiresAt;
14
14
  _exitState;
15
15
  _activeSubFlow = null;
16
+ _lastError = null;
16
17
  constructor(id, sessionId, definition, context, currentState, expiresAt) {
17
18
  this.id = id;
18
19
  this.sessionId = sessionId;
@@ -50,6 +51,8 @@ class FlowInstance {
50
51
  get exitState() { return this._exitState; }
51
52
  get isCompleted() { return this._exitState !== null; }
52
53
  get activeSubFlow() { return this._activeSubFlow; }
54
+ /** Last error message (set when a processor throws and error transition fires). */
55
+ get lastError() { return this._lastError; }
53
56
  /** State path from root to deepest active sub-flow. */
54
57
  statePath() {
55
58
  const path = [this._currentState];
@@ -100,5 +103,6 @@ class FlowInstance {
100
103
  /** @internal */ complete(exitState) { this._exitState = exitState; }
101
104
  /** @internal */ setVersion(version) { this._version = version; }
102
105
  /** @internal */ setActiveSubFlow(sub) { this._activeSubFlow = sub; }
106
+ /** @internal */ setLastError(error) { this._lastError = error; }
103
107
  }
104
108
  exports.FlowInstance = FlowInstance;
@@ -12,7 +12,7 @@ export declare class InMemoryFlowStore {
12
12
  private flows;
13
13
  private _transitionLog;
14
14
  create(flow: FlowInstance<any>): void;
15
- loadForUpdate<S extends string>(flowId: string): FlowInstance<S> | undefined;
15
+ loadForUpdate<S extends string>(flowId: string, _definition?: any): FlowInstance<S> | undefined;
16
16
  save(flow: FlowInstance<any>): void;
17
17
  recordTransition(flowId: string, from: string | null, to: string, trigger: string, _ctx: FlowContext): void;
18
18
  get transitionLog(): readonly TransitionRecord[];
@@ -7,7 +7,7 @@ class InMemoryFlowStore {
7
7
  create(flow) {
8
8
  this.flows.set(flow.id, flow);
9
9
  }
10
- loadForUpdate(flowId) {
10
+ loadForUpdate(flowId, _definition) {
11
11
  const flow = this.flows.get(flowId);
12
12
  if (!flow || flow.isCompleted)
13
13
  return undefined;
@@ -4,5 +4,7 @@ import { FlowEngine } from './flow-engine.js';
4
4
  import type { InMemoryFlowStore } from './in-memory-flow-store.js';
5
5
  export declare class Tramli {
6
6
  static define<S extends string>(name: string, stateConfig: Record<S, StateConfig>): Builder<S>;
7
- static engine(store: InMemoryFlowStore): FlowEngine;
7
+ static engine(store: InMemoryFlowStore, options?: {
8
+ strictMode?: boolean;
9
+ }): FlowEngine;
8
10
  }
@@ -7,8 +7,8 @@ class Tramli {
7
7
  static define(name, stateConfig) {
8
8
  return new flow_definition_js_1.Builder(name, stateConfig);
9
9
  }
10
- static engine(store) {
11
- return new flow_engine_js_1.FlowEngine(store);
10
+ static engine(store, options) {
11
+ return new flow_engine_js_1.FlowEngine(store, options);
12
12
  }
13
13
  }
14
14
  exports.Tramli = Tramli;
@@ -3,11 +3,15 @@ import { FlowInstance } from './flow-instance.js';
3
3
  import type { InMemoryFlowStore } from './in-memory-flow-store.js';
4
4
  export declare class FlowEngine {
5
5
  private readonly store;
6
- constructor(store: InMemoryFlowStore);
6
+ private readonly strictMode;
7
+ constructor(store: InMemoryFlowStore, options?: {
8
+ strictMode?: boolean;
9
+ });
7
10
  startFlow<S extends string>(definition: FlowDefinition<S>, sessionId: string, initialData: Map<string, unknown>): Promise<FlowInstance<S>>;
8
11
  resumeAndExecute<S extends string>(flowId: string, definition: FlowDefinition<S>, externalData?: Map<string, unknown>): Promise<FlowInstance<S>>;
9
12
  private executeAutoChain;
10
13
  private executeSubFlow;
11
14
  private resumeSubFlow;
15
+ private verifyProduces;
12
16
  private handleError;
13
17
  }
@@ -4,8 +4,10 @@ import { FlowError } from './flow-error.js';
4
4
  const MAX_CHAIN_DEPTH = 10;
5
5
  export class FlowEngine {
6
6
  store;
7
- constructor(store) {
7
+ strictMode;
8
+ constructor(store, options) {
8
9
  this.store = store;
10
+ this.strictMode = options?.strictMode ?? false;
9
11
  }
10
12
  async startFlow(definition, sessionId, initialData) {
11
13
  const flowId = crypto.randomUUID();
@@ -23,7 +25,7 @@ export class FlowEngine {
23
25
  return flow;
24
26
  }
25
27
  async resumeAndExecute(flowId, definition, externalData) {
26
- const flow = this.store.loadForUpdate(flowId);
28
+ const flow = this.store.loadForUpdate(flowId, definition);
27
29
  if (!flow)
28
30
  throw new FlowError('FLOW_NOT_FOUND', `Flow ${flowId} not found or already completed`);
29
31
  if (externalData) {
@@ -60,9 +62,9 @@ export class FlowEngine {
60
62
  flow.transitionTo(transition.to);
61
63
  this.store.recordTransition(flow.id, from, transition.to, guard.name, flow.context);
62
64
  }
63
- catch {
65
+ catch (e) {
64
66
  flow.context.restoreFrom(backup);
65
- this.handleError(flow, currentState);
67
+ this.handleError(flow, currentState, e instanceof Error ? e : new Error(String(e)));
66
68
  this.store.save(flow);
67
69
  return flow;
68
70
  }
@@ -116,8 +118,10 @@ export class FlowEngine {
116
118
  const backup = flow.context.snapshot();
117
119
  try {
118
120
  if (autoOrBranch.type === 'auto') {
119
- if (autoOrBranch.processor)
121
+ if (autoOrBranch.processor) {
120
122
  await autoOrBranch.processor.process(flow.context);
123
+ this.verifyProduces(autoOrBranch.processor, flow.context);
124
+ }
121
125
  const from = flow.currentState;
122
126
  flow.transitionTo(autoOrBranch.to);
123
127
  this.store.recordTransition(flow.id, from, autoOrBranch.to, autoOrBranch.processor?.name ?? 'auto', flow.context);
@@ -137,9 +141,9 @@ export class FlowEngine {
137
141
  this.store.recordTransition(flow.id, from, target, `${branch.name}:${label}`, flow.context);
138
142
  }
139
143
  }
140
- catch {
144
+ catch (e) {
141
145
  flow.context.restoreFrom(backup);
142
- this.handleError(flow, flow.currentState);
146
+ this.handleError(flow, flow.currentState, e instanceof Error ? e : new Error(String(e)));
143
147
  return;
144
148
  }
145
149
  depth++;
@@ -222,7 +226,18 @@ export class FlowEngine {
222
226
  this.store.save(parentFlow);
223
227
  return parentFlow;
224
228
  }
225
- handleError(flow, fromState) {
229
+ verifyProduces(processor, ctx) {
230
+ if (!this.strictMode)
231
+ return;
232
+ for (const prod of processor.produces) {
233
+ if (!ctx.has(prod)) {
234
+ throw new FlowError('PRODUCES_VIOLATION', `Processor '${processor.name}' declares produces ${prod} but did not put it in context (strictMode)`);
235
+ }
236
+ }
237
+ }
238
+ handleError(flow, fromState, cause) {
239
+ if (cause)
240
+ flow.setLastError(`${cause.constructor.name}: ${cause.message}`);
226
241
  const errorTarget = flow.definition.errorTransitions.get(fromState);
227
242
  if (errorTarget) {
228
243
  const from = flow.currentState;
@@ -12,6 +12,7 @@ export declare class FlowInstance<S extends string> {
12
12
  readonly expiresAt: Date;
13
13
  private _exitState;
14
14
  private _activeSubFlow;
15
+ private _lastError;
15
16
  constructor(id: string, sessionId: string, definition: FlowDefinition<S>, context: FlowContext, currentState: S, expiresAt: Date);
16
17
  /**
17
18
  * Restore a FlowInstance from persisted state.
@@ -24,6 +25,8 @@ export declare class FlowInstance<S extends string> {
24
25
  get exitState(): string | null;
25
26
  get isCompleted(): boolean;
26
27
  get activeSubFlow(): FlowInstance<any> | null;
28
+ /** Last error message (set when a processor throws and error transition fires). */
29
+ get lastError(): string | null;
27
30
  /** State path from root to deepest active sub-flow. */
28
31
  statePath(): string[];
29
32
  /** State path as slash-separated string. */
@@ -41,4 +44,5 @@ export declare class FlowInstance<S extends string> {
41
44
  /** @internal */ complete(exitState: string): void;
42
45
  /** @internal */ setVersion(version: number): void;
43
46
  /** @internal */ setActiveSubFlow(sub: FlowInstance<any> | null): void;
47
+ /** @internal */ setLastError(error: string | null): void;
44
48
  }
@@ -10,6 +10,7 @@ export class FlowInstance {
10
10
  expiresAt;
11
11
  _exitState;
12
12
  _activeSubFlow = null;
13
+ _lastError = null;
13
14
  constructor(id, sessionId, definition, context, currentState, expiresAt) {
14
15
  this.id = id;
15
16
  this.sessionId = sessionId;
@@ -47,6 +48,8 @@ export class FlowInstance {
47
48
  get exitState() { return this._exitState; }
48
49
  get isCompleted() { return this._exitState !== null; }
49
50
  get activeSubFlow() { return this._activeSubFlow; }
51
+ /** Last error message (set when a processor throws and error transition fires). */
52
+ get lastError() { return this._lastError; }
50
53
  /** State path from root to deepest active sub-flow. */
51
54
  statePath() {
52
55
  const path = [this._currentState];
@@ -97,4 +100,5 @@ export class FlowInstance {
97
100
  /** @internal */ complete(exitState) { this._exitState = exitState; }
98
101
  /** @internal */ setVersion(version) { this._version = version; }
99
102
  /** @internal */ setActiveSubFlow(sub) { this._activeSubFlow = sub; }
103
+ /** @internal */ setLastError(error) { this._lastError = error; }
100
104
  }
@@ -12,7 +12,7 @@ export declare class InMemoryFlowStore {
12
12
  private flows;
13
13
  private _transitionLog;
14
14
  create(flow: FlowInstance<any>): void;
15
- loadForUpdate<S extends string>(flowId: string): FlowInstance<S> | undefined;
15
+ loadForUpdate<S extends string>(flowId: string, _definition?: any): FlowInstance<S> | undefined;
16
16
  save(flow: FlowInstance<any>): void;
17
17
  recordTransition(flowId: string, from: string | null, to: string, trigger: string, _ctx: FlowContext): void;
18
18
  get transitionLog(): readonly TransitionRecord[];
@@ -4,7 +4,7 @@ export class InMemoryFlowStore {
4
4
  create(flow) {
5
5
  this.flows.set(flow.id, flow);
6
6
  }
7
- loadForUpdate(flowId) {
7
+ loadForUpdate(flowId, _definition) {
8
8
  const flow = this.flows.get(flowId);
9
9
  if (!flow || flow.isCompleted)
10
10
  return undefined;
@@ -4,5 +4,7 @@ import { FlowEngine } from './flow-engine.js';
4
4
  import type { InMemoryFlowStore } from './in-memory-flow-store.js';
5
5
  export declare class Tramli {
6
6
  static define<S extends string>(name: string, stateConfig: Record<S, StateConfig>): Builder<S>;
7
- static engine(store: InMemoryFlowStore): FlowEngine;
7
+ static engine(store: InMemoryFlowStore, options?: {
8
+ strictMode?: boolean;
9
+ }): FlowEngine;
8
10
  }
@@ -4,7 +4,7 @@ export class Tramli {
4
4
  static define(name, stateConfig) {
5
5
  return new Builder(name, stateConfig);
6
6
  }
7
- static engine(store) {
8
- return new FlowEngine(store);
7
+ static engine(store, options) {
8
+ return new FlowEngine(store, options);
9
9
  }
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unlaxer/tramli",
3
- "version": "1.5.3",
3
+ "version": "1.7.0",
4
4
  "description": "Constrained flow engine — state machines that prevent invalid transitions at build time",
5
5
  "type": "module",
6
6
  "exports": {