@unlaxer/tramli 1.10.0 → 1.12.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.
@@ -11,6 +11,11 @@ export declare class FlowDefinition<S extends string> {
11
11
  readonly initialState: S | null;
12
12
  readonly terminalStates: Set<S>;
13
13
  readonly dataFlowGraph: DataFlowGraph<S> | null;
14
+ readonly warnings: string[];
15
+ readonly exceptionRoutes: Map<S, Array<{
16
+ errorClass: new (...args: any[]) => Error;
17
+ target: S;
18
+ }>>;
14
19
  private constructor();
15
20
  transitionsFrom(state: S): Transition<S>[];
16
21
  externalFrom(state: S): Transition<S> | undefined;
@@ -28,6 +33,7 @@ export declare class Builder<S extends string> {
28
33
  private maxGuardRetries;
29
34
  private readonly transitions;
30
35
  private readonly errorTransitions;
36
+ private readonly _exceptionRoutes;
31
37
  private readonly initiallyAvailableKeys;
32
38
  constructor(name: string, stateConfig: Record<S, StateConfig>);
33
39
  initiallyAvailable(...keys: FlowKey<unknown>[]): this;
@@ -35,6 +41,8 @@ export declare class Builder<S extends string> {
35
41
  setMaxGuardRetries(max: number): this;
36
42
  from(state: S): FromBuilder<S>;
37
43
  onError(from: S, to: S): this;
44
+ /** Route specific error types to specific states. Checked before onError. */
45
+ onStepError(from: S, errorClass: new (...args: any[]) => Error, to: S): this;
38
46
  onAnyError(errorState: S): this;
39
47
  /** @internal */
40
48
  addTransition(t: Transition<S>): void;
@@ -13,6 +13,8 @@ class FlowDefinition {
13
13
  initialState;
14
14
  terminalStates;
15
15
  dataFlowGraph;
16
+ warnings;
17
+ exceptionRoutes;
16
18
  constructor(name, stateConfig, ttl, maxGuardRetries, transitions, errorTransitions) {
17
19
  this.name = name;
18
20
  this.stateConfig = stateConfig;
@@ -89,6 +91,7 @@ class Builder {
89
91
  maxGuardRetries = 3;
90
92
  transitions = [];
91
93
  errorTransitions = new Map();
94
+ _exceptionRoutes = new Map();
92
95
  initiallyAvailableKeys = [];
93
96
  constructor(name, stateConfig) {
94
97
  this.name = name;
@@ -108,6 +111,13 @@ class Builder {
108
111
  this.errorTransitions.set(from, to);
109
112
  return this;
110
113
  }
114
+ /** Route specific error types to specific states. Checked before onError. */
115
+ onStepError(from, errorClass, to) {
116
+ if (!this._exceptionRoutes.has(from))
117
+ this._exceptionRoutes.set(from, []);
118
+ this._exceptionRoutes.get(from).push({ errorClass, target: to });
119
+ return this;
120
+ }
111
121
  onAnyError(errorState) {
112
122
  for (const s of Object.keys(this.stateConfig)) {
113
123
  if (!this.stateConfig[s].terminal)
@@ -143,6 +153,15 @@ class Builder {
143
153
  result.dataFlowGraph = null;
144
154
  this.validate(result);
145
155
  result.dataFlowGraph = data_flow_graph_js_1.DataFlowGraph.build(result, this.initiallyAvailableKeys);
156
+ // Build warnings
157
+ const warnings = [];
158
+ const perpetual = terminals.size === 0;
159
+ const hasExternal = this.transitions.some(t => t.type === 'external');
160
+ if (perpetual && hasExternal) {
161
+ warnings.push(`Perpetual flow '${this.name}' has External transitions — ensure events are always delivered to avoid deadlock (liveness risk)`);
162
+ }
163
+ result.warnings = warnings;
164
+ result.exceptionRoutes = new Map(this._exceptionRoutes);
146
165
  return result;
147
166
  }
148
167
  validate(def) {
@@ -266,6 +266,23 @@ class FlowEngine {
266
266
  }
267
267
  }
268
268
  this.errorLogger?.({ flowId: flow.id, from: fromState, to: null, trigger: 'error', cause: cause ?? null });
269
+ // 1. Try exception-typed routes first (onStepError)
270
+ if (cause && flow.definition.exceptionRoutes) {
271
+ const routes = flow.definition.exceptionRoutes.get(fromState);
272
+ if (routes) {
273
+ for (const route of routes) {
274
+ if (cause instanceof route.errorClass) {
275
+ const from = flow.currentState;
276
+ flow.transitionTo(route.target);
277
+ this.store.recordTransition(flow.id, from, route.target, `error:${cause.constructor.name}`, flow.context);
278
+ if (flow.definition.stateConfig[route.target].terminal)
279
+ flow.complete(route.target);
280
+ return;
281
+ }
282
+ }
283
+ }
284
+ }
285
+ // 2. Fall back to state-based error transition (onError)
269
286
  const errorTarget = flow.definition.errorTransitions.get(fromState);
270
287
  if (errorTarget) {
271
288
  const from = flow.currentState;
@@ -11,6 +11,11 @@ export declare class FlowDefinition<S extends string> {
11
11
  readonly initialState: S | null;
12
12
  readonly terminalStates: Set<S>;
13
13
  readonly dataFlowGraph: DataFlowGraph<S> | null;
14
+ readonly warnings: string[];
15
+ readonly exceptionRoutes: Map<S, Array<{
16
+ errorClass: new (...args: any[]) => Error;
17
+ target: S;
18
+ }>>;
14
19
  private constructor();
15
20
  transitionsFrom(state: S): Transition<S>[];
16
21
  externalFrom(state: S): Transition<S> | undefined;
@@ -28,6 +33,7 @@ export declare class Builder<S extends string> {
28
33
  private maxGuardRetries;
29
34
  private readonly transitions;
30
35
  private readonly errorTransitions;
36
+ private readonly _exceptionRoutes;
31
37
  private readonly initiallyAvailableKeys;
32
38
  constructor(name: string, stateConfig: Record<S, StateConfig>);
33
39
  initiallyAvailable(...keys: FlowKey<unknown>[]): this;
@@ -35,6 +41,8 @@ export declare class Builder<S extends string> {
35
41
  setMaxGuardRetries(max: number): this;
36
42
  from(state: S): FromBuilder<S>;
37
43
  onError(from: S, to: S): this;
44
+ /** Route specific error types to specific states. Checked before onError. */
45
+ onStepError(from: S, errorClass: new (...args: any[]) => Error, to: S): this;
38
46
  onAnyError(errorState: S): this;
39
47
  /** @internal */
40
48
  addTransition(t: Transition<S>): void;
@@ -10,6 +10,8 @@ export class FlowDefinition {
10
10
  initialState;
11
11
  terminalStates;
12
12
  dataFlowGraph;
13
+ warnings;
14
+ exceptionRoutes;
13
15
  constructor(name, stateConfig, ttl, maxGuardRetries, transitions, errorTransitions) {
14
16
  this.name = name;
15
17
  this.stateConfig = stateConfig;
@@ -85,6 +87,7 @@ export class Builder {
85
87
  maxGuardRetries = 3;
86
88
  transitions = [];
87
89
  errorTransitions = new Map();
90
+ _exceptionRoutes = new Map();
88
91
  initiallyAvailableKeys = [];
89
92
  constructor(name, stateConfig) {
90
93
  this.name = name;
@@ -104,6 +107,13 @@ export class Builder {
104
107
  this.errorTransitions.set(from, to);
105
108
  return this;
106
109
  }
110
+ /** Route specific error types to specific states. Checked before onError. */
111
+ onStepError(from, errorClass, to) {
112
+ if (!this._exceptionRoutes.has(from))
113
+ this._exceptionRoutes.set(from, []);
114
+ this._exceptionRoutes.get(from).push({ errorClass, target: to });
115
+ return this;
116
+ }
107
117
  onAnyError(errorState) {
108
118
  for (const s of Object.keys(this.stateConfig)) {
109
119
  if (!this.stateConfig[s].terminal)
@@ -139,6 +149,15 @@ export class Builder {
139
149
  result.dataFlowGraph = null;
140
150
  this.validate(result);
141
151
  result.dataFlowGraph = DataFlowGraph.build(result, this.initiallyAvailableKeys);
152
+ // Build warnings
153
+ const warnings = [];
154
+ const perpetual = terminals.size === 0;
155
+ const hasExternal = this.transitions.some(t => t.type === 'external');
156
+ if (perpetual && hasExternal) {
157
+ warnings.push(`Perpetual flow '${this.name}' has External transitions — ensure events are always delivered to avoid deadlock (liveness risk)`);
158
+ }
159
+ result.warnings = warnings;
160
+ result.exceptionRoutes = new Map(this._exceptionRoutes);
142
161
  return result;
143
162
  }
144
163
  validate(def) {
@@ -263,6 +263,23 @@ export class FlowEngine {
263
263
  }
264
264
  }
265
265
  this.errorLogger?.({ flowId: flow.id, from: fromState, to: null, trigger: 'error', cause: cause ?? null });
266
+ // 1. Try exception-typed routes first (onStepError)
267
+ if (cause && flow.definition.exceptionRoutes) {
268
+ const routes = flow.definition.exceptionRoutes.get(fromState);
269
+ if (routes) {
270
+ for (const route of routes) {
271
+ if (cause instanceof route.errorClass) {
272
+ const from = flow.currentState;
273
+ flow.transitionTo(route.target);
274
+ this.store.recordTransition(flow.id, from, route.target, `error:${cause.constructor.name}`, flow.context);
275
+ if (flow.definition.stateConfig[route.target].terminal)
276
+ flow.complete(route.target);
277
+ return;
278
+ }
279
+ }
280
+ }
281
+ }
282
+ // 2. Fall back to state-based error transition (onError)
266
283
  const errorTarget = flow.definition.errorTransitions.get(fromState);
267
284
  if (errorTarget) {
268
285
  const from = flow.currentState;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unlaxer/tramli",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "Constrained flow engine — state machines that prevent invalid transitions at build time",
5
5
  "type": "module",
6
6
  "exports": {