@unlaxer/tramli 1.1.0 → 1.2.1
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/flow-definition.d.ts +13 -0
- package/dist/flow-definition.js +83 -1
- package/dist/flow-engine.d.ts +2 -11
- package/dist/flow-engine.js +88 -11
- 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
|
@@ -47,6 +47,9 @@ export declare class Builder<S extends string> {
|
|
|
47
47
|
private checkRequiresProducesFrom;
|
|
48
48
|
private checkAutoExternalConflict;
|
|
49
49
|
private checkTerminalNoOutgoing;
|
|
50
|
+
private checkSubFlowNestingDepth;
|
|
51
|
+
private checkSubFlowCircularRef;
|
|
52
|
+
private checkSubFlowExitCompleteness;
|
|
50
53
|
}
|
|
51
54
|
export declare class FromBuilder<S extends string> {
|
|
52
55
|
private readonly builder;
|
|
@@ -55,6 +58,16 @@ export declare class FromBuilder<S extends string> {
|
|
|
55
58
|
auto(to: S, processor: StateProcessor<S>): Builder<S>;
|
|
56
59
|
external(to: S, guard: TransitionGuard<S>, processor?: StateProcessor<S>): Builder<S>;
|
|
57
60
|
branch(branch: BranchProcessor<S>): BranchBuilder<S>;
|
|
61
|
+
subFlow(subFlowDef: FlowDefinition<any>): SubFlowBuilder<S>;
|
|
62
|
+
}
|
|
63
|
+
export declare class SubFlowBuilder<S extends string> {
|
|
64
|
+
private readonly builder;
|
|
65
|
+
private readonly fromState;
|
|
66
|
+
private readonly subFlowDef;
|
|
67
|
+
private readonly exitMap;
|
|
68
|
+
constructor(builder: Builder<S>, fromState: S, subFlowDef: FlowDefinition<any>);
|
|
69
|
+
onExit(terminalName: string, parentState: S): this;
|
|
70
|
+
endSubFlow(): Builder<S>;
|
|
58
71
|
}
|
|
59
72
|
export declare class BranchBuilder<S extends string> {
|
|
60
73
|
private readonly builder;
|
package/dist/flow-definition.js
CHANGED
|
@@ -118,6 +118,9 @@ 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);
|
|
122
|
+
this.checkSubFlowNestingDepth(def, errors, 0);
|
|
123
|
+
this.checkSubFlowCircularRef(def, errors, new Set());
|
|
121
124
|
if (errors.length > 0) {
|
|
122
125
|
throw new FlowError('INVALID_FLOW_DEFINITION', `Flow '${this.name}' has ${errors.length} validation error(s):\n - ${errors.join('\n - ')}`);
|
|
123
126
|
}
|
|
@@ -131,6 +134,15 @@ export class Builder {
|
|
|
131
134
|
while (queue.length > 0) {
|
|
132
135
|
const current = queue.shift();
|
|
133
136
|
for (const t of def.transitionsFrom(current)) {
|
|
137
|
+
if (t.type === 'sub_flow' && t.exitMappings) {
|
|
138
|
+
for (const target of t.exitMappings.values()) {
|
|
139
|
+
if (!visited.has(target)) {
|
|
140
|
+
visited.add(target);
|
|
141
|
+
queue.push(target);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
134
146
|
if (!visited.has(t.to)) {
|
|
135
147
|
visited.add(t.to);
|
|
136
148
|
queue.push(t.to);
|
|
@@ -163,6 +175,13 @@ export class Builder {
|
|
|
163
175
|
return false;
|
|
164
176
|
visited.add(state);
|
|
165
177
|
for (const t of def.transitionsFrom(state)) {
|
|
178
|
+
if (t.type === 'sub_flow' && t.exitMappings) {
|
|
179
|
+
for (const target of t.exitMappings.values()) {
|
|
180
|
+
if (this.canReachTerminal(def, target, visited))
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
166
185
|
if (this.canReachTerminal(def, t.to, visited))
|
|
167
186
|
return true;
|
|
168
187
|
}
|
|
@@ -300,11 +319,46 @@ export class Builder {
|
|
|
300
319
|
}
|
|
301
320
|
checkTerminalNoOutgoing(def, errors) {
|
|
302
321
|
for (const t of def.transitions) {
|
|
303
|
-
if (def.stateConfig[t.from].terminal) {
|
|
322
|
+
if (def.stateConfig[t.from].terminal && t.type !== 'sub_flow') {
|
|
304
323
|
errors.push(`Terminal state ${t.from} has an outgoing transition to ${t.to}`);
|
|
305
324
|
}
|
|
306
325
|
}
|
|
307
326
|
}
|
|
327
|
+
checkSubFlowNestingDepth(def, errors, depth) {
|
|
328
|
+
if (depth > 3) {
|
|
329
|
+
errors.push(`SubFlow nesting depth exceeds maximum of 3 (flow: ${def.name})`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
for (const t of def.transitions) {
|
|
333
|
+
if (t.type === 'sub_flow' && t.subFlowDefinition) {
|
|
334
|
+
this.checkSubFlowNestingDepth(t.subFlowDefinition, errors, depth + 1);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
checkSubFlowCircularRef(def, errors, visited) {
|
|
339
|
+
if (visited.has(def.name)) {
|
|
340
|
+
errors.push(`Circular sub-flow reference detected: ${[...visited].join(' -> ')} -> ${def.name}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
visited.add(def.name);
|
|
344
|
+
for (const t of def.transitions) {
|
|
345
|
+
if (t.type === 'sub_flow' && t.subFlowDefinition) {
|
|
346
|
+
this.checkSubFlowCircularRef(t.subFlowDefinition, errors, new Set(visited));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
checkSubFlowExitCompleteness(def, errors) {
|
|
351
|
+
for (const t of def.transitions) {
|
|
352
|
+
if (t.type !== 'sub_flow' || !t.subFlowDefinition)
|
|
353
|
+
continue;
|
|
354
|
+
const subDef = t.subFlowDefinition;
|
|
355
|
+
for (const terminal of subDef.terminalStates) {
|
|
356
|
+
if (!t.exitMappings?.has(terminal)) {
|
|
357
|
+
errors.push(`SubFlow '${subDef.name}' at ${t.from} has terminal state ${terminal} with no onExit mapping`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
308
362
|
}
|
|
309
363
|
export class FromBuilder {
|
|
310
364
|
builder;
|
|
@@ -330,6 +384,34 @@ export class FromBuilder {
|
|
|
330
384
|
branch(branch) {
|
|
331
385
|
return new BranchBuilder(this.builder, this.fromState, branch);
|
|
332
386
|
}
|
|
387
|
+
subFlow(subFlowDef) {
|
|
388
|
+
return new SubFlowBuilder(this.builder, this.fromState, subFlowDef);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
export class SubFlowBuilder {
|
|
392
|
+
builder;
|
|
393
|
+
fromState;
|
|
394
|
+
subFlowDef;
|
|
395
|
+
exitMap = new Map();
|
|
396
|
+
constructor(builder, fromState, subFlowDef) {
|
|
397
|
+
this.builder = builder;
|
|
398
|
+
this.fromState = fromState;
|
|
399
|
+
this.subFlowDef = subFlowDef;
|
|
400
|
+
}
|
|
401
|
+
onExit(terminalName, parentState) {
|
|
402
|
+
this.exitMap.set(terminalName, parentState);
|
|
403
|
+
return this;
|
|
404
|
+
}
|
|
405
|
+
endSubFlow() {
|
|
406
|
+
this.builder.addTransition({
|
|
407
|
+
from: this.fromState, to: this.fromState, type: 'sub_flow',
|
|
408
|
+
processor: undefined, guard: undefined, branch: undefined,
|
|
409
|
+
branchTargets: new Map(),
|
|
410
|
+
subFlowDefinition: this.subFlowDef,
|
|
411
|
+
exitMappings: new Map(this.exitMap),
|
|
412
|
+
});
|
|
413
|
+
return this.builder;
|
|
414
|
+
}
|
|
333
415
|
}
|
|
334
416
|
export class BranchBuilder {
|
|
335
417
|
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,81 @@ 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
|
+
// Error bubbling: no exit mapping → fall back to parent's error transitions
|
|
167
|
+
this.handleError(parentFlow, parentFlow.currentState);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
return 0; // sub-flow stopped at external
|
|
171
|
+
}
|
|
172
|
+
async resumeSubFlow(parentFlow, parentDef) {
|
|
173
|
+
const subFlow = parentFlow.activeSubFlow;
|
|
174
|
+
const subDef = subFlow.definition;
|
|
175
|
+
const transition = subDef.externalFrom(subFlow.currentState);
|
|
176
|
+
if (!transition) {
|
|
177
|
+
throw new FlowError('INVALID_TRANSITION', `No external transition from sub-flow state ${subFlow.currentState}`);
|
|
178
|
+
}
|
|
179
|
+
const guard = transition.guard;
|
|
180
|
+
if (guard) {
|
|
181
|
+
const output = await guard.validate(parentFlow.context);
|
|
182
|
+
if (output.type === 'accepted') {
|
|
183
|
+
if (output.data) {
|
|
184
|
+
for (const [key, value] of output.data)
|
|
185
|
+
parentFlow.context.put(key, value);
|
|
186
|
+
}
|
|
187
|
+
subFlow.transitionTo(transition.to);
|
|
188
|
+
this.store.recordTransition(parentFlow.id, subFlow.currentState, transition.to, guard.name, parentFlow.context);
|
|
189
|
+
}
|
|
190
|
+
else if (output.type === 'rejected') {
|
|
191
|
+
subFlow.incrementGuardFailure();
|
|
192
|
+
if (subFlow.guardFailureCount >= subDef.maxGuardRetries) {
|
|
193
|
+
subFlow.complete('ERROR');
|
|
194
|
+
}
|
|
195
|
+
this.store.save(parentFlow);
|
|
196
|
+
return parentFlow;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
parentFlow.complete('EXPIRED');
|
|
200
|
+
this.store.save(parentFlow);
|
|
201
|
+
return parentFlow;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
subFlow.transitionTo(transition.to);
|
|
206
|
+
}
|
|
207
|
+
await this.executeAutoChain(subFlow);
|
|
208
|
+
if (subFlow.isCompleted) {
|
|
209
|
+
parentFlow.setActiveSubFlow(null);
|
|
210
|
+
const subFlowT = parentDef.transitionsFrom(parentFlow.currentState)
|
|
211
|
+
.find(t => t.type === 'sub_flow');
|
|
212
|
+
if (subFlowT?.exitMappings) {
|
|
213
|
+
const target = subFlowT.exitMappings.get(subFlow.exitState);
|
|
214
|
+
if (target) {
|
|
215
|
+
const from = parentFlow.currentState;
|
|
216
|
+
parentFlow.transitionTo(target);
|
|
217
|
+
this.store.recordTransition(parentFlow.id, from, target, `subFlow:${subDef.name}/${subFlow.exitState}`, parentFlow.context);
|
|
218
|
+
await this.executeAutoChain(parentFlow);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
this.store.save(parentFlow);
|
|
223
|
+
return parentFlow;
|
|
224
|
+
}
|
|
148
225
|
handleError(flow, fromState) {
|
|
149
226
|
const errorTarget = flow.definition.errorTransitions.get(fromState);
|
|
150
227
|
if (errorTarget) {
|
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.
|