@unlaxer/tramli 1.0.0 → 1.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/data-flow-graph.d.ts +30 -0
- package/dist/data-flow-graph.js +101 -0
- package/dist/flow-definition.d.ts +11 -0
- package/dist/flow-definition.js +58 -1
- package/dist/flow-engine.d.ts +2 -11
- package/dist/flow-engine.js +85 -11
- package/dist/flow-error.d.ts +6 -0
- package/dist/flow-error.js +10 -0
- package/dist/flow-instance.d.ts +3 -0
- package/dist/flow-instance.js +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { FlowDefinition } from './flow-definition.js';
|
|
2
2
|
import type { FlowKey } from './flow-key.js';
|
|
3
|
+
import type { StateProcessor } from './types.js';
|
|
4
|
+
import type { FlowContext } from './flow-context.js';
|
|
3
5
|
export interface NodeInfo<S extends string> {
|
|
4
6
|
name: string;
|
|
5
7
|
fromState: S;
|
|
@@ -25,8 +27,36 @@ export declare class DataFlowGraph<S extends string> {
|
|
|
25
27
|
consumersOf(key: FlowKey<unknown>): NodeInfo<S>[];
|
|
26
28
|
/** Types produced but never required by any downstream processor/guard. */
|
|
27
29
|
deadData(): Set<string>;
|
|
30
|
+
/** Data lifetime: which states a type is first produced and last consumed. */
|
|
31
|
+
lifetime(key: FlowKey<unknown>): {
|
|
32
|
+
firstProduced: S;
|
|
33
|
+
lastConsumed: S;
|
|
34
|
+
} | null;
|
|
35
|
+
/** Context pruning hints: for each state, types available but not required at that state. */
|
|
36
|
+
pruningHints(): Map<S, Set<string>>;
|
|
37
|
+
/**
|
|
38
|
+
* Check if processor B can replace processor A without breaking data-flow.
|
|
39
|
+
* B is compatible with A if: B requires no more than A, and B produces at least what A produces.
|
|
40
|
+
*/
|
|
41
|
+
static isCompatible<S extends string>(a: {
|
|
42
|
+
requires: FlowKey<unknown>[];
|
|
43
|
+
produces: FlowKey<unknown>[];
|
|
44
|
+
}, b: {
|
|
45
|
+
requires: FlowKey<unknown>[];
|
|
46
|
+
produces: FlowKey<unknown>[];
|
|
47
|
+
}): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Verify a processor's declared requires/produces against actual context usage.
|
|
50
|
+
* Returns list of violations (empty = OK).
|
|
51
|
+
*/
|
|
52
|
+
static verifyProcessor<S extends string>(processor: StateProcessor<S>, ctx: FlowContext): Promise<string[]>;
|
|
28
53
|
/** All type nodes in the graph. */
|
|
29
54
|
allTypes(): Set<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Assert that a flow instance's context satisfies the data-flow invariant.
|
|
57
|
+
* Returns list of missing type keys (empty = OK).
|
|
58
|
+
*/
|
|
59
|
+
assertDataFlow(ctx: FlowContext, currentState: S): string[];
|
|
30
60
|
/** Generate Mermaid data-flow diagram. */
|
|
31
61
|
toMermaid(): string;
|
|
32
62
|
static build<S extends string>(def: FlowDefinition<S>, initiallyAvailable: string[]): DataFlowGraph<S>;
|
package/dist/data-flow-graph.js
CHANGED
|
@@ -34,6 +34,95 @@ export class DataFlowGraph {
|
|
|
34
34
|
dead.delete(c);
|
|
35
35
|
return dead;
|
|
36
36
|
}
|
|
37
|
+
/** Data lifetime: which states a type is first produced and last consumed. */
|
|
38
|
+
lifetime(key) {
|
|
39
|
+
const prods = this._producers.get(key);
|
|
40
|
+
const cons = this._consumers.get(key);
|
|
41
|
+
if (!prods || prods.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
const firstProduced = prods[0].toState;
|
|
44
|
+
const lastConsumed = cons && cons.length > 0 ? cons[cons.length - 1].fromState : firstProduced;
|
|
45
|
+
return { firstProduced, lastConsumed };
|
|
46
|
+
}
|
|
47
|
+
/** Context pruning hints: for each state, types available but not required at that state. */
|
|
48
|
+
pruningHints() {
|
|
49
|
+
const consumedAt = new Map();
|
|
50
|
+
for (const [typeName, nodes] of this._consumers) {
|
|
51
|
+
for (const node of nodes) {
|
|
52
|
+
if (!consumedAt.has(node.fromState))
|
|
53
|
+
consumedAt.set(node.fromState, new Set());
|
|
54
|
+
consumedAt.get(node.fromState).add(typeName);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const hints = new Map();
|
|
58
|
+
for (const [state, available] of this._availableAtState) {
|
|
59
|
+
const needed = consumedAt.get(state) ?? new Set();
|
|
60
|
+
const prunable = new Set();
|
|
61
|
+
for (const type of available) {
|
|
62
|
+
if (!needed.has(type))
|
|
63
|
+
prunable.add(type);
|
|
64
|
+
}
|
|
65
|
+
if (prunable.size > 0)
|
|
66
|
+
hints.set(state, prunable);
|
|
67
|
+
}
|
|
68
|
+
return hints;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if processor B can replace processor A without breaking data-flow.
|
|
72
|
+
* B is compatible with A if: B requires no more than A, and B produces at least what A produces.
|
|
73
|
+
*/
|
|
74
|
+
static isCompatible(a, b) {
|
|
75
|
+
const aReqs = new Set(a.requires);
|
|
76
|
+
const bReqs = new Set(b.requires);
|
|
77
|
+
const aProds = new Set(a.produces);
|
|
78
|
+
const bProds = new Set(b.produces);
|
|
79
|
+
for (const r of bReqs) {
|
|
80
|
+
if (!aReqs.has(r))
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
for (const p of aProds) {
|
|
84
|
+
if (!bProds.has(p))
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Verify a processor's declared requires/produces against actual context usage.
|
|
91
|
+
* Returns list of violations (empty = OK).
|
|
92
|
+
*/
|
|
93
|
+
static async verifyProcessor(processor, ctx) {
|
|
94
|
+
const violations = [];
|
|
95
|
+
for (const req of processor.requires) {
|
|
96
|
+
if (!ctx.has(req))
|
|
97
|
+
violations.push(`requires ${req} but not in context`);
|
|
98
|
+
}
|
|
99
|
+
const beforeKeys = new Set();
|
|
100
|
+
for (const req of processor.requires) {
|
|
101
|
+
if (ctx.has(req))
|
|
102
|
+
beforeKeys.add(req);
|
|
103
|
+
}
|
|
104
|
+
// Capture all existing keys
|
|
105
|
+
const snapshot = ctx.snapshot();
|
|
106
|
+
const existingKeys = new Set(snapshot.keys());
|
|
107
|
+
try {
|
|
108
|
+
await processor.process(ctx);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
violations.push(`threw ${e.constructor.name}: ${e.message}`);
|
|
112
|
+
return violations;
|
|
113
|
+
}
|
|
114
|
+
const afterSnapshot = ctx.snapshot();
|
|
115
|
+
for (const prod of processor.produces) {
|
|
116
|
+
if (!afterSnapshot.has(prod))
|
|
117
|
+
violations.push(`declares produces ${prod} but did not put it`);
|
|
118
|
+
}
|
|
119
|
+
for (const [key] of afterSnapshot) {
|
|
120
|
+
if (!existingKeys.has(key) && !processor.produces.includes(key)) {
|
|
121
|
+
violations.push(`put ${key} but did not declare it in produces`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return violations;
|
|
125
|
+
}
|
|
37
126
|
/** All type nodes in the graph. */
|
|
38
127
|
allTypes() {
|
|
39
128
|
const types = new Set(this._allProduced);
|
|
@@ -41,6 +130,18 @@ export class DataFlowGraph {
|
|
|
41
130
|
types.add(c);
|
|
42
131
|
return types;
|
|
43
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Assert that a flow instance's context satisfies the data-flow invariant.
|
|
135
|
+
* Returns list of missing type keys (empty = OK).
|
|
136
|
+
*/
|
|
137
|
+
assertDataFlow(ctx, currentState) {
|
|
138
|
+
const missing = [];
|
|
139
|
+
for (const type of this.availableAt(currentState)) {
|
|
140
|
+
if (!ctx.has(type))
|
|
141
|
+
missing.push(type);
|
|
142
|
+
}
|
|
143
|
+
return missing;
|
|
144
|
+
}
|
|
44
145
|
/** Generate Mermaid data-flow diagram. */
|
|
45
146
|
toMermaid() {
|
|
46
147
|
const lines = ['flowchart LR'];
|
|
@@ -47,6 +47,7 @@ export declare class Builder<S extends string> {
|
|
|
47
47
|
private checkRequiresProducesFrom;
|
|
48
48
|
private checkAutoExternalConflict;
|
|
49
49
|
private checkTerminalNoOutgoing;
|
|
50
|
+
private checkSubFlowExitCompleteness;
|
|
50
51
|
}
|
|
51
52
|
export declare class FromBuilder<S extends string> {
|
|
52
53
|
private readonly builder;
|
|
@@ -55,6 +56,16 @@ export declare class FromBuilder<S extends string> {
|
|
|
55
56
|
auto(to: S, processor: StateProcessor<S>): Builder<S>;
|
|
56
57
|
external(to: S, guard: TransitionGuard<S>, processor?: StateProcessor<S>): Builder<S>;
|
|
57
58
|
branch(branch: BranchProcessor<S>): BranchBuilder<S>;
|
|
59
|
+
subFlow(subFlowDef: FlowDefinition<any>): SubFlowBuilder<S>;
|
|
60
|
+
}
|
|
61
|
+
export declare class SubFlowBuilder<S extends string> {
|
|
62
|
+
private readonly builder;
|
|
63
|
+
private readonly fromState;
|
|
64
|
+
private readonly subFlowDef;
|
|
65
|
+
private readonly exitMap;
|
|
66
|
+
constructor(builder: Builder<S>, fromState: S, subFlowDef: FlowDefinition<any>);
|
|
67
|
+
onExit(terminalName: string, parentState: S): this;
|
|
68
|
+
endSubFlow(): Builder<S>;
|
|
58
69
|
}
|
|
59
70
|
export declare class BranchBuilder<S extends string> {
|
|
60
71
|
private readonly builder;
|
package/dist/flow-definition.js
CHANGED
|
@@ -118,6 +118,7 @@ export class Builder {
|
|
|
118
118
|
this.checkRequiresProduces(def, errors);
|
|
119
119
|
this.checkAutoExternalConflict(def, errors);
|
|
120
120
|
this.checkTerminalNoOutgoing(def, errors);
|
|
121
|
+
this.checkSubFlowExitCompleteness(def, errors);
|
|
121
122
|
if (errors.length > 0) {
|
|
122
123
|
throw new FlowError('INVALID_FLOW_DEFINITION', `Flow '${this.name}' has ${errors.length} validation error(s):\n - ${errors.join('\n - ')}`);
|
|
123
124
|
}
|
|
@@ -131,6 +132,15 @@ export class Builder {
|
|
|
131
132
|
while (queue.length > 0) {
|
|
132
133
|
const current = queue.shift();
|
|
133
134
|
for (const t of def.transitionsFrom(current)) {
|
|
135
|
+
if (t.type === 'sub_flow' && t.exitMappings) {
|
|
136
|
+
for (const target of t.exitMappings.values()) {
|
|
137
|
+
if (!visited.has(target)) {
|
|
138
|
+
visited.add(target);
|
|
139
|
+
queue.push(target);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
134
144
|
if (!visited.has(t.to)) {
|
|
135
145
|
visited.add(t.to);
|
|
136
146
|
queue.push(t.to);
|
|
@@ -163,6 +173,13 @@ export class Builder {
|
|
|
163
173
|
return false;
|
|
164
174
|
visited.add(state);
|
|
165
175
|
for (const t of def.transitionsFrom(state)) {
|
|
176
|
+
if (t.type === 'sub_flow' && t.exitMappings) {
|
|
177
|
+
for (const target of t.exitMappings.values()) {
|
|
178
|
+
if (this.canReachTerminal(def, target, visited))
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
166
183
|
if (this.canReachTerminal(def, t.to, visited))
|
|
167
184
|
return true;
|
|
168
185
|
}
|
|
@@ -300,11 +317,23 @@ export class Builder {
|
|
|
300
317
|
}
|
|
301
318
|
checkTerminalNoOutgoing(def, errors) {
|
|
302
319
|
for (const t of def.transitions) {
|
|
303
|
-
if (def.stateConfig[t.from].terminal) {
|
|
320
|
+
if (def.stateConfig[t.from].terminal && t.type !== 'sub_flow') {
|
|
304
321
|
errors.push(`Terminal state ${t.from} has an outgoing transition to ${t.to}`);
|
|
305
322
|
}
|
|
306
323
|
}
|
|
307
324
|
}
|
|
325
|
+
checkSubFlowExitCompleteness(def, errors) {
|
|
326
|
+
for (const t of def.transitions) {
|
|
327
|
+
if (t.type !== 'sub_flow' || !t.subFlowDefinition)
|
|
328
|
+
continue;
|
|
329
|
+
const subDef = t.subFlowDefinition;
|
|
330
|
+
for (const terminal of subDef.terminalStates) {
|
|
331
|
+
if (!t.exitMappings?.has(terminal)) {
|
|
332
|
+
errors.push(`SubFlow '${subDef.name}' at ${t.from} has terminal state ${terminal} with no onExit mapping`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
308
337
|
}
|
|
309
338
|
export class FromBuilder {
|
|
310
339
|
builder;
|
|
@@ -330,6 +359,34 @@ export class FromBuilder {
|
|
|
330
359
|
branch(branch) {
|
|
331
360
|
return new BranchBuilder(this.builder, this.fromState, branch);
|
|
332
361
|
}
|
|
362
|
+
subFlow(subFlowDef) {
|
|
363
|
+
return new SubFlowBuilder(this.builder, this.fromState, subFlowDef);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
export class SubFlowBuilder {
|
|
367
|
+
builder;
|
|
368
|
+
fromState;
|
|
369
|
+
subFlowDef;
|
|
370
|
+
exitMap = new Map();
|
|
371
|
+
constructor(builder, fromState, subFlowDef) {
|
|
372
|
+
this.builder = builder;
|
|
373
|
+
this.fromState = fromState;
|
|
374
|
+
this.subFlowDef = subFlowDef;
|
|
375
|
+
}
|
|
376
|
+
onExit(terminalName, parentState) {
|
|
377
|
+
this.exitMap.set(terminalName, parentState);
|
|
378
|
+
return this;
|
|
379
|
+
}
|
|
380
|
+
endSubFlow() {
|
|
381
|
+
this.builder.addTransition({
|
|
382
|
+
from: this.fromState, to: this.fromState, type: 'sub_flow',
|
|
383
|
+
processor: undefined, guard: undefined, branch: undefined,
|
|
384
|
+
branchTargets: new Map(),
|
|
385
|
+
subFlowDefinition: this.subFlowDef,
|
|
386
|
+
exitMappings: new Map(this.exitMap),
|
|
387
|
+
});
|
|
388
|
+
return this.builder;
|
|
389
|
+
}
|
|
333
390
|
}
|
|
334
391
|
export class BranchBuilder {
|
|
335
392
|
builder;
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
import type { FlowDefinition } from './flow-definition.js';
|
|
2
2
|
import { FlowInstance } from './flow-instance.js';
|
|
3
3
|
import type { InMemoryFlowStore } from './in-memory-flow-store.js';
|
|
4
|
-
/**
|
|
5
|
-
* Generic engine that drives all flow state machines.
|
|
6
|
-
*
|
|
7
|
-
* Exceptions:
|
|
8
|
-
* - FLOW_NOT_FOUND: resumeAndExecute with unknown or completed flowId
|
|
9
|
-
* - INVALID_TRANSITION: resumeAndExecute when no external transition exists
|
|
10
|
-
* - MAX_CHAIN_DEPTH: auto-chain exceeded 10 steps
|
|
11
|
-
* - EXPIRED: flow TTL exceeded at resumeAndExecute entry
|
|
12
|
-
*
|
|
13
|
-
* Processor and branch exceptions are caught and routed to error transitions.
|
|
14
|
-
*/
|
|
15
4
|
export declare class FlowEngine {
|
|
16
5
|
private readonly store;
|
|
17
6
|
constructor(store: InMemoryFlowStore);
|
|
18
7
|
startFlow<S extends string>(definition: FlowDefinition<S>, sessionId: string, initialData: Map<string, unknown>): Promise<FlowInstance<S>>;
|
|
19
8
|
resumeAndExecute<S extends string>(flowId: string, definition: FlowDefinition<S>, externalData?: Map<string, unknown>): Promise<FlowInstance<S>>;
|
|
20
9
|
private executeAutoChain;
|
|
10
|
+
private executeSubFlow;
|
|
11
|
+
private resumeSubFlow;
|
|
21
12
|
private handleError;
|
|
22
13
|
}
|
package/dist/flow-engine.js
CHANGED
|
@@ -2,17 +2,6 @@ import { FlowContext } from './flow-context.js';
|
|
|
2
2
|
import { FlowInstance } from './flow-instance.js';
|
|
3
3
|
import { FlowError } from './flow-error.js';
|
|
4
4
|
const MAX_CHAIN_DEPTH = 10;
|
|
5
|
-
/**
|
|
6
|
-
* Generic engine that drives all flow state machines.
|
|
7
|
-
*
|
|
8
|
-
* Exceptions:
|
|
9
|
-
* - FLOW_NOT_FOUND: resumeAndExecute with unknown or completed flowId
|
|
10
|
-
* - INVALID_TRANSITION: resumeAndExecute when no external transition exists
|
|
11
|
-
* - MAX_CHAIN_DEPTH: auto-chain exceeded 10 steps
|
|
12
|
-
* - EXPIRED: flow TTL exceeded at resumeAndExecute entry
|
|
13
|
-
*
|
|
14
|
-
* Processor and branch exceptions are caught and routed to error transitions.
|
|
15
|
-
*/
|
|
16
5
|
export class FlowEngine {
|
|
17
6
|
store;
|
|
18
7
|
constructor(store) {
|
|
@@ -46,6 +35,10 @@ export class FlowEngine {
|
|
|
46
35
|
this.store.save(flow);
|
|
47
36
|
return flow;
|
|
48
37
|
}
|
|
38
|
+
// If actively in a sub-flow, delegate resume
|
|
39
|
+
if (flow.activeSubFlow) {
|
|
40
|
+
return this.resumeSubFlow(flow, definition);
|
|
41
|
+
}
|
|
49
42
|
const currentState = flow.currentState;
|
|
50
43
|
const transition = definition.externalFrom(currentState);
|
|
51
44
|
if (!transition)
|
|
@@ -108,6 +101,15 @@ export class FlowEngine {
|
|
|
108
101
|
break;
|
|
109
102
|
}
|
|
110
103
|
const transitions = flow.definition.transitionsFrom(current);
|
|
104
|
+
// Check for sub-flow transition
|
|
105
|
+
const subFlowT = transitions.find(t => t.type === 'sub_flow');
|
|
106
|
+
if (subFlowT) {
|
|
107
|
+
const advanced = await this.executeSubFlow(flow, subFlowT);
|
|
108
|
+
depth += advanced;
|
|
109
|
+
if (advanced === 0)
|
|
110
|
+
break; // sub-flow stopped at external
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
111
113
|
const autoOrBranch = transitions.find(t => t.type === 'auto' || t.type === 'branch');
|
|
112
114
|
if (!autoOrBranch)
|
|
113
115
|
break;
|
|
@@ -145,6 +147,78 @@ export class FlowEngine {
|
|
|
145
147
|
if (depth >= MAX_CHAIN_DEPTH)
|
|
146
148
|
throw FlowError.maxChainDepth();
|
|
147
149
|
}
|
|
150
|
+
async executeSubFlow(parentFlow, subFlowTransition) {
|
|
151
|
+
const subDef = subFlowTransition.subFlowDefinition;
|
|
152
|
+
const exitMappings = subFlowTransition.exitMappings;
|
|
153
|
+
const subInitial = subDef.initialState;
|
|
154
|
+
const subFlow = new FlowInstance(parentFlow.id, parentFlow.sessionId, subDef, parentFlow.context, subInitial, parentFlow.expiresAt);
|
|
155
|
+
parentFlow.setActiveSubFlow(subFlow);
|
|
156
|
+
await this.executeAutoChain(subFlow);
|
|
157
|
+
if (subFlow.isCompleted) {
|
|
158
|
+
parentFlow.setActiveSubFlow(null);
|
|
159
|
+
const target = exitMappings.get(subFlow.exitState);
|
|
160
|
+
if (target) {
|
|
161
|
+
const from = parentFlow.currentState;
|
|
162
|
+
parentFlow.transitionTo(target);
|
|
163
|
+
this.store.recordTransition(parentFlow.id, from, target, `subFlow:${subDef.name}/${subFlow.exitState}`, parentFlow.context);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return 0; // sub-flow stopped at external
|
|
168
|
+
}
|
|
169
|
+
async resumeSubFlow(parentFlow, parentDef) {
|
|
170
|
+
const subFlow = parentFlow.activeSubFlow;
|
|
171
|
+
const subDef = subFlow.definition;
|
|
172
|
+
const transition = subDef.externalFrom(subFlow.currentState);
|
|
173
|
+
if (!transition) {
|
|
174
|
+
throw new FlowError('INVALID_TRANSITION', `No external transition from sub-flow state ${subFlow.currentState}`);
|
|
175
|
+
}
|
|
176
|
+
const guard = transition.guard;
|
|
177
|
+
if (guard) {
|
|
178
|
+
const output = await guard.validate(parentFlow.context);
|
|
179
|
+
if (output.type === 'accepted') {
|
|
180
|
+
if (output.data) {
|
|
181
|
+
for (const [key, value] of output.data)
|
|
182
|
+
parentFlow.context.put(key, value);
|
|
183
|
+
}
|
|
184
|
+
subFlow.transitionTo(transition.to);
|
|
185
|
+
this.store.recordTransition(parentFlow.id, subFlow.currentState, transition.to, guard.name, parentFlow.context);
|
|
186
|
+
}
|
|
187
|
+
else if (output.type === 'rejected') {
|
|
188
|
+
subFlow.incrementGuardFailure();
|
|
189
|
+
if (subFlow.guardFailureCount >= subDef.maxGuardRetries) {
|
|
190
|
+
subFlow.complete('ERROR');
|
|
191
|
+
}
|
|
192
|
+
this.store.save(parentFlow);
|
|
193
|
+
return parentFlow;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
parentFlow.complete('EXPIRED');
|
|
197
|
+
this.store.save(parentFlow);
|
|
198
|
+
return parentFlow;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
subFlow.transitionTo(transition.to);
|
|
203
|
+
}
|
|
204
|
+
await this.executeAutoChain(subFlow);
|
|
205
|
+
if (subFlow.isCompleted) {
|
|
206
|
+
parentFlow.setActiveSubFlow(null);
|
|
207
|
+
const subFlowT = parentDef.transitionsFrom(parentFlow.currentState)
|
|
208
|
+
.find(t => t.type === 'sub_flow');
|
|
209
|
+
if (subFlowT?.exitMappings) {
|
|
210
|
+
const target = subFlowT.exitMappings.get(subFlow.exitState);
|
|
211
|
+
if (target) {
|
|
212
|
+
const from = parentFlow.currentState;
|
|
213
|
+
parentFlow.transitionTo(target);
|
|
214
|
+
this.store.recordTransition(parentFlow.id, from, target, `subFlow:${subDef.name}/${subFlow.exitState}`, parentFlow.context);
|
|
215
|
+
await this.executeAutoChain(parentFlow);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
this.store.save(parentFlow);
|
|
220
|
+
return parentFlow;
|
|
221
|
+
}
|
|
148
222
|
handleError(flow, fromState) {
|
|
149
223
|
const errorTarget = flow.definition.errorTransitions.get(fromState);
|
|
150
224
|
if (errorTarget) {
|
package/dist/flow-error.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export declare class FlowError extends Error {
|
|
2
2
|
readonly code: string;
|
|
3
|
+
/** Types that were available in context when the error occurred. */
|
|
4
|
+
availableTypes?: Set<string>;
|
|
5
|
+
/** Types that were expected but missing (if applicable). */
|
|
6
|
+
missingTypes?: Set<string>;
|
|
3
7
|
constructor(code: string, message: string);
|
|
8
|
+
/** Attach context snapshot to this error. */
|
|
9
|
+
withContextSnapshot(available: Set<string>, missing: Set<string>): this;
|
|
4
10
|
static invalidTransition(from: string, to: string): FlowError;
|
|
5
11
|
static missingContext(key: string): FlowError;
|
|
6
12
|
static dagCycle(detail: string): FlowError;
|
package/dist/flow-error.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
export class FlowError extends Error {
|
|
2
2
|
code;
|
|
3
|
+
/** Types that were available in context when the error occurred. */
|
|
4
|
+
availableTypes;
|
|
5
|
+
/** Types that were expected but missing (if applicable). */
|
|
6
|
+
missingTypes;
|
|
3
7
|
constructor(code, message) {
|
|
4
8
|
super(message);
|
|
5
9
|
this.code = code;
|
|
6
10
|
this.name = 'FlowError';
|
|
7
11
|
}
|
|
12
|
+
/** Attach context snapshot to this error. */
|
|
13
|
+
withContextSnapshot(available, missing) {
|
|
14
|
+
this.availableTypes = available;
|
|
15
|
+
this.missingTypes = missing;
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
8
18
|
static invalidTransition(from, to) {
|
|
9
19
|
return new FlowError('INVALID_TRANSITION', `Invalid transition from ${from} to ${to}`);
|
|
10
20
|
}
|
package/dist/flow-instance.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class FlowInstance<S extends string> {
|
|
|
11
11
|
readonly createdAt: Date;
|
|
12
12
|
readonly expiresAt: Date;
|
|
13
13
|
private _exitState;
|
|
14
|
+
private _activeSubFlow;
|
|
14
15
|
constructor(id: string, sessionId: string, definition: FlowDefinition<S>, context: FlowContext, currentState: S, expiresAt: Date);
|
|
15
16
|
/**
|
|
16
17
|
* Restore a FlowInstance from persisted state.
|
|
@@ -22,8 +23,10 @@ export declare class FlowInstance<S extends string> {
|
|
|
22
23
|
get version(): number;
|
|
23
24
|
get exitState(): string | null;
|
|
24
25
|
get isCompleted(): boolean;
|
|
26
|
+
get activeSubFlow(): FlowInstance<any> | null;
|
|
25
27
|
/** @internal */ transitionTo(state: S): void;
|
|
26
28
|
/** @internal */ incrementGuardFailure(): void;
|
|
27
29
|
/** @internal */ complete(exitState: string): void;
|
|
28
30
|
/** @internal */ setVersion(version: number): void;
|
|
31
|
+
/** @internal */ setActiveSubFlow(sub: FlowInstance<any> | null): void;
|
|
29
32
|
}
|
package/dist/flow-instance.js
CHANGED
|
@@ -9,6 +9,7 @@ export class FlowInstance {
|
|
|
9
9
|
createdAt;
|
|
10
10
|
expiresAt;
|
|
11
11
|
_exitState;
|
|
12
|
+
_activeSubFlow = null;
|
|
12
13
|
constructor(id, sessionId, definition, context, currentState, expiresAt) {
|
|
13
14
|
this.id = id;
|
|
14
15
|
this.sessionId = sessionId;
|
|
@@ -45,8 +46,10 @@ export class FlowInstance {
|
|
|
45
46
|
get version() { return this._version; }
|
|
46
47
|
get exitState() { return this._exitState; }
|
|
47
48
|
get isCompleted() { return this._exitState !== null; }
|
|
49
|
+
get activeSubFlow() { return this._activeSubFlow; }
|
|
48
50
|
/** @internal */ transitionTo(state) { this._currentState = state; }
|
|
49
51
|
/** @internal */ incrementGuardFailure() { this._guardFailureCount++; }
|
|
50
52
|
/** @internal */ complete(exitState) { this._exitState = exitState; }
|
|
51
53
|
/** @internal */ setVersion(version) { this._version = version; }
|
|
54
|
+
/** @internal */ setActiveSubFlow(sub) { this._activeSubFlow = sub; }
|
|
52
55
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { Tramli } from './tramli.js';
|
|
|
2
2
|
export { FlowEngine } from './flow-engine.js';
|
|
3
3
|
export { FlowContext } from './flow-context.js';
|
|
4
4
|
export { FlowInstance } from './flow-instance.js';
|
|
5
|
-
export { FlowDefinition, Builder, FromBuilder, BranchBuilder } from './flow-definition.js';
|
|
5
|
+
export { FlowDefinition, Builder, FromBuilder, BranchBuilder, SubFlowBuilder } from './flow-definition.js';
|
|
6
6
|
export { FlowError } from './flow-error.js';
|
|
7
7
|
export { InMemoryFlowStore } from './in-memory-flow-store.js';
|
|
8
8
|
export type { TransitionRecord } from './in-memory-flow-store.js';
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { Tramli } from './tramli.js';
|
|
|
2
2
|
export { FlowEngine } from './flow-engine.js';
|
|
3
3
|
export { FlowContext } from './flow-context.js';
|
|
4
4
|
export { FlowInstance } from './flow-instance.js';
|
|
5
|
-
export { FlowDefinition, Builder, FromBuilder, BranchBuilder } from './flow-definition.js';
|
|
5
|
+
export { FlowDefinition, Builder, FromBuilder, BranchBuilder, SubFlowBuilder } from './flow-definition.js';
|
|
6
6
|
export { FlowError } from './flow-error.js';
|
|
7
7
|
export { InMemoryFlowStore } from './in-memory-flow-store.js';
|
|
8
8
|
export { MermaidGenerator } from './mermaid-generator.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type GuardOutput = {
|
|
|
16
16
|
type: 'expired';
|
|
17
17
|
};
|
|
18
18
|
/** Transition types. */
|
|
19
|
-
export type TransitionType = 'auto' | 'external' | 'branch';
|
|
19
|
+
export type TransitionType = 'auto' | 'external' | 'branch' | 'sub_flow';
|
|
20
20
|
/** A single transition in the flow definition. */
|
|
21
21
|
export interface Transition<S extends string> {
|
|
22
22
|
from: S;
|
|
@@ -26,6 +26,8 @@ export interface Transition<S extends string> {
|
|
|
26
26
|
guard?: TransitionGuard<S>;
|
|
27
27
|
branch?: BranchProcessor<S>;
|
|
28
28
|
branchTargets: Map<string, S>;
|
|
29
|
+
subFlowDefinition?: import('./flow-definition.js').FlowDefinition<any>;
|
|
30
|
+
exitMappings?: Map<string, S>;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* Processes a state transition.
|