@teleporthq/teleport-plugin-next-workflows 0.45.15 → 0.45.16
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/__tests__/cron-route-loop-templateparams.test.ts +5 -4
- package/__tests__/custom-js-loop-scope.test.ts +10 -5
- package/__tests__/loop-collection-unwrap.test.ts +6 -1
- package/__tests__/server-loop-body-branches.test.ts +188 -0
- package/__tests__/server-nested-loops.test.ts +392 -0
- package/__tests__/streaming-route-if-branch.test.ts +107 -0
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +57 -251
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +22 -0
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/server-runtime-code.d.ts.map +1 -1
- package/dist/cjs/server-runtime-code.js +67 -0
- package/dist/cjs/server-runtime-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +57 -251
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +22 -0
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/server-runtime-code.d.ts.map +1 -1
- package/dist/esm/server-runtime-code.js +67 -0
- package/dist/esm/server-runtime-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/api-route-generator.ts +57 -251
- package/src/executor-generator.ts +22 -0
- package/src/server-runtime-code.ts +67 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { generateStreamingServerSegmentAPIRoute } from '../src/api-route-generator'
|
|
2
|
+
import type { WorkflowSegment } from '../src/types'
|
|
3
|
+
import { loadServerRuntime } from './_helpers/load-server-runtime'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A streaming segment route evaluated an if-statement and then ran both of its
|
|
7
|
+
* branches: it never recorded the branch not taken, and never read the record
|
|
8
|
+
* a switch made of its unmatched cases.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const RUNS_KEY = '__streamingIfBranchRuns'
|
|
12
|
+
|
|
13
|
+
const runs = (): string[] => (globalThis as Record<string, any>)[RUNS_KEY] || []
|
|
14
|
+
|
|
15
|
+
const recordingNode = (id: string, stepNumber: number) => ({
|
|
16
|
+
id,
|
|
17
|
+
type: 'general-custom-js',
|
|
18
|
+
config: {
|
|
19
|
+
code: [
|
|
20
|
+
'function customHandler() {',
|
|
21
|
+
` globalThis.${RUNS_KEY} = (globalThis.${RUNS_KEY} || []).concat('${id}');`,
|
|
22
|
+
' return { ok: true };',
|
|
23
|
+
'}',
|
|
24
|
+
].join('\n'),
|
|
25
|
+
context: 'server',
|
|
26
|
+
},
|
|
27
|
+
stepNumber,
|
|
28
|
+
label: id,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const segmentWhere = (leftValue: number): WorkflowSegment =>
|
|
32
|
+
({
|
|
33
|
+
id: 'seg-if',
|
|
34
|
+
env: 'server',
|
|
35
|
+
nodeIds: ['if-1', 'yes-1', 'no-1'],
|
|
36
|
+
nodes: [
|
|
37
|
+
{
|
|
38
|
+
id: 'if-1',
|
|
39
|
+
type: 'general-if-statement',
|
|
40
|
+
config: { conditionType: 'simple-comparison', leftValue, operator: '===', rightValue: 1 },
|
|
41
|
+
stepNumber: 1,
|
|
42
|
+
label: 'Is One',
|
|
43
|
+
},
|
|
44
|
+
recordingNode('yes-1', 2),
|
|
45
|
+
recordingNode('no-1', 3),
|
|
46
|
+
],
|
|
47
|
+
edges: [
|
|
48
|
+
{ id: 'e1', source: 'if-1', target: 'yes-1', sourceHandle: 'true' },
|
|
49
|
+
{ id: 'e2', source: 'if-1', target: 'no-1', sourceHandle: 'false' },
|
|
50
|
+
],
|
|
51
|
+
} as unknown as WorkflowSegment)
|
|
52
|
+
|
|
53
|
+
const run = async (segment: WorkflowSegment) => {
|
|
54
|
+
const runtime = loadServerRuntime()
|
|
55
|
+
const routeModule: { exports: any } = { exports: {} }
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
57
|
+
new Function('module', 'exports', 'require', generateStreamingServerSegmentAPIRoute(segment))(
|
|
58
|
+
routeModule,
|
|
59
|
+
routeModule.exports,
|
|
60
|
+
(id: string): unknown => {
|
|
61
|
+
if (id.endsWith('/utils/workflows/server-runtime')) {
|
|
62
|
+
return runtime
|
|
63
|
+
}
|
|
64
|
+
throw new Error('Unexpected require: ' + id)
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
const reply = { status: 0 }
|
|
68
|
+
const res = {
|
|
69
|
+
status(code: number) {
|
|
70
|
+
reply.status = code
|
|
71
|
+
return this
|
|
72
|
+
},
|
|
73
|
+
json() {
|
|
74
|
+
return this
|
|
75
|
+
},
|
|
76
|
+
setHeader: (): void => undefined,
|
|
77
|
+
}
|
|
78
|
+
await routeModule.exports(
|
|
79
|
+
{ method: 'POST', body: { context: {} }, headers: { host: 'site.example' } },
|
|
80
|
+
res
|
|
81
|
+
)
|
|
82
|
+
return reply
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
delete (globalThis as Record<string, any>)[RUNS_KEY]
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
afterAll(() => {
|
|
90
|
+
delete (globalThis as Record<string, any>)[RUNS_KEY]
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('a streaming server segment route runs one branch of an if-statement', () => {
|
|
94
|
+
it('runs only the true branch when the condition holds', async () => {
|
|
95
|
+
const reply = await run(segmentWhere(1))
|
|
96
|
+
|
|
97
|
+
expect(reply.status).toBe(200)
|
|
98
|
+
expect(runs()).toEqual(['yes-1'])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('runs only the false branch when it does not', async () => {
|
|
102
|
+
const reply = await run(segmentWhere(2))
|
|
103
|
+
|
|
104
|
+
expect(reply.status).toBe(200)
|
|
105
|
+
expect(runs()).toEqual(['no-1'])
|
|
106
|
+
})
|
|
107
|
+
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-route-generator.d.ts","sourceRoot":"","sources":["../../src/api-route-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EAGb,iBAAiB,EACjB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAA;AAuEvE,MAAM,WAAW,yBAAyB;IACxC,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAA;CACrD;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAA;AAEhC,wEAAwE;AACxE,eAAO,MAAM,wBAAwB;;;CAAiC,CAAA;AAEtE,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,4CAA4C,CAAA;AA+O7E,eAAO,MAAM,6BAA6B,YAC/B,eAAe,iBACT,MAAM,eACR,sBAAsB,YAC1B,yBAAyB,KACjC,
|
|
1
|
+
{"version":3,"file":"api-route-generator.d.ts","sourceRoot":"","sources":["../../src/api-route-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EAGb,iBAAiB,EACjB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAA;AAuEvE,MAAM,WAAW,yBAAyB;IACxC,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAA;CACrD;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAA;AAEhC,wEAAwE;AACxE,eAAO,MAAM,wBAAwB;;;CAAiC,CAAA;AAEtE,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,4CAA4C,CAAA;AA+O7E,eAAO,MAAM,6BAA6B,YAC/B,eAAe,iBACT,MAAM,eACR,sBAAsB,YAC1B,yBAAyB,KACjC,MAuSF,CAAA;AAED,eAAO,MAAM,kBAAkB,YAAa,eAAe,KAAG,OAE7D,CAAA;AAED,eAAO,MAAM,sCAAsC,YACxC,eAAe,iBACT,MAAM,eACR,sBAAsB,YAC1B,yBAAyB,KACjC,MA+ZF,CAAA;AA6ID;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,aA4D5B,CAAA;AAEF,qDAAqD;AACrD,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAqBD,eAAO,MAAM,oBAAoB,aACrB,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,YAC3C,0BAA0B,KAClC,MA0HF,CAAA;AAED,eAAO,MAAM,mBAAmB,eAClB,MAAM,aACP,MAAM,iBACF,MAAM,KACpB,MA4BF,CAAA;AAED,eAAO,MAAM,oBAAoB,aAAc,YAAY,KAAG,MAa7D,CAAA;AAmPD,uHAAuH;AACvH,eAAO,MAAM,uBAAuB,kBAAmB,iBAAiB,KAAG,OAES,CAAA;AAEpF,eAAO,MAAM,+BAA+B,aAChC,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,YAC3C,0BAA0B,KAClC,MAkOF,CAAA;AAkED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,uBAAuB,aACxB,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,KACnD,MA8KF,CAAA;AAED,eAAO,MAAM,uBAAuB,aAAc,YAAY,KAAG,MAiBhE,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,cAC1B,aAAa,GAAG,SAAS,cACxB,MAAM,KACjB,MAAM,GAAG,IAYX,CAAA;AAED,eAAO,MAAM,mBAAmB,aAAc,YAAY,KAAG,MAAM,EAqBlE,CAAA"}
|
|
@@ -319,6 +319,8 @@ module.exports = async function handler(req, res) {
|
|
|
319
319
|
// the caller as if that branch had run (a custom node answers the output
|
|
320
320
|
// of the last node it ran).
|
|
321
321
|
if (context.__skippedNodes && context.__skippedNodes[node.id]) continue;
|
|
322
|
+
// A node of a loop body ran inside its loop, an if-statement included.
|
|
323
|
+
if (context.__loopBodyNodeIds && context.__loopBodyNodeIds[node.id]) continue;
|
|
322
324
|
const resolved = resolveNodeConfig(node, context);
|
|
323
325
|
// Every data node lives in a SERVER segment, so this guard — which turns an
|
|
324
326
|
// unresolved {{…}} into a validation error instead of letting the literal
|
|
@@ -334,119 +336,34 @@ module.exports = async function handler(req, res) {
|
|
|
334
336
|
if (node.type === 'general-if-statement') {
|
|
335
337
|
const condResult = utils.evaluateCondition(resolved, context, node.config);
|
|
336
338
|
context[node.id] = { result: condResult };
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
var skipEdge = segEdgesIf.find(function(e) { return e.source === node.id && e.sourceHandle === skipHandle; });
|
|
340
|
-
if (skipEdge) {
|
|
341
|
-
// BFS to collect all nodes in the non-taken branch
|
|
342
|
-
var skipIds = {};
|
|
343
|
-
var skipQueue = [skipEdge.target];
|
|
344
|
-
while (skipQueue.length > 0) {
|
|
345
|
-
var sid = skipQueue.shift();
|
|
346
|
-
if (skipIds[sid] || sid === node.id) continue;
|
|
347
|
-
skipIds[sid] = true;
|
|
348
|
-
segEdgesIf.forEach(function(e) {
|
|
349
|
-
if (e.source === sid && !skipIds[e.target] && e.target !== node.id) skipQueue.push(e.target);
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
if (!context.__skippedNodes) context.__skippedNodes = {};
|
|
353
|
-
Object.assign(context.__skippedNodes, skipIds);
|
|
354
|
-
}
|
|
339
|
+
if (!context.__skippedNodes) context.__skippedNodes = {};
|
|
340
|
+
Object.assign(context.__skippedNodes, utils.nonTakenBranchNodeIds(SEGMENT_CONFIG.edges, node.id, condResult));
|
|
355
341
|
continue;
|
|
356
342
|
}
|
|
357
343
|
|
|
358
|
-
//
|
|
359
|
-
//
|
|
344
|
+
// A loop runs its body once per pass of its type, a loop inside the body
|
|
345
|
+
// included (see runServerLoop in the server runtime).
|
|
360
346
|
if (node.type === 'general-loop') {
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const queue = [loopBodyEdge.target];
|
|
369
|
-
while (queue.length > 0) {
|
|
370
|
-
const nid = queue.shift();
|
|
371
|
-
if (bodyNodeIds[nid] || nid === node.id) continue;
|
|
372
|
-
bodyNodeIds[nid] = true;
|
|
373
|
-
SEGMENT_CONFIG.edges.forEach(function(e) {
|
|
374
|
-
if (e.source === nid && (e.sourceHandle === 'loop-body-out' || !e.sourceHandle)) {
|
|
375
|
-
if (e.target !== node.id) queue.push(e.target);
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
const bodyNodes = sortedNodes.filter(function(n) { return bodyNodeIds[n.id]; });
|
|
381
|
-
// For loopType=map, the schema (general-loop in node-context-schemas.json)
|
|
382
|
-
// defines results as the array of last body node outputs (same as
|
|
383
|
-
// Array.prototype.map). Pick the last body node so downstream nodes
|
|
384
|
-
// (e.g. state-update-global-state) consume the transformed value
|
|
385
|
-
// instead of the per-iteration scaffolding object.
|
|
386
|
-
const __loopType = resolved.loopType || 'forEach';
|
|
387
|
-
const __lastBodyNode = bodyNodes[bodyNodes.length - 1];
|
|
388
|
-
|
|
389
|
-
// Resolve the collection to iterate over. unwrapWorkflowCollection
|
|
390
|
-
// handles the common envelope shape that helper nodes return —
|
|
391
|
-
// transform-array { result, ... }, integration-airtable { records, ... },
|
|
392
|
-
// utility-semantic-search { matches, ... } — by unwrapping a single
|
|
393
|
-
// array property when the upstream binding pointed at the parent node
|
|
394
|
-
// instead of the inner field. See runtime-utils for the full rules.
|
|
395
|
-
var collection = utils.unwrapWorkflowCollection(resolved.collection);
|
|
396
|
-
var loopResults = [];
|
|
397
|
-
|
|
398
|
-
// Track this loop's body scope so general-custom-js (and any handler
|
|
399
|
-
// that consults the scope stack) can build the params / innerParams
|
|
400
|
-
// arrays the workflow editor advertises: params = nodes BEFORE the
|
|
401
|
-
// outermost loop, innerParamsN = nodes inside the loop at depth N.
|
|
402
|
-
if (!context.__loopScopeStack) context.__loopScopeStack = [];
|
|
403
|
-
context.__loopScopeStack.push({ loopNodeId: node.id, bodyNodeIds: bodyNodeIds });
|
|
404
|
-
// Persistent registry of every loop scaffold node we've entered, so
|
|
405
|
-
// a sibling that runs AFTER an inner loop completes still recognises
|
|
406
|
-
// that inner loop id as scaffolding (not as a regular workflow node).
|
|
407
|
-
if (!context.__loopNodeIds) context.__loopNodeIds = {};
|
|
408
|
-
context.__loopNodeIds[node.id] = true;
|
|
409
|
-
|
|
410
|
-
var loopSteps = utils.createLoopSteps(node, resolved, context, collection);
|
|
411
|
-
for (var loopPass = loopSteps.next(context); loopPass; loopPass = loopSteps.next(context)) {
|
|
412
|
-
context[loopPass.key] = loopPass.value;
|
|
413
|
-
for (var bi = 0; bi < bodyNodes.length; bi++) {
|
|
414
|
-
var bNode = bodyNodes[bi];
|
|
415
|
-
var bResolved = resolveNodeConfig(bNode, context);
|
|
416
|
-
bResolved.__nodeId = bNode.id;
|
|
417
|
-
if (bNode.type === 'general-if-statement') {
|
|
418
|
-
context[bNode.id] = { result: utils.evaluateCondition(bResolved, context, bNode.config) };
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
var bHandler = nodeHandlers[bNode.type];
|
|
422
|
-
if (!bHandler) continue;
|
|
423
|
-
if (utils.isFireAndForgetNode(bNode)) {
|
|
424
|
-
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(bNode, bHandler, bResolved, context));
|
|
425
|
-
context[bNode.id] = null;
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
var bResult = await bHandler(bResolved, context);
|
|
429
|
-
if (bResult && (bResult.success === false || (typeof bResult.error === 'string' && bResult.error))) {
|
|
430
|
-
throw new Error(bResult.error || 'Loop body node execution failed');
|
|
431
|
-
}
|
|
432
|
-
context[bNode.id] = bResult;
|
|
347
|
+
const bodyNodeIds = await utils.runServerLoop(node, resolved, sortedNodes, SEGMENT_CONFIG.edges, context, async function(bNode, bResolved) {
|
|
348
|
+
var bHandler = nodeHandlers[bNode.type];
|
|
349
|
+
if (!bHandler) return;
|
|
350
|
+
if (utils.isFireAndForgetNode(bNode)) {
|
|
351
|
+
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(bNode, bHandler, bResolved, context));
|
|
352
|
+
context[bNode.id] = null;
|
|
353
|
+
return;
|
|
433
354
|
}
|
|
434
|
-
|
|
435
|
-
|
|
355
|
+
var bResult = await bHandler(bResolved, context);
|
|
356
|
+
if (bResult && (bResult.success === false || (typeof bResult.error === 'string' && bResult.error))) {
|
|
357
|
+
throw new Error(bResult.error || 'Loop body node execution failed');
|
|
436
358
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
context[node.id] = { completed: true, iterations: loopSteps.iterations(), results: loopResults };
|
|
442
|
-
// Store body node IDs so the outer loop can skip them
|
|
359
|
+
context[bNode.id] = bResult;
|
|
360
|
+
});
|
|
361
|
+
// The walk below must not run a body node again.
|
|
443
362
|
if (!context.__loopBodyNodeIds) context.__loopBodyNodeIds = {};
|
|
444
363
|
Object.assign(context.__loopBodyNodeIds, bodyNodeIds);
|
|
445
364
|
continue;
|
|
446
365
|
}
|
|
447
366
|
|
|
448
|
-
// Skip nodes that were part of a loop body (already executed inside the loop)
|
|
449
|
-
if (context.__loopBodyNodeIds && context.__loopBodyNodeIds[node.id]) continue;
|
|
450
367
|
|
|
451
368
|
// Switch nodes evaluate a value, match a case, execute that branch, skip all others
|
|
452
369
|
if (node.type === 'general-switch') {
|
|
@@ -719,6 +636,8 @@ module.exports = async function handler(req, res) {
|
|
|
719
636
|
for (let i = 0; i < sortedNodes.length; i++) {
|
|
720
637
|
const node = sortedNodes[i];
|
|
721
638
|
if (executed[node.id]) continue;
|
|
639
|
+
// The branch an if-statement or a switch did not take never runs.
|
|
640
|
+
if (context.__skippedNodes && context.__skippedNodes[node.id]) continue;
|
|
722
641
|
const resolved = resolveNodeConfig(node, context);
|
|
723
642
|
// See the non-streaming segment above — without this the unresolved-token
|
|
724
643
|
// guard never runs on the server, which is where every data node executes.
|
|
@@ -731,72 +650,27 @@ module.exports = async function handler(req, res) {
|
|
|
731
650
|
const condResult = utils.evaluateCondition(resolved, context, node.config);
|
|
732
651
|
context[node.id] = { result: condResult };
|
|
733
652
|
executed[node.id] = true;
|
|
653
|
+
if (!context.__skippedNodes) context.__skippedNodes = {};
|
|
654
|
+
Object.assign(context.__skippedNodes, utils.nonTakenBranchNodeIds(SEGMENT_CONFIG.edges, node.id, condResult));
|
|
734
655
|
if (streamStarted) {
|
|
735
656
|
__streamNodeResult(res, node.id, context[node.id]);
|
|
736
657
|
}
|
|
737
658
|
continue;
|
|
738
659
|
}
|
|
739
660
|
|
|
740
|
-
// Loop nodes in streaming segments
|
|
661
|
+
// Loop nodes in streaming segments: same runner as the regular-segment
|
|
662
|
+
// loop above, a loop inside the body included.
|
|
741
663
|
if (node.type === 'general-loop') {
|
|
742
|
-
var
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (ssBodyNodeIds[sslnid] || sslnid === node.id) continue;
|
|
750
|
-
ssBodyNodeIds[sslnid] = true;
|
|
751
|
-
SEGMENT_CONFIG.edges.forEach(function(e) {
|
|
752
|
-
if (e.source === sslnid && (e.sourceHandle === 'loop-body-out' || !e.sourceHandle)) {
|
|
753
|
-
if (e.target !== node.id) sslq.push(e.target);
|
|
754
|
-
}
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
var ssBodyNodes = sortedNodes.filter(function(n) { return ssBodyNodeIds[n.id]; });
|
|
759
|
-
// See the regular-segment loop above: loopType=map must publish the
|
|
760
|
-
// last body node output as results[i] so the schema contract holds.
|
|
761
|
-
var ssLoopType = resolved.loopType || 'forEach';
|
|
762
|
-
var ssLastBodyNode = ssBodyNodes[ssBodyNodes.length - 1];
|
|
763
|
-
// Same envelope unwrap as the regular-segment loop above.
|
|
764
|
-
var ssLoopCollection = utils.unwrapWorkflowCollection(resolved.collection);
|
|
765
|
-
var ssLoopResults = [];
|
|
766
|
-
// Same loop-scope tracking as the regular-segment loop above. Custom-js
|
|
767
|
-
// bodies can read context.__loopScopeStack to recover the params /
|
|
768
|
-
// innerParams contract documented in workflow-utils.ts.
|
|
769
|
-
if (!context.__loopScopeStack) context.__loopScopeStack = [];
|
|
770
|
-
context.__loopScopeStack.push({ loopNodeId: node.id, bodyNodeIds: ssBodyNodeIds });
|
|
771
|
-
if (!context.__loopNodeIds) context.__loopNodeIds = {};
|
|
772
|
-
context.__loopNodeIds[node.id] = true;
|
|
773
|
-
var ssLoopSteps = utils.createLoopSteps(node, resolved, context, ssLoopCollection);
|
|
774
|
-
for (var ssLoopPass = ssLoopSteps.next(context); ssLoopPass; ssLoopPass = ssLoopSteps.next(context)) {
|
|
775
|
-
context[ssLoopPass.key] = ssLoopPass.value;
|
|
776
|
-
for (var ssbi = 0; ssbi < ssBodyNodes.length; ssbi++) {
|
|
777
|
-
var ssbNode = ssBodyNodes[ssbi];
|
|
778
|
-
var ssbRes = resolveNodeConfig(ssbNode, context);
|
|
779
|
-
ssbRes.__nodeId = ssbNode.id;
|
|
780
|
-
if (ssbNode.type === 'general-if-statement') {
|
|
781
|
-
context[ssbNode.id] = { result: utils.evaluateCondition(ssbRes, context, ssbNode.config) };
|
|
782
|
-
continue;
|
|
783
|
-
}
|
|
784
|
-
var ssbHandler = nodeHandlers[ssbNode.type];
|
|
785
|
-
if (!ssbHandler) continue;
|
|
786
|
-
if (utils.isFireAndForgetNode(ssbNode)) {
|
|
787
|
-
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(ssbNode, ssbHandler, ssbRes, context));
|
|
788
|
-
context[ssbNode.id] = null;
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
|
-
var ssbResult = await ssbHandler(ssbRes, context);
|
|
792
|
-
context[ssbNode.id] = ssbResult;
|
|
793
|
-
}
|
|
794
|
-
if (ssLoopType === 'map') {
|
|
795
|
-
ssLoopResults.push(ssLastBodyNode ? context[ssLastBodyNode.id] : context[node.id]);
|
|
664
|
+
var ssBodyNodeIds = await utils.runServerLoop(node, resolved, sortedNodes, SEGMENT_CONFIG.edges, context, async function(ssbNode, ssbRes) {
|
|
665
|
+
var ssbHandler = nodeHandlers[ssbNode.type];
|
|
666
|
+
if (!ssbHandler) return;
|
|
667
|
+
if (utils.isFireAndForgetNode(ssbNode)) {
|
|
668
|
+
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(ssbNode, ssbHandler, ssbRes, context));
|
|
669
|
+
context[ssbNode.id] = null;
|
|
670
|
+
return;
|
|
796
671
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
context[node.id] = { completed: true, iterations: ssLoopSteps.iterations(), results: ssLoopResults };
|
|
672
|
+
context[ssbNode.id] = await ssbHandler(ssbRes, context);
|
|
673
|
+
});
|
|
800
674
|
Object.keys(ssBodyNodeIds).forEach(function(k) { executed[k] = true; });
|
|
801
675
|
executed[node.id] = true;
|
|
802
676
|
if (streamStarted) {
|
|
@@ -1459,11 +1333,11 @@ const generateNodeExecutionLoop = (configVar, options) => {
|
|
|
1459
1333
|
// top-level one does: its handler only hands back the marker.
|
|
1460
1334
|
const loopBodyCustomNodeBlock = (options === null || options === void 0 ? void 0 : options.hasCustomNodes)
|
|
1461
1335
|
? `
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1336
|
+
if (bResult && bResult.__customNode) {
|
|
1337
|
+
var bcnFn = typeof __customNodes !== 'undefined' ? __customNodes[bResult.customNodeId] : null;
|
|
1338
|
+
context[bNode.id] = bcnFn ? await bcnFn(context, bResult.parameters, nodeHandlers) : null;
|
|
1339
|
+
return;
|
|
1340
|
+
}`
|
|
1467
1341
|
: '';
|
|
1468
1342
|
return ` var sortedNodes = ${configVar}.nodes.slice().sort(function(a, b) { return a.stepNumber - b.stepNumber; });
|
|
1469
1343
|
${resumeBlock}
|
|
@@ -1472,6 +1346,8 @@ ${resumeBlock}
|
|
|
1472
1346
|
// Never resolved nor run on a non-taken branch, an if-statement included
|
|
1473
1347
|
// (see the segment executor above).
|
|
1474
1348
|
if (context.__skippedNodes && context.__skippedNodes[node.id]) continue;
|
|
1349
|
+
// A node of a loop body ran inside its loop, an if-statement included.
|
|
1350
|
+
if (context.__loopBodyNodeIds && context.__loopBodyNodeIds[node.id]) continue;
|
|
1475
1351
|
var resolved = resolveNodeConfig(node, context);
|
|
1476
1352
|
// See the other segment executors — the unresolved-token guard must run
|
|
1477
1353
|
// wherever data nodes execute, and they all execute on the server.
|
|
@@ -1482,103 +1358,33 @@ ${resumeBlock}
|
|
|
1482
1358
|
if (node.type === 'general-if-statement') {
|
|
1483
1359
|
var condResult = utils.evaluateCondition(resolved, context, node.config);
|
|
1484
1360
|
context[node.id] = { result: condResult };
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
var skipEdge = cfgEdges.find(function(e) { return e.source === node.id && e.sourceHandle === skipHandle; });
|
|
1488
|
-
if (skipEdge) {
|
|
1489
|
-
var skipIds = {};
|
|
1490
|
-
var skipQueue = [skipEdge.target];
|
|
1491
|
-
while (skipQueue.length > 0) {
|
|
1492
|
-
var sid = skipQueue.shift();
|
|
1493
|
-
if (skipIds[sid] || sid === node.id) continue;
|
|
1494
|
-
skipIds[sid] = true;
|
|
1495
|
-
cfgEdges.forEach(function(e) {
|
|
1496
|
-
if (e.source === sid && !skipIds[e.target] && e.target !== node.id) skipQueue.push(e.target);
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
if (!context.__skippedNodes) context.__skippedNodes = {};
|
|
1500
|
-
Object.assign(context.__skippedNodes, skipIds);
|
|
1501
|
-
}
|
|
1361
|
+
if (!context.__skippedNodes) context.__skippedNodes = {};
|
|
1362
|
+
Object.assign(context.__skippedNodes, utils.nonTakenBranchNodeIds(${configVar}.edges, node.id, condResult));
|
|
1502
1363
|
continue;
|
|
1503
1364
|
}
|
|
1504
1365
|
|
|
1366
|
+
// Same runner as the segment routes, a loop inside the body included
|
|
1367
|
+
// (see runServerLoop in the server runtime).
|
|
1505
1368
|
if (node.type === 'general-loop') {
|
|
1506
|
-
var
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
var nid = lQueue.shift();
|
|
1514
|
-
if (bodyNodeIds[nid] || nid === node.id) continue;
|
|
1515
|
-
bodyNodeIds[nid] = true;
|
|
1516
|
-
${configVar}.edges.forEach(function(e) {
|
|
1517
|
-
if (e.source === nid && (e.sourceHandle === 'loop-body-out' || !e.sourceHandle)) {
|
|
1518
|
-
if (e.target !== node.id) lQueue.push(e.target);
|
|
1519
|
-
}
|
|
1520
|
-
});
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
var bodyNodes = sortedNodes.filter(function(n) { return bodyNodeIds[n.id]; });
|
|
1524
|
-
// For loopType=map, the schema (general-loop in node-context-schemas.json)
|
|
1525
|
-
// defines results as the array of last body node outputs (same as
|
|
1526
|
-
// Array.prototype.map). Pick the last body node so downstream nodes
|
|
1527
|
-
// (e.g. state-update-global-state) consume the transformed value.
|
|
1528
|
-
var __wfLoopType = resolved.loopType || 'forEach';
|
|
1529
|
-
var __wfLastBodyNode = bodyNodes[bodyNodes.length - 1];
|
|
1530
|
-
|
|
1531
|
-
// Same envelope unwrap as the regular-segment loop above.
|
|
1532
|
-
var collection = utils.unwrapWorkflowCollection(resolved.collection);
|
|
1533
|
-
var loopResults = [];
|
|
1534
|
-
|
|
1535
|
-
// Same loop-scope tracking as the regular-segment loop above so
|
|
1536
|
-
// general-custom-js inside the loop body sees the right params /
|
|
1537
|
-
// innerParams. Required for webhook + cron routes.
|
|
1538
|
-
if (!context.__loopScopeStack) context.__loopScopeStack = [];
|
|
1539
|
-
context.__loopScopeStack.push({ loopNodeId: node.id, bodyNodeIds: bodyNodeIds });
|
|
1540
|
-
if (!context.__loopNodeIds) context.__loopNodeIds = {};
|
|
1541
|
-
context.__loopNodeIds[node.id] = true;
|
|
1542
|
-
|
|
1543
|
-
var loopSteps = utils.createLoopSteps(node, resolved, context, collection);
|
|
1544
|
-
for (var loopPass = loopSteps.next(context); loopPass; loopPass = loopSteps.next(context)) {
|
|
1545
|
-
context[loopPass.key] = loopPass.value;
|
|
1546
|
-
for (var bi = 0; bi < bodyNodes.length; bi++) {
|
|
1547
|
-
var bNode = bodyNodes[bi];
|
|
1548
|
-
var bResolved = resolveNodeConfig(bNode, context);
|
|
1549
|
-
bResolved.__nodeId = bNode.id;
|
|
1550
|
-
if (bNode.type === 'general-if-statement') {
|
|
1551
|
-
context[bNode.id] = { result: utils.evaluateCondition(bResolved, context, bNode.config) };
|
|
1552
|
-
continue;
|
|
1553
|
-
}
|
|
1554
|
-
var bHandler = nodeHandlers[bNode.type];
|
|
1555
|
-
if (!bHandler) continue;
|
|
1556
|
-
if (utils.isFireAndForgetNode(bNode)) {
|
|
1557
|
-
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(bNode, bHandler, bResolved, context));
|
|
1558
|
-
context[bNode.id] = null;
|
|
1559
|
-
continue;
|
|
1560
|
-
}
|
|
1561
|
-
var bResult = await bHandler(bResolved, context);${loopBodyCustomNodeBlock}
|
|
1562
|
-
if (bResult && (bResult.success === false || (typeof bResult.error === 'string' && bResult.error))) {
|
|
1563
|
-
throw new Error(bResult.error || 'Loop body node execution failed');
|
|
1564
|
-
}
|
|
1565
|
-
context[bNode.id] = bResult;
|
|
1369
|
+
var bodyNodeIds = await utils.runServerLoop(node, resolved, sortedNodes, ${configVar}.edges, context, async function(bNode, bResolved) {
|
|
1370
|
+
var bHandler = nodeHandlers[bNode.type];
|
|
1371
|
+
if (!bHandler) return;
|
|
1372
|
+
if (utils.isFireAndForgetNode(bNode)) {
|
|
1373
|
+
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(bNode, bHandler, bResolved, context));
|
|
1374
|
+
context[bNode.id] = null;
|
|
1375
|
+
return;
|
|
1566
1376
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1377
|
+
var bResult = await bHandler(bResolved, context);${loopBodyCustomNodeBlock}
|
|
1378
|
+
if (bResult && (bResult.success === false || (typeof bResult.error === 'string' && bResult.error))) {
|
|
1379
|
+
throw new Error(bResult.error || 'Loop body node execution failed');
|
|
1569
1380
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
context.__loopScopeStack.pop();
|
|
1573
|
-
|
|
1574
|
-
context[node.id] = { completed: true, iterations: loopSteps.iterations(), results: loopResults };
|
|
1381
|
+
context[bNode.id] = bResult;
|
|
1382
|
+
});
|
|
1575
1383
|
if (!context.__loopBodyNodeIds) context.__loopBodyNodeIds = {};
|
|
1576
1384
|
Object.assign(context.__loopBodyNodeIds, bodyNodeIds);
|
|
1577
1385
|
continue;
|
|
1578
1386
|
}
|
|
1579
1387
|
|
|
1580
|
-
if (context.__loopBodyNodeIds && context.__loopBodyNodeIds[node.id]) continue;
|
|
1581
|
-
|
|
1582
1388
|
if (node.type === 'general-switch') {
|
|
1583
1389
|
// Only an authored case expression is compiled (see matchSwitchCase).
|
|
1584
1390
|
var swMatchedCase = utils.matchSwitchCase(node.config, resolved, context);
|