@unlaxer/tramli 3.1.0 → 3.2.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.
- package/dist/cjs/flow-context.d.ts +12 -0
- package/dist/cjs/flow-context.js +27 -0
- package/dist/cjs/flow-definition.d.ts +17 -1
- package/dist/cjs/flow-definition.js +38 -13
- package/dist/cjs/flow-engine.d.ts +18 -0
- package/dist/cjs/flow-engine.js +80 -12
- package/dist/cjs/flow-instance.d.ts +4 -1
- package/dist/cjs/flow-instance.js +17 -2
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/pipeline.js +3 -3
- package/dist/esm/flow-context.d.ts +12 -0
- package/dist/esm/flow-context.js +27 -0
- package/dist/esm/flow-definition.d.ts +17 -1
- package/dist/esm/flow-definition.js +38 -13
- package/dist/esm/flow-engine.d.ts +18 -0
- package/dist/esm/flow-engine.js +80 -12
- package/dist/esm/flow-instance.d.ts +4 -1
- package/dist/esm/flow-instance.js +17 -2
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/pipeline.js +3 -3
- package/package.json +1 -1
|
@@ -9,6 +9,8 @@ export declare class FlowContext {
|
|
|
9
9
|
readonly flowId: string;
|
|
10
10
|
readonly createdAt: Date;
|
|
11
11
|
private attributes;
|
|
12
|
+
private aliasToKey;
|
|
13
|
+
private keyToAlias;
|
|
12
14
|
constructor(flowId: string, createdAt?: Date, attributes?: Map<string, unknown>);
|
|
13
15
|
get<T>(key: FlowKey<T>): T;
|
|
14
16
|
find<T>(key: FlowKey<T>): T | undefined;
|
|
@@ -16,4 +18,14 @@ export declare class FlowContext {
|
|
|
16
18
|
has(key: FlowKey<unknown>): boolean;
|
|
17
19
|
snapshot(): Map<string, unknown>;
|
|
18
20
|
restoreFrom(snapshot: Map<string, unknown>): void;
|
|
21
|
+
/** Register a string alias for a FlowKey. Used for cross-language serialization. */
|
|
22
|
+
registerAlias(key: FlowKey<unknown>, alias: string): void;
|
|
23
|
+
/** Get the alias for a key (if registered). */
|
|
24
|
+
aliasOf(key: FlowKey<unknown>): string | undefined;
|
|
25
|
+
/** Get the key for an alias (if registered). */
|
|
26
|
+
keyOfAlias(alias: string): FlowKey<unknown> | undefined;
|
|
27
|
+
/** Export all registered aliases as a map (alias → key). */
|
|
28
|
+
toAliasMap(): Map<string, string>;
|
|
29
|
+
/** Import aliases from a map (alias → key). */
|
|
30
|
+
fromAliasMap(map: Map<string, string>): void;
|
|
19
31
|
}
|
package/dist/cjs/flow-context.js
CHANGED
|
@@ -12,6 +12,8 @@ class FlowContext {
|
|
|
12
12
|
flowId;
|
|
13
13
|
createdAt;
|
|
14
14
|
attributes;
|
|
15
|
+
aliasToKey = new Map();
|
|
16
|
+
keyToAlias = new Map();
|
|
15
17
|
constructor(flowId, createdAt, attributes) {
|
|
16
18
|
this.flowId = flowId;
|
|
17
19
|
this.createdAt = createdAt ?? new Date();
|
|
@@ -40,5 +42,30 @@ class FlowContext {
|
|
|
40
42
|
for (const [k, v] of snapshot)
|
|
41
43
|
this.attributes.set(k, v);
|
|
42
44
|
}
|
|
45
|
+
// ─── Alias support (for cross-language serialization) ──────────────────
|
|
46
|
+
/** Register a string alias for a FlowKey. Used for cross-language serialization. */
|
|
47
|
+
registerAlias(key, alias) {
|
|
48
|
+
this.aliasToKey.set(alias, key);
|
|
49
|
+
this.keyToAlias.set(key, alias);
|
|
50
|
+
}
|
|
51
|
+
/** Get the alias for a key (if registered). */
|
|
52
|
+
aliasOf(key) {
|
|
53
|
+
return this.keyToAlias.get(key);
|
|
54
|
+
}
|
|
55
|
+
/** Get the key for an alias (if registered). */
|
|
56
|
+
keyOfAlias(alias) {
|
|
57
|
+
return this.aliasToKey.get(alias);
|
|
58
|
+
}
|
|
59
|
+
/** Export all registered aliases as a map (alias → key). */
|
|
60
|
+
toAliasMap() {
|
|
61
|
+
return new Map(this.aliasToKey);
|
|
62
|
+
}
|
|
63
|
+
/** Import aliases from a map (alias → key). */
|
|
64
|
+
fromAliasMap(map) {
|
|
65
|
+
for (const [alias, key] of map) {
|
|
66
|
+
this.aliasToKey.set(alias, key);
|
|
67
|
+
this.keyToAlias.set(key, alias);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
43
70
|
}
|
|
44
71
|
exports.FlowContext = FlowContext;
|
|
@@ -16,9 +16,17 @@ export declare class FlowDefinition<S extends string> {
|
|
|
16
16
|
errorClass: new (...args: any[]) => Error;
|
|
17
17
|
target: S;
|
|
18
18
|
}>>;
|
|
19
|
+
readonly enterActions: Map<S, (ctx: import('./flow-context.js').FlowContext) => void>;
|
|
20
|
+
readonly exitActions: Map<S, (ctx: import('./flow-context.js').FlowContext) => void>;
|
|
21
|
+
/** Get enter action for a state (or undefined). */
|
|
22
|
+
enterAction(state: S): ((ctx: import('./flow-context.js').FlowContext) => void) | undefined;
|
|
23
|
+
/** Get exit action for a state (or undefined). */
|
|
24
|
+
exitAction(state: S): ((ctx: import('./flow-context.js').FlowContext) => void) | undefined;
|
|
19
25
|
private constructor();
|
|
20
26
|
transitionsFrom(state: S): Transition<S>[];
|
|
21
27
|
externalFrom(state: S): Transition<S> | undefined;
|
|
28
|
+
/** All external transitions from a state (for multi-external). */
|
|
29
|
+
externalsFrom(state: S): Transition<S>[];
|
|
22
30
|
allStates(): S[];
|
|
23
31
|
/**
|
|
24
32
|
* Create a new FlowDefinition with a sub-flow inserted before a specific transition.
|
|
@@ -34,7 +42,10 @@ export declare class Builder<S extends string> {
|
|
|
34
42
|
private readonly transitions;
|
|
35
43
|
private readonly errorTransitions;
|
|
36
44
|
private readonly _exceptionRoutes;
|
|
45
|
+
private readonly _enterActions;
|
|
46
|
+
private readonly _exitActions;
|
|
37
47
|
private readonly initiallyAvailableKeys;
|
|
48
|
+
private _perpetual;
|
|
38
49
|
constructor(name: string, stateConfig: Record<S, StateConfig>);
|
|
39
50
|
initiallyAvailable(...keys: FlowKey<unknown>[]): this;
|
|
40
51
|
setTtl(ms: number): this;
|
|
@@ -44,6 +55,12 @@ export declare class Builder<S extends string> {
|
|
|
44
55
|
/** Route specific error types to specific states. Checked before onError. */
|
|
45
56
|
onStepError(from: S, errorClass: new (...args: any[]) => Error, to: S): this;
|
|
46
57
|
onAnyError(errorState: S): this;
|
|
58
|
+
/** Callback when entering a state (pure data/metrics, no I/O). */
|
|
59
|
+
onStateEnter(state: S, action: (ctx: import('./flow-context.js').FlowContext) => void): this;
|
|
60
|
+
/** Callback when exiting a state (pure data/metrics, no I/O). */
|
|
61
|
+
onStateExit(state: S, action: (ctx: import('./flow-context.js').FlowContext) => void): this;
|
|
62
|
+
/** Allow perpetual flows (no terminal states). Skips path-to-terminal validation. */
|
|
63
|
+
allowPerpetual(): this;
|
|
47
64
|
/** @internal */
|
|
48
65
|
addTransition(t: Transition<S>): void;
|
|
49
66
|
build(): FlowDefinition<S>;
|
|
@@ -53,7 +70,6 @@ export declare class Builder<S extends string> {
|
|
|
53
70
|
private canReachTerminal;
|
|
54
71
|
private checkDag;
|
|
55
72
|
private hasCycle;
|
|
56
|
-
private checkExternalUniqueness;
|
|
57
73
|
private checkBranchCompleteness;
|
|
58
74
|
private checkRequiresProduces;
|
|
59
75
|
private checkRequiresProducesFrom;
|
|
@@ -15,6 +15,16 @@ class FlowDefinition {
|
|
|
15
15
|
dataFlowGraph;
|
|
16
16
|
warnings;
|
|
17
17
|
exceptionRoutes;
|
|
18
|
+
enterActions;
|
|
19
|
+
exitActions;
|
|
20
|
+
/** Get enter action for a state (or undefined). */
|
|
21
|
+
enterAction(state) {
|
|
22
|
+
return this.enterActions?.get(state);
|
|
23
|
+
}
|
|
24
|
+
/** Get exit action for a state (or undefined). */
|
|
25
|
+
exitAction(state) {
|
|
26
|
+
return this.exitActions?.get(state);
|
|
27
|
+
}
|
|
18
28
|
constructor(name, stateConfig, ttl, maxGuardRetries, transitions, errorTransitions) {
|
|
19
29
|
this.name = name;
|
|
20
30
|
this.stateConfig = stateConfig;
|
|
@@ -39,6 +49,10 @@ class FlowDefinition {
|
|
|
39
49
|
externalFrom(state) {
|
|
40
50
|
return this.transitions.find(t => t.from === state && t.type === 'external');
|
|
41
51
|
}
|
|
52
|
+
/** All external transitions from a state (for multi-external). */
|
|
53
|
+
externalsFrom(state) {
|
|
54
|
+
return this.transitions.filter(t => t.from === state && t.type === 'external');
|
|
55
|
+
}
|
|
42
56
|
allStates() {
|
|
43
57
|
return Object.keys(this.stateConfig);
|
|
44
58
|
}
|
|
@@ -75,6 +89,10 @@ class FlowDefinition {
|
|
|
75
89
|
initialState: this.initialState,
|
|
76
90
|
terminalStates: this.terminalStates,
|
|
77
91
|
dataFlowGraph: this.dataFlowGraph, // reuse parent's graph
|
|
92
|
+
warnings: this.warnings,
|
|
93
|
+
exceptionRoutes: this.exceptionRoutes ? new Map(this.exceptionRoutes) : new Map(),
|
|
94
|
+
enterActions: this.enterActions ? new Map(this.enterActions) : new Map(),
|
|
95
|
+
exitActions: this.exitActions ? new Map(this.exitActions) : new Map(),
|
|
78
96
|
});
|
|
79
97
|
return result;
|
|
80
98
|
}
|
|
@@ -92,7 +110,10 @@ class Builder {
|
|
|
92
110
|
transitions = [];
|
|
93
111
|
errorTransitions = new Map();
|
|
94
112
|
_exceptionRoutes = new Map();
|
|
113
|
+
_enterActions = new Map();
|
|
114
|
+
_exitActions = new Map();
|
|
95
115
|
initiallyAvailableKeys = [];
|
|
116
|
+
_perpetual = false;
|
|
96
117
|
constructor(name, stateConfig) {
|
|
97
118
|
this.name = name;
|
|
98
119
|
this.stateConfig = stateConfig;
|
|
@@ -125,6 +146,18 @@ class Builder {
|
|
|
125
146
|
}
|
|
126
147
|
return this;
|
|
127
148
|
}
|
|
149
|
+
/** Callback when entering a state (pure data/metrics, no I/O). */
|
|
150
|
+
onStateEnter(state, action) {
|
|
151
|
+
this._enterActions.set(state, action);
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
/** Callback when exiting a state (pure data/metrics, no I/O). */
|
|
155
|
+
onStateExit(state, action) {
|
|
156
|
+
this._exitActions.set(state, action);
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
/** Allow perpetual flows (no terminal states). Skips path-to-terminal validation. */
|
|
160
|
+
allowPerpetual() { this._perpetual = true; return this; }
|
|
128
161
|
/** @internal */
|
|
129
162
|
addTransition(t) { this.transitions.push(t); }
|
|
130
163
|
build() {
|
|
@@ -162,6 +195,8 @@ class Builder {
|
|
|
162
195
|
}
|
|
163
196
|
result.warnings = warnings;
|
|
164
197
|
result.exceptionRoutes = new Map(this._exceptionRoutes);
|
|
198
|
+
result.enterActions = new Map(this._enterActions);
|
|
199
|
+
result.exitActions = new Map(this._exitActions);
|
|
165
200
|
return result;
|
|
166
201
|
}
|
|
167
202
|
validate(def) {
|
|
@@ -170,9 +205,10 @@ class Builder {
|
|
|
170
205
|
errors.push('No initial state found (exactly one state must have initial=true)');
|
|
171
206
|
}
|
|
172
207
|
this.checkReachability(def, errors);
|
|
173
|
-
this.
|
|
208
|
+
if (!this._perpetual)
|
|
209
|
+
this.checkPathToTerminal(def, errors);
|
|
174
210
|
this.checkDag(def, errors);
|
|
175
|
-
|
|
211
|
+
// checkExternalUniqueness removed (DD-020: multi-external allowed)
|
|
176
212
|
this.checkBranchCompleteness(def, errors);
|
|
177
213
|
this.checkRequiresProduces(def, errors);
|
|
178
214
|
this.checkAutoExternalConflict(def, errors);
|
|
@@ -277,17 +313,6 @@ class Builder {
|
|
|
277
313
|
inStack.delete(node);
|
|
278
314
|
return false;
|
|
279
315
|
}
|
|
280
|
-
checkExternalUniqueness(def, errors) {
|
|
281
|
-
const counts = new Map();
|
|
282
|
-
for (const t of def.transitions) {
|
|
283
|
-
if (t.type === 'external')
|
|
284
|
-
counts.set(t.from, (counts.get(t.from) ?? 0) + 1);
|
|
285
|
-
}
|
|
286
|
-
for (const [state, count] of counts) {
|
|
287
|
-
if (count > 1)
|
|
288
|
-
errors.push(`State ${state} has ${count} external transitions (max 1)`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
316
|
checkBranchCompleteness(def, errors) {
|
|
292
317
|
const allStates = new Set(def.allStates());
|
|
293
318
|
for (const t of def.transitions) {
|
|
@@ -6,23 +6,34 @@ export declare const DEFAULT_MAX_CHAIN_DEPTH = 10;
|
|
|
6
6
|
/** Log entry types for tramli's pluggable logger API. */
|
|
7
7
|
export interface TransitionLogEntry {
|
|
8
8
|
flowId: string;
|
|
9
|
+
flowName: string;
|
|
9
10
|
from: string | null;
|
|
10
11
|
to: string;
|
|
11
12
|
trigger: string;
|
|
12
13
|
}
|
|
13
14
|
export interface StateLogEntry {
|
|
14
15
|
flowId: string;
|
|
16
|
+
flowName: string;
|
|
15
17
|
state: string;
|
|
16
18
|
key: string;
|
|
17
19
|
value: unknown;
|
|
18
20
|
}
|
|
19
21
|
export interface ErrorLogEntry {
|
|
20
22
|
flowId: string;
|
|
23
|
+
flowName: string;
|
|
21
24
|
from: string | null;
|
|
22
25
|
to: string | null;
|
|
23
26
|
trigger: string;
|
|
24
27
|
cause: Error | null;
|
|
25
28
|
}
|
|
29
|
+
export interface GuardLogEntry {
|
|
30
|
+
flowId: string;
|
|
31
|
+
flowName: string;
|
|
32
|
+
state: string;
|
|
33
|
+
guardName: string;
|
|
34
|
+
result: 'accepted' | 'rejected' | 'expired';
|
|
35
|
+
reason?: string;
|
|
36
|
+
}
|
|
26
37
|
export declare class FlowEngine {
|
|
27
38
|
private readonly store;
|
|
28
39
|
private readonly strictMode;
|
|
@@ -30,6 +41,7 @@ export declare class FlowEngine {
|
|
|
30
41
|
private transitionLogger?;
|
|
31
42
|
private stateLogger?;
|
|
32
43
|
private errorLogger?;
|
|
44
|
+
private guardLogger?;
|
|
33
45
|
constructor(store: InMemoryFlowStore, options?: {
|
|
34
46
|
strictMode?: boolean;
|
|
35
47
|
maxChainDepth?: number;
|
|
@@ -37,6 +49,7 @@ export declare class FlowEngine {
|
|
|
37
49
|
setTransitionLogger(logger: ((entry: TransitionLogEntry) => void) | null): void;
|
|
38
50
|
setStateLogger(logger: ((entry: StateLogEntry) => void) | null): void;
|
|
39
51
|
setErrorLogger(logger: ((entry: ErrorLogEntry) => void) | null): void;
|
|
52
|
+
setGuardLogger(logger: ((entry: GuardLogEntry) => void) | null): void;
|
|
40
53
|
removeAllLoggers(): void;
|
|
41
54
|
startFlow<S extends string>(definition: FlowDefinition<S>, sessionId: string, initialData: Map<string, unknown>): Promise<FlowInstance<S>>;
|
|
42
55
|
resumeAndExecute<S extends string>(flowId: string, definition: FlowDefinition<S>, externalData?: Map<string, unknown>): Promise<FlowInstance<S>>;
|
|
@@ -44,5 +57,10 @@ export declare class FlowEngine {
|
|
|
44
57
|
private executeSubFlow;
|
|
45
58
|
private resumeSubFlow;
|
|
46
59
|
private verifyProduces;
|
|
60
|
+
private fireEnter;
|
|
61
|
+
private fireExit;
|
|
62
|
+
private logTransition;
|
|
63
|
+
private logError;
|
|
64
|
+
private logGuard;
|
|
47
65
|
private handleError;
|
|
48
66
|
}
|
package/dist/cjs/flow-engine.js
CHANGED
|
@@ -13,6 +13,7 @@ class FlowEngine {
|
|
|
13
13
|
transitionLogger;
|
|
14
14
|
stateLogger;
|
|
15
15
|
errorLogger;
|
|
16
|
+
guardLogger;
|
|
16
17
|
constructor(store, options) {
|
|
17
18
|
this.store = store;
|
|
18
19
|
this.strictMode = options?.strictMode ?? false;
|
|
@@ -27,10 +28,14 @@ class FlowEngine {
|
|
|
27
28
|
setErrorLogger(logger) {
|
|
28
29
|
this.errorLogger = logger ?? undefined;
|
|
29
30
|
}
|
|
31
|
+
setGuardLogger(logger) {
|
|
32
|
+
this.guardLogger = logger ?? undefined;
|
|
33
|
+
}
|
|
30
34
|
removeAllLoggers() {
|
|
31
35
|
this.transitionLogger = undefined;
|
|
32
36
|
this.stateLogger = undefined;
|
|
33
37
|
this.errorLogger = undefined;
|
|
38
|
+
this.guardLogger = undefined;
|
|
34
39
|
}
|
|
35
40
|
async startFlow(definition, sessionId, initialData) {
|
|
36
41
|
const flowId = crypto.randomUUID();
|
|
@@ -65,9 +70,22 @@ class FlowEngine {
|
|
|
65
70
|
return this.resumeSubFlow(flow, definition);
|
|
66
71
|
}
|
|
67
72
|
const currentState = flow.currentState;
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
// Multi-external: select guard by requires matching
|
|
74
|
+
const externals = definition.externalsFrom(currentState);
|
|
75
|
+
if (externals.length === 0)
|
|
70
76
|
throw flow_error_js_1.FlowError.invalidTransition(currentState, currentState);
|
|
77
|
+
let transition;
|
|
78
|
+
const dataKeys = externalData ? new Set(externalData.keys()) : new Set();
|
|
79
|
+
for (const ext of externals) {
|
|
80
|
+
if (ext.guard && ext.guard.requires.every(r => dataKeys.has(r))) {
|
|
81
|
+
transition = ext;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!transition) {
|
|
86
|
+
// Fallback: first external
|
|
87
|
+
transition = externals[0];
|
|
88
|
+
}
|
|
71
89
|
// Per-state timeout check
|
|
72
90
|
if (transition.timeout != null) {
|
|
73
91
|
const deadline = new Date(flow.stateEnteredAt.getTime() + transition.timeout);
|
|
@@ -82,6 +100,7 @@ class FlowEngine {
|
|
|
82
100
|
const output = await guard.validate(flow.context);
|
|
83
101
|
switch (output.type) {
|
|
84
102
|
case 'accepted': {
|
|
103
|
+
this.logGuard(flow, currentState, guard.name, 'accepted');
|
|
85
104
|
const backup = flow.context.snapshot();
|
|
86
105
|
if (output.data) {
|
|
87
106
|
for (const [key, value] of output.data)
|
|
@@ -91,8 +110,11 @@ class FlowEngine {
|
|
|
91
110
|
if (transition.processor)
|
|
92
111
|
await transition.processor.process(flow.context);
|
|
93
112
|
const from = flow.currentState;
|
|
113
|
+
this.fireExit(flow, from);
|
|
94
114
|
flow.transitionTo(transition.to);
|
|
115
|
+
this.fireEnter(flow, transition.to);
|
|
95
116
|
this.store.recordTransition(flow.id, from, transition.to, guard.name, flow.context);
|
|
117
|
+
this.logTransition(flow, from, transition.to, guard.name);
|
|
96
118
|
}
|
|
97
119
|
catch (e) {
|
|
98
120
|
flow.context.restoreFrom(backup);
|
|
@@ -103,7 +125,8 @@ class FlowEngine {
|
|
|
103
125
|
break;
|
|
104
126
|
}
|
|
105
127
|
case 'rejected': {
|
|
106
|
-
flow.
|
|
128
|
+
this.logGuard(flow, currentState, guard.name, 'rejected', output.reason);
|
|
129
|
+
flow.incrementGuardFailure(guard.name);
|
|
107
130
|
if (flow.guardFailureCount >= definition.maxGuardRetries) {
|
|
108
131
|
this.handleError(flow, currentState);
|
|
109
132
|
}
|
|
@@ -111,6 +134,7 @@ class FlowEngine {
|
|
|
111
134
|
return flow;
|
|
112
135
|
}
|
|
113
136
|
case 'expired': {
|
|
137
|
+
this.logGuard(flow, currentState, guard.name, 'expired');
|
|
114
138
|
flow.complete('EXPIRED');
|
|
115
139
|
this.store.save(flow);
|
|
116
140
|
return flow;
|
|
@@ -119,8 +143,11 @@ class FlowEngine {
|
|
|
119
143
|
}
|
|
120
144
|
else {
|
|
121
145
|
const from = flow.currentState;
|
|
146
|
+
this.fireExit(flow, from);
|
|
122
147
|
flow.transitionTo(transition.to);
|
|
148
|
+
this.fireEnter(flow, transition.to);
|
|
123
149
|
this.store.recordTransition(flow.id, from, transition.to, 'external', flow.context);
|
|
150
|
+
this.logTransition(flow, from, transition.to, 'external');
|
|
124
151
|
}
|
|
125
152
|
await this.executeAutoChain(flow);
|
|
126
153
|
this.store.save(flow);
|
|
@@ -155,8 +182,12 @@ class FlowEngine {
|
|
|
155
182
|
this.verifyProduces(autoOrBranch.processor, flow.context);
|
|
156
183
|
}
|
|
157
184
|
const from = flow.currentState;
|
|
185
|
+
this.fireExit(flow, from);
|
|
158
186
|
flow.transitionTo(autoOrBranch.to);
|
|
159
|
-
this.
|
|
187
|
+
this.fireEnter(flow, autoOrBranch.to);
|
|
188
|
+
const trigger = autoOrBranch.processor?.name ?? 'auto';
|
|
189
|
+
this.store.recordTransition(flow.id, from, autoOrBranch.to, trigger, flow.context);
|
|
190
|
+
this.logTransition(flow, from, autoOrBranch.to, trigger);
|
|
160
191
|
}
|
|
161
192
|
else {
|
|
162
193
|
const branch = autoOrBranch.branch;
|
|
@@ -169,8 +200,12 @@ class FlowEngine {
|
|
|
169
200
|
if (specific.processor)
|
|
170
201
|
await specific.processor.process(flow.context);
|
|
171
202
|
const from = flow.currentState;
|
|
203
|
+
this.fireExit(flow, from);
|
|
172
204
|
flow.transitionTo(target);
|
|
173
|
-
this.
|
|
205
|
+
this.fireEnter(flow, target);
|
|
206
|
+
const trigger = `${branch.name}:${label}`;
|
|
207
|
+
this.store.recordTransition(flow.id, from, target, trigger, flow.context);
|
|
208
|
+
this.logTransition(flow, from, target, trigger);
|
|
174
209
|
}
|
|
175
210
|
}
|
|
176
211
|
catch (e) {
|
|
@@ -195,8 +230,12 @@ class FlowEngine {
|
|
|
195
230
|
const target = exitMappings.get(subFlow.exitState);
|
|
196
231
|
if (target) {
|
|
197
232
|
const from = parentFlow.currentState;
|
|
233
|
+
this.fireExit(parentFlow, from);
|
|
198
234
|
parentFlow.transitionTo(target);
|
|
199
|
-
this.
|
|
235
|
+
this.fireEnter(parentFlow, target);
|
|
236
|
+
const trigger = `subFlow:${subDef.name}/${subFlow.exitState}`;
|
|
237
|
+
this.store.recordTransition(parentFlow.id, from, target, trigger, parentFlow.context);
|
|
238
|
+
this.logTransition(parentFlow, from, target, trigger);
|
|
200
239
|
return 1;
|
|
201
240
|
}
|
|
202
241
|
// Error bubbling: no exit mapping → fall back to parent's error transitions
|
|
@@ -220,8 +259,11 @@ class FlowEngine {
|
|
|
220
259
|
for (const [key, value] of output.data)
|
|
221
260
|
parentFlow.context.put(key, value);
|
|
222
261
|
}
|
|
262
|
+
const sfFrom = subFlow.currentState;
|
|
223
263
|
subFlow.transitionTo(transition.to);
|
|
224
|
-
this.store.recordTransition(parentFlow.id,
|
|
264
|
+
this.store.recordTransition(parentFlow.id, sfFrom, transition.to, guard.name, parentFlow.context);
|
|
265
|
+
this.logTransition(parentFlow, sfFrom, transition.to, guard.name);
|
|
266
|
+
this.logGuard(parentFlow, sfFrom, guard.name, 'accepted');
|
|
225
267
|
}
|
|
226
268
|
else if (output.type === 'rejected') {
|
|
227
269
|
subFlow.incrementGuardFailure();
|
|
@@ -249,8 +291,12 @@ class FlowEngine {
|
|
|
249
291
|
const target = subFlowT.exitMappings.get(subFlow.exitState);
|
|
250
292
|
if (target) {
|
|
251
293
|
const from = parentFlow.currentState;
|
|
294
|
+
this.fireExit(parentFlow, from);
|
|
252
295
|
parentFlow.transitionTo(target);
|
|
253
|
-
this.
|
|
296
|
+
this.fireEnter(parentFlow, target);
|
|
297
|
+
const trigger = `subFlow:${subDef.name}/${subFlow.exitState}`;
|
|
298
|
+
this.store.recordTransition(parentFlow.id, from, target, trigger, parentFlow.context);
|
|
299
|
+
this.logTransition(parentFlow, from, target, trigger);
|
|
254
300
|
await this.executeAutoChain(parentFlow);
|
|
255
301
|
}
|
|
256
302
|
}
|
|
@@ -267,6 +313,25 @@ class FlowEngine {
|
|
|
267
313
|
}
|
|
268
314
|
}
|
|
269
315
|
}
|
|
316
|
+
fireEnter(flow, state) {
|
|
317
|
+
const action = flow.definition.enterAction(state);
|
|
318
|
+
if (action)
|
|
319
|
+
action(flow.context);
|
|
320
|
+
}
|
|
321
|
+
fireExit(flow, state) {
|
|
322
|
+
const action = flow.definition.exitAction(state);
|
|
323
|
+
if (action)
|
|
324
|
+
action(flow.context);
|
|
325
|
+
}
|
|
326
|
+
logTransition(flow, from, to, trigger) {
|
|
327
|
+
this.transitionLogger?.({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger });
|
|
328
|
+
}
|
|
329
|
+
logError(flow, from, to, trigger, cause) {
|
|
330
|
+
this.errorLogger?.({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger, cause });
|
|
331
|
+
}
|
|
332
|
+
logGuard(flow, state, guardName, result, reason) {
|
|
333
|
+
this.guardLogger?.({ flowId: flow.id, flowName: flow.definition.name, state, guardName, result, reason });
|
|
334
|
+
}
|
|
270
335
|
handleError(flow, fromState, cause) {
|
|
271
336
|
if (cause) {
|
|
272
337
|
flow.setLastError(`${cause.constructor.name}: ${cause.message}`);
|
|
@@ -277,7 +342,7 @@ class FlowEngine {
|
|
|
277
342
|
cause.withContextSnapshot(available, new Set());
|
|
278
343
|
}
|
|
279
344
|
}
|
|
280
|
-
this.
|
|
345
|
+
this.logError(flow, fromState, null, 'error', cause ?? null);
|
|
281
346
|
// 1. Try exception-typed routes first (onStepError)
|
|
282
347
|
if (cause && flow.definition.exceptionRoutes) {
|
|
283
348
|
const routes = flow.definition.exceptionRoutes.get(fromState);
|
|
@@ -286,8 +351,10 @@ class FlowEngine {
|
|
|
286
351
|
if (cause instanceof route.errorClass) {
|
|
287
352
|
const from = flow.currentState;
|
|
288
353
|
flow.transitionTo(route.target);
|
|
289
|
-
|
|
290
|
-
|
|
354
|
+
const trigger = `error:${cause.constructor.name}`;
|
|
355
|
+
this.store.recordTransition(flow.id, from, route.target, trigger, flow.context);
|
|
356
|
+
this.logTransition(flow, from, route.target, trigger);
|
|
357
|
+
if (flow.definition.stateConfig[route.target]?.terminal)
|
|
291
358
|
flow.complete(route.target);
|
|
292
359
|
return;
|
|
293
360
|
}
|
|
@@ -300,7 +367,8 @@ class FlowEngine {
|
|
|
300
367
|
const from = flow.currentState;
|
|
301
368
|
flow.transitionTo(errorTarget);
|
|
302
369
|
this.store.recordTransition(flow.id, from, errorTarget, 'error', flow.context);
|
|
303
|
-
|
|
370
|
+
this.logTransition(flow, from, errorTarget, 'error');
|
|
371
|
+
if (flow.definition.stateConfig[errorTarget]?.terminal)
|
|
304
372
|
flow.complete(errorTarget);
|
|
305
373
|
}
|
|
306
374
|
else {
|
|
@@ -7,6 +7,7 @@ export declare class FlowInstance<S extends string> {
|
|
|
7
7
|
readonly context: FlowContext;
|
|
8
8
|
private _currentState;
|
|
9
9
|
private _guardFailureCount;
|
|
10
|
+
private _guardFailureCounts;
|
|
10
11
|
private _version;
|
|
11
12
|
readonly createdAt: Date;
|
|
12
13
|
readonly expiresAt: Date;
|
|
@@ -22,6 +23,8 @@ export declare class FlowInstance<S extends string> {
|
|
|
22
23
|
static restore<S extends string>(id: string, sessionId: string, definition: FlowDefinition<S>, context: FlowContext, currentState: S, createdAt: Date, expiresAt: Date, guardFailureCount: number, version: number, exitState: string | null): FlowInstance<S>;
|
|
23
24
|
get currentState(): S;
|
|
24
25
|
get guardFailureCount(): number;
|
|
26
|
+
/** Guard failure count for a specific guard (by name). */
|
|
27
|
+
guardFailureCountFor(guardName: string): number;
|
|
25
28
|
get version(): number;
|
|
26
29
|
get exitState(): string | null;
|
|
27
30
|
get isCompleted(): boolean;
|
|
@@ -42,7 +45,7 @@ export declare class FlowInstance<S extends string> {
|
|
|
42
45
|
withVersion(newVersion: number): FlowInstance<S>;
|
|
43
46
|
get stateEnteredAt(): Date;
|
|
44
47
|
/** @internal */ transitionTo(state: S): void;
|
|
45
|
-
/** @internal */ incrementGuardFailure(): void;
|
|
48
|
+
/** @internal */ incrementGuardFailure(guardName?: string): void;
|
|
46
49
|
/** @internal */ complete(exitState: string): void;
|
|
47
50
|
/** @internal */ setVersion(version: number): void;
|
|
48
51
|
/** @internal */ setActiveSubFlow(sub: FlowInstance<any> | null): void;
|
|
@@ -8,6 +8,7 @@ class FlowInstance {
|
|
|
8
8
|
context;
|
|
9
9
|
_currentState;
|
|
10
10
|
_guardFailureCount;
|
|
11
|
+
_guardFailureCounts = new Map();
|
|
11
12
|
_version;
|
|
12
13
|
createdAt;
|
|
13
14
|
expiresAt;
|
|
@@ -48,6 +49,8 @@ class FlowInstance {
|
|
|
48
49
|
}
|
|
49
50
|
get currentState() { return this._currentState; }
|
|
50
51
|
get guardFailureCount() { return this._guardFailureCount; }
|
|
52
|
+
/** Guard failure count for a specific guard (by name). */
|
|
53
|
+
guardFailureCountFor(guardName) { return this._guardFailureCounts.get(guardName) ?? 0; }
|
|
51
54
|
get version() { return this._version; }
|
|
52
55
|
get exitState() { return this._exitState; }
|
|
53
56
|
get isCompleted() { return this._exitState !== null; }
|
|
@@ -100,8 +103,20 @@ class FlowInstance {
|
|
|
100
103
|
return copy;
|
|
101
104
|
}
|
|
102
105
|
get stateEnteredAt() { return this._stateEnteredAt; }
|
|
103
|
-
/** @internal */ transitionTo(state) {
|
|
104
|
-
|
|
106
|
+
/** @internal */ transitionTo(state) {
|
|
107
|
+
const stateChanged = this._currentState !== state;
|
|
108
|
+
this._currentState = state;
|
|
109
|
+
this._stateEnteredAt = new Date();
|
|
110
|
+
if (stateChanged) {
|
|
111
|
+
this._guardFailureCount = 0;
|
|
112
|
+
this._guardFailureCounts.clear();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** @internal */ incrementGuardFailure(guardName) {
|
|
116
|
+
this._guardFailureCount++;
|
|
117
|
+
if (guardName)
|
|
118
|
+
this._guardFailureCounts.set(guardName, (this._guardFailureCounts.get(guardName) ?? 0) + 1);
|
|
119
|
+
}
|
|
105
120
|
/** @internal */ complete(exitState) { this._exitState = exitState; }
|
|
106
121
|
/** @internal */ setVersion(version) { this._version = version; }
|
|
107
122
|
/** @internal */ setActiveSubFlow(sub) { this._activeSubFlow = sub; }
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Tramli } from './tramli.js';
|
|
2
2
|
export { FlowEngine } from './flow-engine.js';
|
|
3
|
-
export type { TransitionLogEntry, StateLogEntry, ErrorLogEntry } from './flow-engine.js';
|
|
3
|
+
export type { TransitionLogEntry, StateLogEntry, ErrorLogEntry, GuardLogEntry } from './flow-engine.js';
|
|
4
4
|
export { FlowContext } from './flow-context.js';
|
|
5
5
|
export { FlowInstance } from './flow-instance.js';
|
|
6
6
|
export { FlowDefinition, Builder, FromBuilder, BranchBuilder, SubFlowBuilder } from './flow-definition.js';
|
package/dist/cjs/pipeline.js
CHANGED
|
@@ -92,14 +92,14 @@ class Pipeline {
|
|
|
92
92
|
const completed = [];
|
|
93
93
|
let prev = 'initial';
|
|
94
94
|
for (const step of this.steps) {
|
|
95
|
-
this.transitionLogger?.({ flowId, from: prev, to: step.name, trigger: step.name });
|
|
95
|
+
this.transitionLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name });
|
|
96
96
|
const keysBefore = this.stateLogger ? new Set(ctx.snapshot().keys()) : null;
|
|
97
97
|
try {
|
|
98
98
|
await step.process(ctx);
|
|
99
99
|
}
|
|
100
100
|
catch (e) {
|
|
101
101
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
102
|
-
this.errorLogger?.({ flowId, from: prev, to: step.name, trigger: step.name, cause: err });
|
|
102
|
+
this.errorLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name, cause: err });
|
|
103
103
|
throw new PipelineException(step.name, [...completed], ctx, err);
|
|
104
104
|
}
|
|
105
105
|
if (this.strictMode) {
|
|
@@ -113,7 +113,7 @@ class Pipeline {
|
|
|
113
113
|
if (this.stateLogger && keysBefore) {
|
|
114
114
|
for (const [k] of ctx.snapshot()) {
|
|
115
115
|
if (!keysBefore.has(k)) {
|
|
116
|
-
this.stateLogger({ flowId, state: step.name, key: k, value: ctx.snapshot().get(k) });
|
|
116
|
+
this.stateLogger({ flowId, flowName: this.name, state: step.name, key: k, value: ctx.snapshot().get(k) });
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
}
|
|
@@ -9,6 +9,8 @@ export declare class FlowContext {
|
|
|
9
9
|
readonly flowId: string;
|
|
10
10
|
readonly createdAt: Date;
|
|
11
11
|
private attributes;
|
|
12
|
+
private aliasToKey;
|
|
13
|
+
private keyToAlias;
|
|
12
14
|
constructor(flowId: string, createdAt?: Date, attributes?: Map<string, unknown>);
|
|
13
15
|
get<T>(key: FlowKey<T>): T;
|
|
14
16
|
find<T>(key: FlowKey<T>): T | undefined;
|
|
@@ -16,4 +18,14 @@ export declare class FlowContext {
|
|
|
16
18
|
has(key: FlowKey<unknown>): boolean;
|
|
17
19
|
snapshot(): Map<string, unknown>;
|
|
18
20
|
restoreFrom(snapshot: Map<string, unknown>): void;
|
|
21
|
+
/** Register a string alias for a FlowKey. Used for cross-language serialization. */
|
|
22
|
+
registerAlias(key: FlowKey<unknown>, alias: string): void;
|
|
23
|
+
/** Get the alias for a key (if registered). */
|
|
24
|
+
aliasOf(key: FlowKey<unknown>): string | undefined;
|
|
25
|
+
/** Get the key for an alias (if registered). */
|
|
26
|
+
keyOfAlias(alias: string): FlowKey<unknown> | undefined;
|
|
27
|
+
/** Export all registered aliases as a map (alias → key). */
|
|
28
|
+
toAliasMap(): Map<string, string>;
|
|
29
|
+
/** Import aliases from a map (alias → key). */
|
|
30
|
+
fromAliasMap(map: Map<string, string>): void;
|
|
19
31
|
}
|
package/dist/esm/flow-context.js
CHANGED
|
@@ -9,6 +9,8 @@ export class FlowContext {
|
|
|
9
9
|
flowId;
|
|
10
10
|
createdAt;
|
|
11
11
|
attributes;
|
|
12
|
+
aliasToKey = new Map();
|
|
13
|
+
keyToAlias = new Map();
|
|
12
14
|
constructor(flowId, createdAt, attributes) {
|
|
13
15
|
this.flowId = flowId;
|
|
14
16
|
this.createdAt = createdAt ?? new Date();
|
|
@@ -37,4 +39,29 @@ export class FlowContext {
|
|
|
37
39
|
for (const [k, v] of snapshot)
|
|
38
40
|
this.attributes.set(k, v);
|
|
39
41
|
}
|
|
42
|
+
// ─── Alias support (for cross-language serialization) ──────────────────
|
|
43
|
+
/** Register a string alias for a FlowKey. Used for cross-language serialization. */
|
|
44
|
+
registerAlias(key, alias) {
|
|
45
|
+
this.aliasToKey.set(alias, key);
|
|
46
|
+
this.keyToAlias.set(key, alias);
|
|
47
|
+
}
|
|
48
|
+
/** Get the alias for a key (if registered). */
|
|
49
|
+
aliasOf(key) {
|
|
50
|
+
return this.keyToAlias.get(key);
|
|
51
|
+
}
|
|
52
|
+
/** Get the key for an alias (if registered). */
|
|
53
|
+
keyOfAlias(alias) {
|
|
54
|
+
return this.aliasToKey.get(alias);
|
|
55
|
+
}
|
|
56
|
+
/** Export all registered aliases as a map (alias → key). */
|
|
57
|
+
toAliasMap() {
|
|
58
|
+
return new Map(this.aliasToKey);
|
|
59
|
+
}
|
|
60
|
+
/** Import aliases from a map (alias → key). */
|
|
61
|
+
fromAliasMap(map) {
|
|
62
|
+
for (const [alias, key] of map) {
|
|
63
|
+
this.aliasToKey.set(alias, key);
|
|
64
|
+
this.keyToAlias.set(key, alias);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
40
67
|
}
|
|
@@ -16,9 +16,17 @@ export declare class FlowDefinition<S extends string> {
|
|
|
16
16
|
errorClass: new (...args: any[]) => Error;
|
|
17
17
|
target: S;
|
|
18
18
|
}>>;
|
|
19
|
+
readonly enterActions: Map<S, (ctx: import('./flow-context.js').FlowContext) => void>;
|
|
20
|
+
readonly exitActions: Map<S, (ctx: import('./flow-context.js').FlowContext) => void>;
|
|
21
|
+
/** Get enter action for a state (or undefined). */
|
|
22
|
+
enterAction(state: S): ((ctx: import('./flow-context.js').FlowContext) => void) | undefined;
|
|
23
|
+
/** Get exit action for a state (or undefined). */
|
|
24
|
+
exitAction(state: S): ((ctx: import('./flow-context.js').FlowContext) => void) | undefined;
|
|
19
25
|
private constructor();
|
|
20
26
|
transitionsFrom(state: S): Transition<S>[];
|
|
21
27
|
externalFrom(state: S): Transition<S> | undefined;
|
|
28
|
+
/** All external transitions from a state (for multi-external). */
|
|
29
|
+
externalsFrom(state: S): Transition<S>[];
|
|
22
30
|
allStates(): S[];
|
|
23
31
|
/**
|
|
24
32
|
* Create a new FlowDefinition with a sub-flow inserted before a specific transition.
|
|
@@ -34,7 +42,10 @@ export declare class Builder<S extends string> {
|
|
|
34
42
|
private readonly transitions;
|
|
35
43
|
private readonly errorTransitions;
|
|
36
44
|
private readonly _exceptionRoutes;
|
|
45
|
+
private readonly _enterActions;
|
|
46
|
+
private readonly _exitActions;
|
|
37
47
|
private readonly initiallyAvailableKeys;
|
|
48
|
+
private _perpetual;
|
|
38
49
|
constructor(name: string, stateConfig: Record<S, StateConfig>);
|
|
39
50
|
initiallyAvailable(...keys: FlowKey<unknown>[]): this;
|
|
40
51
|
setTtl(ms: number): this;
|
|
@@ -44,6 +55,12 @@ export declare class Builder<S extends string> {
|
|
|
44
55
|
/** Route specific error types to specific states. Checked before onError. */
|
|
45
56
|
onStepError(from: S, errorClass: new (...args: any[]) => Error, to: S): this;
|
|
46
57
|
onAnyError(errorState: S): this;
|
|
58
|
+
/** Callback when entering a state (pure data/metrics, no I/O). */
|
|
59
|
+
onStateEnter(state: S, action: (ctx: import('./flow-context.js').FlowContext) => void): this;
|
|
60
|
+
/** Callback when exiting a state (pure data/metrics, no I/O). */
|
|
61
|
+
onStateExit(state: S, action: (ctx: import('./flow-context.js').FlowContext) => void): this;
|
|
62
|
+
/** Allow perpetual flows (no terminal states). Skips path-to-terminal validation. */
|
|
63
|
+
allowPerpetual(): this;
|
|
47
64
|
/** @internal */
|
|
48
65
|
addTransition(t: Transition<S>): void;
|
|
49
66
|
build(): FlowDefinition<S>;
|
|
@@ -53,7 +70,6 @@ export declare class Builder<S extends string> {
|
|
|
53
70
|
private canReachTerminal;
|
|
54
71
|
private checkDag;
|
|
55
72
|
private hasCycle;
|
|
56
|
-
private checkExternalUniqueness;
|
|
57
73
|
private checkBranchCompleteness;
|
|
58
74
|
private checkRequiresProduces;
|
|
59
75
|
private checkRequiresProducesFrom;
|
|
@@ -12,6 +12,16 @@ export class FlowDefinition {
|
|
|
12
12
|
dataFlowGraph;
|
|
13
13
|
warnings;
|
|
14
14
|
exceptionRoutes;
|
|
15
|
+
enterActions;
|
|
16
|
+
exitActions;
|
|
17
|
+
/** Get enter action for a state (or undefined). */
|
|
18
|
+
enterAction(state) {
|
|
19
|
+
return this.enterActions?.get(state);
|
|
20
|
+
}
|
|
21
|
+
/** Get exit action for a state (or undefined). */
|
|
22
|
+
exitAction(state) {
|
|
23
|
+
return this.exitActions?.get(state);
|
|
24
|
+
}
|
|
15
25
|
constructor(name, stateConfig, ttl, maxGuardRetries, transitions, errorTransitions) {
|
|
16
26
|
this.name = name;
|
|
17
27
|
this.stateConfig = stateConfig;
|
|
@@ -36,6 +46,10 @@ export class FlowDefinition {
|
|
|
36
46
|
externalFrom(state) {
|
|
37
47
|
return this.transitions.find(t => t.from === state && t.type === 'external');
|
|
38
48
|
}
|
|
49
|
+
/** All external transitions from a state (for multi-external). */
|
|
50
|
+
externalsFrom(state) {
|
|
51
|
+
return this.transitions.filter(t => t.from === state && t.type === 'external');
|
|
52
|
+
}
|
|
39
53
|
allStates() {
|
|
40
54
|
return Object.keys(this.stateConfig);
|
|
41
55
|
}
|
|
@@ -72,6 +86,10 @@ export class FlowDefinition {
|
|
|
72
86
|
initialState: this.initialState,
|
|
73
87
|
terminalStates: this.terminalStates,
|
|
74
88
|
dataFlowGraph: this.dataFlowGraph, // reuse parent's graph
|
|
89
|
+
warnings: this.warnings,
|
|
90
|
+
exceptionRoutes: this.exceptionRoutes ? new Map(this.exceptionRoutes) : new Map(),
|
|
91
|
+
enterActions: this.enterActions ? new Map(this.enterActions) : new Map(),
|
|
92
|
+
exitActions: this.exitActions ? new Map(this.exitActions) : new Map(),
|
|
75
93
|
});
|
|
76
94
|
return result;
|
|
77
95
|
}
|
|
@@ -88,7 +106,10 @@ export class Builder {
|
|
|
88
106
|
transitions = [];
|
|
89
107
|
errorTransitions = new Map();
|
|
90
108
|
_exceptionRoutes = new Map();
|
|
109
|
+
_enterActions = new Map();
|
|
110
|
+
_exitActions = new Map();
|
|
91
111
|
initiallyAvailableKeys = [];
|
|
112
|
+
_perpetual = false;
|
|
92
113
|
constructor(name, stateConfig) {
|
|
93
114
|
this.name = name;
|
|
94
115
|
this.stateConfig = stateConfig;
|
|
@@ -121,6 +142,18 @@ export class Builder {
|
|
|
121
142
|
}
|
|
122
143
|
return this;
|
|
123
144
|
}
|
|
145
|
+
/** Callback when entering a state (pure data/metrics, no I/O). */
|
|
146
|
+
onStateEnter(state, action) {
|
|
147
|
+
this._enterActions.set(state, action);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
/** Callback when exiting a state (pure data/metrics, no I/O). */
|
|
151
|
+
onStateExit(state, action) {
|
|
152
|
+
this._exitActions.set(state, action);
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
/** Allow perpetual flows (no terminal states). Skips path-to-terminal validation. */
|
|
156
|
+
allowPerpetual() { this._perpetual = true; return this; }
|
|
124
157
|
/** @internal */
|
|
125
158
|
addTransition(t) { this.transitions.push(t); }
|
|
126
159
|
build() {
|
|
@@ -158,6 +191,8 @@ export class Builder {
|
|
|
158
191
|
}
|
|
159
192
|
result.warnings = warnings;
|
|
160
193
|
result.exceptionRoutes = new Map(this._exceptionRoutes);
|
|
194
|
+
result.enterActions = new Map(this._enterActions);
|
|
195
|
+
result.exitActions = new Map(this._exitActions);
|
|
161
196
|
return result;
|
|
162
197
|
}
|
|
163
198
|
validate(def) {
|
|
@@ -166,9 +201,10 @@ export class Builder {
|
|
|
166
201
|
errors.push('No initial state found (exactly one state must have initial=true)');
|
|
167
202
|
}
|
|
168
203
|
this.checkReachability(def, errors);
|
|
169
|
-
this.
|
|
204
|
+
if (!this._perpetual)
|
|
205
|
+
this.checkPathToTerminal(def, errors);
|
|
170
206
|
this.checkDag(def, errors);
|
|
171
|
-
|
|
207
|
+
// checkExternalUniqueness removed (DD-020: multi-external allowed)
|
|
172
208
|
this.checkBranchCompleteness(def, errors);
|
|
173
209
|
this.checkRequiresProduces(def, errors);
|
|
174
210
|
this.checkAutoExternalConflict(def, errors);
|
|
@@ -273,17 +309,6 @@ export class Builder {
|
|
|
273
309
|
inStack.delete(node);
|
|
274
310
|
return false;
|
|
275
311
|
}
|
|
276
|
-
checkExternalUniqueness(def, errors) {
|
|
277
|
-
const counts = new Map();
|
|
278
|
-
for (const t of def.transitions) {
|
|
279
|
-
if (t.type === 'external')
|
|
280
|
-
counts.set(t.from, (counts.get(t.from) ?? 0) + 1);
|
|
281
|
-
}
|
|
282
|
-
for (const [state, count] of counts) {
|
|
283
|
-
if (count > 1)
|
|
284
|
-
errors.push(`State ${state} has ${count} external transitions (max 1)`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
312
|
checkBranchCompleteness(def, errors) {
|
|
288
313
|
const allStates = new Set(def.allStates());
|
|
289
314
|
for (const t of def.transitions) {
|
|
@@ -6,23 +6,34 @@ export declare const DEFAULT_MAX_CHAIN_DEPTH = 10;
|
|
|
6
6
|
/** Log entry types for tramli's pluggable logger API. */
|
|
7
7
|
export interface TransitionLogEntry {
|
|
8
8
|
flowId: string;
|
|
9
|
+
flowName: string;
|
|
9
10
|
from: string | null;
|
|
10
11
|
to: string;
|
|
11
12
|
trigger: string;
|
|
12
13
|
}
|
|
13
14
|
export interface StateLogEntry {
|
|
14
15
|
flowId: string;
|
|
16
|
+
flowName: string;
|
|
15
17
|
state: string;
|
|
16
18
|
key: string;
|
|
17
19
|
value: unknown;
|
|
18
20
|
}
|
|
19
21
|
export interface ErrorLogEntry {
|
|
20
22
|
flowId: string;
|
|
23
|
+
flowName: string;
|
|
21
24
|
from: string | null;
|
|
22
25
|
to: string | null;
|
|
23
26
|
trigger: string;
|
|
24
27
|
cause: Error | null;
|
|
25
28
|
}
|
|
29
|
+
export interface GuardLogEntry {
|
|
30
|
+
flowId: string;
|
|
31
|
+
flowName: string;
|
|
32
|
+
state: string;
|
|
33
|
+
guardName: string;
|
|
34
|
+
result: 'accepted' | 'rejected' | 'expired';
|
|
35
|
+
reason?: string;
|
|
36
|
+
}
|
|
26
37
|
export declare class FlowEngine {
|
|
27
38
|
private readonly store;
|
|
28
39
|
private readonly strictMode;
|
|
@@ -30,6 +41,7 @@ export declare class FlowEngine {
|
|
|
30
41
|
private transitionLogger?;
|
|
31
42
|
private stateLogger?;
|
|
32
43
|
private errorLogger?;
|
|
44
|
+
private guardLogger?;
|
|
33
45
|
constructor(store: InMemoryFlowStore, options?: {
|
|
34
46
|
strictMode?: boolean;
|
|
35
47
|
maxChainDepth?: number;
|
|
@@ -37,6 +49,7 @@ export declare class FlowEngine {
|
|
|
37
49
|
setTransitionLogger(logger: ((entry: TransitionLogEntry) => void) | null): void;
|
|
38
50
|
setStateLogger(logger: ((entry: StateLogEntry) => void) | null): void;
|
|
39
51
|
setErrorLogger(logger: ((entry: ErrorLogEntry) => void) | null): void;
|
|
52
|
+
setGuardLogger(logger: ((entry: GuardLogEntry) => void) | null): void;
|
|
40
53
|
removeAllLoggers(): void;
|
|
41
54
|
startFlow<S extends string>(definition: FlowDefinition<S>, sessionId: string, initialData: Map<string, unknown>): Promise<FlowInstance<S>>;
|
|
42
55
|
resumeAndExecute<S extends string>(flowId: string, definition: FlowDefinition<S>, externalData?: Map<string, unknown>): Promise<FlowInstance<S>>;
|
|
@@ -44,5 +57,10 @@ export declare class FlowEngine {
|
|
|
44
57
|
private executeSubFlow;
|
|
45
58
|
private resumeSubFlow;
|
|
46
59
|
private verifyProduces;
|
|
60
|
+
private fireEnter;
|
|
61
|
+
private fireExit;
|
|
62
|
+
private logTransition;
|
|
63
|
+
private logError;
|
|
64
|
+
private logGuard;
|
|
47
65
|
private handleError;
|
|
48
66
|
}
|
package/dist/esm/flow-engine.js
CHANGED
|
@@ -10,6 +10,7 @@ export class FlowEngine {
|
|
|
10
10
|
transitionLogger;
|
|
11
11
|
stateLogger;
|
|
12
12
|
errorLogger;
|
|
13
|
+
guardLogger;
|
|
13
14
|
constructor(store, options) {
|
|
14
15
|
this.store = store;
|
|
15
16
|
this.strictMode = options?.strictMode ?? false;
|
|
@@ -24,10 +25,14 @@ export class FlowEngine {
|
|
|
24
25
|
setErrorLogger(logger) {
|
|
25
26
|
this.errorLogger = logger ?? undefined;
|
|
26
27
|
}
|
|
28
|
+
setGuardLogger(logger) {
|
|
29
|
+
this.guardLogger = logger ?? undefined;
|
|
30
|
+
}
|
|
27
31
|
removeAllLoggers() {
|
|
28
32
|
this.transitionLogger = undefined;
|
|
29
33
|
this.stateLogger = undefined;
|
|
30
34
|
this.errorLogger = undefined;
|
|
35
|
+
this.guardLogger = undefined;
|
|
31
36
|
}
|
|
32
37
|
async startFlow(definition, sessionId, initialData) {
|
|
33
38
|
const flowId = crypto.randomUUID();
|
|
@@ -62,9 +67,22 @@ export class FlowEngine {
|
|
|
62
67
|
return this.resumeSubFlow(flow, definition);
|
|
63
68
|
}
|
|
64
69
|
const currentState = flow.currentState;
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
// Multi-external: select guard by requires matching
|
|
71
|
+
const externals = definition.externalsFrom(currentState);
|
|
72
|
+
if (externals.length === 0)
|
|
67
73
|
throw FlowError.invalidTransition(currentState, currentState);
|
|
74
|
+
let transition;
|
|
75
|
+
const dataKeys = externalData ? new Set(externalData.keys()) : new Set();
|
|
76
|
+
for (const ext of externals) {
|
|
77
|
+
if (ext.guard && ext.guard.requires.every(r => dataKeys.has(r))) {
|
|
78
|
+
transition = ext;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!transition) {
|
|
83
|
+
// Fallback: first external
|
|
84
|
+
transition = externals[0];
|
|
85
|
+
}
|
|
68
86
|
// Per-state timeout check
|
|
69
87
|
if (transition.timeout != null) {
|
|
70
88
|
const deadline = new Date(flow.stateEnteredAt.getTime() + transition.timeout);
|
|
@@ -79,6 +97,7 @@ export class FlowEngine {
|
|
|
79
97
|
const output = await guard.validate(flow.context);
|
|
80
98
|
switch (output.type) {
|
|
81
99
|
case 'accepted': {
|
|
100
|
+
this.logGuard(flow, currentState, guard.name, 'accepted');
|
|
82
101
|
const backup = flow.context.snapshot();
|
|
83
102
|
if (output.data) {
|
|
84
103
|
for (const [key, value] of output.data)
|
|
@@ -88,8 +107,11 @@ export class FlowEngine {
|
|
|
88
107
|
if (transition.processor)
|
|
89
108
|
await transition.processor.process(flow.context);
|
|
90
109
|
const from = flow.currentState;
|
|
110
|
+
this.fireExit(flow, from);
|
|
91
111
|
flow.transitionTo(transition.to);
|
|
112
|
+
this.fireEnter(flow, transition.to);
|
|
92
113
|
this.store.recordTransition(flow.id, from, transition.to, guard.name, flow.context);
|
|
114
|
+
this.logTransition(flow, from, transition.to, guard.name);
|
|
93
115
|
}
|
|
94
116
|
catch (e) {
|
|
95
117
|
flow.context.restoreFrom(backup);
|
|
@@ -100,7 +122,8 @@ export class FlowEngine {
|
|
|
100
122
|
break;
|
|
101
123
|
}
|
|
102
124
|
case 'rejected': {
|
|
103
|
-
flow.
|
|
125
|
+
this.logGuard(flow, currentState, guard.name, 'rejected', output.reason);
|
|
126
|
+
flow.incrementGuardFailure(guard.name);
|
|
104
127
|
if (flow.guardFailureCount >= definition.maxGuardRetries) {
|
|
105
128
|
this.handleError(flow, currentState);
|
|
106
129
|
}
|
|
@@ -108,6 +131,7 @@ export class FlowEngine {
|
|
|
108
131
|
return flow;
|
|
109
132
|
}
|
|
110
133
|
case 'expired': {
|
|
134
|
+
this.logGuard(flow, currentState, guard.name, 'expired');
|
|
111
135
|
flow.complete('EXPIRED');
|
|
112
136
|
this.store.save(flow);
|
|
113
137
|
return flow;
|
|
@@ -116,8 +140,11 @@ export class FlowEngine {
|
|
|
116
140
|
}
|
|
117
141
|
else {
|
|
118
142
|
const from = flow.currentState;
|
|
143
|
+
this.fireExit(flow, from);
|
|
119
144
|
flow.transitionTo(transition.to);
|
|
145
|
+
this.fireEnter(flow, transition.to);
|
|
120
146
|
this.store.recordTransition(flow.id, from, transition.to, 'external', flow.context);
|
|
147
|
+
this.logTransition(flow, from, transition.to, 'external');
|
|
121
148
|
}
|
|
122
149
|
await this.executeAutoChain(flow);
|
|
123
150
|
this.store.save(flow);
|
|
@@ -152,8 +179,12 @@ export class FlowEngine {
|
|
|
152
179
|
this.verifyProduces(autoOrBranch.processor, flow.context);
|
|
153
180
|
}
|
|
154
181
|
const from = flow.currentState;
|
|
182
|
+
this.fireExit(flow, from);
|
|
155
183
|
flow.transitionTo(autoOrBranch.to);
|
|
156
|
-
this.
|
|
184
|
+
this.fireEnter(flow, autoOrBranch.to);
|
|
185
|
+
const trigger = autoOrBranch.processor?.name ?? 'auto';
|
|
186
|
+
this.store.recordTransition(flow.id, from, autoOrBranch.to, trigger, flow.context);
|
|
187
|
+
this.logTransition(flow, from, autoOrBranch.to, trigger);
|
|
157
188
|
}
|
|
158
189
|
else {
|
|
159
190
|
const branch = autoOrBranch.branch;
|
|
@@ -166,8 +197,12 @@ export class FlowEngine {
|
|
|
166
197
|
if (specific.processor)
|
|
167
198
|
await specific.processor.process(flow.context);
|
|
168
199
|
const from = flow.currentState;
|
|
200
|
+
this.fireExit(flow, from);
|
|
169
201
|
flow.transitionTo(target);
|
|
170
|
-
this.
|
|
202
|
+
this.fireEnter(flow, target);
|
|
203
|
+
const trigger = `${branch.name}:${label}`;
|
|
204
|
+
this.store.recordTransition(flow.id, from, target, trigger, flow.context);
|
|
205
|
+
this.logTransition(flow, from, target, trigger);
|
|
171
206
|
}
|
|
172
207
|
}
|
|
173
208
|
catch (e) {
|
|
@@ -192,8 +227,12 @@ export class FlowEngine {
|
|
|
192
227
|
const target = exitMappings.get(subFlow.exitState);
|
|
193
228
|
if (target) {
|
|
194
229
|
const from = parentFlow.currentState;
|
|
230
|
+
this.fireExit(parentFlow, from);
|
|
195
231
|
parentFlow.transitionTo(target);
|
|
196
|
-
this.
|
|
232
|
+
this.fireEnter(parentFlow, target);
|
|
233
|
+
const trigger = `subFlow:${subDef.name}/${subFlow.exitState}`;
|
|
234
|
+
this.store.recordTransition(parentFlow.id, from, target, trigger, parentFlow.context);
|
|
235
|
+
this.logTransition(parentFlow, from, target, trigger);
|
|
197
236
|
return 1;
|
|
198
237
|
}
|
|
199
238
|
// Error bubbling: no exit mapping → fall back to parent's error transitions
|
|
@@ -217,8 +256,11 @@ export class FlowEngine {
|
|
|
217
256
|
for (const [key, value] of output.data)
|
|
218
257
|
parentFlow.context.put(key, value);
|
|
219
258
|
}
|
|
259
|
+
const sfFrom = subFlow.currentState;
|
|
220
260
|
subFlow.transitionTo(transition.to);
|
|
221
|
-
this.store.recordTransition(parentFlow.id,
|
|
261
|
+
this.store.recordTransition(parentFlow.id, sfFrom, transition.to, guard.name, parentFlow.context);
|
|
262
|
+
this.logTransition(parentFlow, sfFrom, transition.to, guard.name);
|
|
263
|
+
this.logGuard(parentFlow, sfFrom, guard.name, 'accepted');
|
|
222
264
|
}
|
|
223
265
|
else if (output.type === 'rejected') {
|
|
224
266
|
subFlow.incrementGuardFailure();
|
|
@@ -246,8 +288,12 @@ export class FlowEngine {
|
|
|
246
288
|
const target = subFlowT.exitMappings.get(subFlow.exitState);
|
|
247
289
|
if (target) {
|
|
248
290
|
const from = parentFlow.currentState;
|
|
291
|
+
this.fireExit(parentFlow, from);
|
|
249
292
|
parentFlow.transitionTo(target);
|
|
250
|
-
this.
|
|
293
|
+
this.fireEnter(parentFlow, target);
|
|
294
|
+
const trigger = `subFlow:${subDef.name}/${subFlow.exitState}`;
|
|
295
|
+
this.store.recordTransition(parentFlow.id, from, target, trigger, parentFlow.context);
|
|
296
|
+
this.logTransition(parentFlow, from, target, trigger);
|
|
251
297
|
await this.executeAutoChain(parentFlow);
|
|
252
298
|
}
|
|
253
299
|
}
|
|
@@ -264,6 +310,25 @@ export class FlowEngine {
|
|
|
264
310
|
}
|
|
265
311
|
}
|
|
266
312
|
}
|
|
313
|
+
fireEnter(flow, state) {
|
|
314
|
+
const action = flow.definition.enterAction(state);
|
|
315
|
+
if (action)
|
|
316
|
+
action(flow.context);
|
|
317
|
+
}
|
|
318
|
+
fireExit(flow, state) {
|
|
319
|
+
const action = flow.definition.exitAction(state);
|
|
320
|
+
if (action)
|
|
321
|
+
action(flow.context);
|
|
322
|
+
}
|
|
323
|
+
logTransition(flow, from, to, trigger) {
|
|
324
|
+
this.transitionLogger?.({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger });
|
|
325
|
+
}
|
|
326
|
+
logError(flow, from, to, trigger, cause) {
|
|
327
|
+
this.errorLogger?.({ flowId: flow.id, flowName: flow.definition.name, from, to, trigger, cause });
|
|
328
|
+
}
|
|
329
|
+
logGuard(flow, state, guardName, result, reason) {
|
|
330
|
+
this.guardLogger?.({ flowId: flow.id, flowName: flow.definition.name, state, guardName, result, reason });
|
|
331
|
+
}
|
|
267
332
|
handleError(flow, fromState, cause) {
|
|
268
333
|
if (cause) {
|
|
269
334
|
flow.setLastError(`${cause.constructor.name}: ${cause.message}`);
|
|
@@ -274,7 +339,7 @@ export class FlowEngine {
|
|
|
274
339
|
cause.withContextSnapshot(available, new Set());
|
|
275
340
|
}
|
|
276
341
|
}
|
|
277
|
-
this.
|
|
342
|
+
this.logError(flow, fromState, null, 'error', cause ?? null);
|
|
278
343
|
// 1. Try exception-typed routes first (onStepError)
|
|
279
344
|
if (cause && flow.definition.exceptionRoutes) {
|
|
280
345
|
const routes = flow.definition.exceptionRoutes.get(fromState);
|
|
@@ -283,8 +348,10 @@ export class FlowEngine {
|
|
|
283
348
|
if (cause instanceof route.errorClass) {
|
|
284
349
|
const from = flow.currentState;
|
|
285
350
|
flow.transitionTo(route.target);
|
|
286
|
-
|
|
287
|
-
|
|
351
|
+
const trigger = `error:${cause.constructor.name}`;
|
|
352
|
+
this.store.recordTransition(flow.id, from, route.target, trigger, flow.context);
|
|
353
|
+
this.logTransition(flow, from, route.target, trigger);
|
|
354
|
+
if (flow.definition.stateConfig[route.target]?.terminal)
|
|
288
355
|
flow.complete(route.target);
|
|
289
356
|
return;
|
|
290
357
|
}
|
|
@@ -297,7 +364,8 @@ export class FlowEngine {
|
|
|
297
364
|
const from = flow.currentState;
|
|
298
365
|
flow.transitionTo(errorTarget);
|
|
299
366
|
this.store.recordTransition(flow.id, from, errorTarget, 'error', flow.context);
|
|
300
|
-
|
|
367
|
+
this.logTransition(flow, from, errorTarget, 'error');
|
|
368
|
+
if (flow.definition.stateConfig[errorTarget]?.terminal)
|
|
301
369
|
flow.complete(errorTarget);
|
|
302
370
|
}
|
|
303
371
|
else {
|
|
@@ -7,6 +7,7 @@ export declare class FlowInstance<S extends string> {
|
|
|
7
7
|
readonly context: FlowContext;
|
|
8
8
|
private _currentState;
|
|
9
9
|
private _guardFailureCount;
|
|
10
|
+
private _guardFailureCounts;
|
|
10
11
|
private _version;
|
|
11
12
|
readonly createdAt: Date;
|
|
12
13
|
readonly expiresAt: Date;
|
|
@@ -22,6 +23,8 @@ export declare class FlowInstance<S extends string> {
|
|
|
22
23
|
static restore<S extends string>(id: string, sessionId: string, definition: FlowDefinition<S>, context: FlowContext, currentState: S, createdAt: Date, expiresAt: Date, guardFailureCount: number, version: number, exitState: string | null): FlowInstance<S>;
|
|
23
24
|
get currentState(): S;
|
|
24
25
|
get guardFailureCount(): number;
|
|
26
|
+
/** Guard failure count for a specific guard (by name). */
|
|
27
|
+
guardFailureCountFor(guardName: string): number;
|
|
25
28
|
get version(): number;
|
|
26
29
|
get exitState(): string | null;
|
|
27
30
|
get isCompleted(): boolean;
|
|
@@ -42,7 +45,7 @@ export declare class FlowInstance<S extends string> {
|
|
|
42
45
|
withVersion(newVersion: number): FlowInstance<S>;
|
|
43
46
|
get stateEnteredAt(): Date;
|
|
44
47
|
/** @internal */ transitionTo(state: S): void;
|
|
45
|
-
/** @internal */ incrementGuardFailure(): void;
|
|
48
|
+
/** @internal */ incrementGuardFailure(guardName?: string): void;
|
|
46
49
|
/** @internal */ complete(exitState: string): void;
|
|
47
50
|
/** @internal */ setVersion(version: number): void;
|
|
48
51
|
/** @internal */ setActiveSubFlow(sub: FlowInstance<any> | null): void;
|
|
@@ -5,6 +5,7 @@ export class FlowInstance {
|
|
|
5
5
|
context;
|
|
6
6
|
_currentState;
|
|
7
7
|
_guardFailureCount;
|
|
8
|
+
_guardFailureCounts = new Map();
|
|
8
9
|
_version;
|
|
9
10
|
createdAt;
|
|
10
11
|
expiresAt;
|
|
@@ -45,6 +46,8 @@ export class FlowInstance {
|
|
|
45
46
|
}
|
|
46
47
|
get currentState() { return this._currentState; }
|
|
47
48
|
get guardFailureCount() { return this._guardFailureCount; }
|
|
49
|
+
/** Guard failure count for a specific guard (by name). */
|
|
50
|
+
guardFailureCountFor(guardName) { return this._guardFailureCounts.get(guardName) ?? 0; }
|
|
48
51
|
get version() { return this._version; }
|
|
49
52
|
get exitState() { return this._exitState; }
|
|
50
53
|
get isCompleted() { return this._exitState !== null; }
|
|
@@ -97,8 +100,20 @@ export class FlowInstance {
|
|
|
97
100
|
return copy;
|
|
98
101
|
}
|
|
99
102
|
get stateEnteredAt() { return this._stateEnteredAt; }
|
|
100
|
-
/** @internal */ transitionTo(state) {
|
|
101
|
-
|
|
103
|
+
/** @internal */ transitionTo(state) {
|
|
104
|
+
const stateChanged = this._currentState !== state;
|
|
105
|
+
this._currentState = state;
|
|
106
|
+
this._stateEnteredAt = new Date();
|
|
107
|
+
if (stateChanged) {
|
|
108
|
+
this._guardFailureCount = 0;
|
|
109
|
+
this._guardFailureCounts.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** @internal */ incrementGuardFailure(guardName) {
|
|
113
|
+
this._guardFailureCount++;
|
|
114
|
+
if (guardName)
|
|
115
|
+
this._guardFailureCounts.set(guardName, (this._guardFailureCounts.get(guardName) ?? 0) + 1);
|
|
116
|
+
}
|
|
102
117
|
/** @internal */ complete(exitState) { this._exitState = exitState; }
|
|
103
118
|
/** @internal */ setVersion(version) { this._version = version; }
|
|
104
119
|
/** @internal */ setActiveSubFlow(sub) { this._activeSubFlow = sub; }
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Tramli } from './tramli.js';
|
|
2
2
|
export { FlowEngine } from './flow-engine.js';
|
|
3
|
-
export type { TransitionLogEntry, StateLogEntry, ErrorLogEntry } from './flow-engine.js';
|
|
3
|
+
export type { TransitionLogEntry, StateLogEntry, ErrorLogEntry, GuardLogEntry } from './flow-engine.js';
|
|
4
4
|
export { FlowContext } from './flow-context.js';
|
|
5
5
|
export { FlowInstance } from './flow-instance.js';
|
|
6
6
|
export { FlowDefinition, Builder, FromBuilder, BranchBuilder, SubFlowBuilder } from './flow-definition.js';
|
package/dist/esm/pipeline.js
CHANGED
|
@@ -87,14 +87,14 @@ export class Pipeline {
|
|
|
87
87
|
const completed = [];
|
|
88
88
|
let prev = 'initial';
|
|
89
89
|
for (const step of this.steps) {
|
|
90
|
-
this.transitionLogger?.({ flowId, from: prev, to: step.name, trigger: step.name });
|
|
90
|
+
this.transitionLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name });
|
|
91
91
|
const keysBefore = this.stateLogger ? new Set(ctx.snapshot().keys()) : null;
|
|
92
92
|
try {
|
|
93
93
|
await step.process(ctx);
|
|
94
94
|
}
|
|
95
95
|
catch (e) {
|
|
96
96
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
97
|
-
this.errorLogger?.({ flowId, from: prev, to: step.name, trigger: step.name, cause: err });
|
|
97
|
+
this.errorLogger?.({ flowId, flowName: this.name, from: prev, to: step.name, trigger: step.name, cause: err });
|
|
98
98
|
throw new PipelineException(step.name, [...completed], ctx, err);
|
|
99
99
|
}
|
|
100
100
|
if (this.strictMode) {
|
|
@@ -108,7 +108,7 @@ export class Pipeline {
|
|
|
108
108
|
if (this.stateLogger && keysBefore) {
|
|
109
109
|
for (const [k] of ctx.snapshot()) {
|
|
110
110
|
if (!keysBefore.has(k)) {
|
|
111
|
-
this.stateLogger({ flowId, state: step.name, key: k, value: ctx.snapshot().get(k) });
|
|
111
|
+
this.stateLogger({ flowId, flowName: this.name, state: step.name, key: k, value: ctx.snapshot().get(k) });
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
}
|