@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) {
|
package/dist/cjs/flow-engine.js
CHANGED
|
@@ -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) {
|
package/dist/esm/flow-engine.js
CHANGED
|
@@ -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;
|