@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.
Files changed (30) hide show
  1. package/__tests__/cron-route-loop-templateparams.test.ts +5 -4
  2. package/__tests__/custom-js-loop-scope.test.ts +10 -5
  3. package/__tests__/loop-collection-unwrap.test.ts +6 -1
  4. package/__tests__/server-loop-body-branches.test.ts +188 -0
  5. package/__tests__/server-nested-loops.test.ts +392 -0
  6. package/__tests__/streaming-route-if-branch.test.ts +107 -0
  7. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  8. package/dist/cjs/api-route-generator.js +57 -251
  9. package/dist/cjs/api-route-generator.js.map +1 -1
  10. package/dist/cjs/executor-generator.d.ts.map +1 -1
  11. package/dist/cjs/executor-generator.js +22 -0
  12. package/dist/cjs/executor-generator.js.map +1 -1
  13. package/dist/cjs/server-runtime-code.d.ts.map +1 -1
  14. package/dist/cjs/server-runtime-code.js +67 -0
  15. package/dist/cjs/server-runtime-code.js.map +1 -1
  16. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  17. package/dist/esm/api-route-generator.d.ts.map +1 -1
  18. package/dist/esm/api-route-generator.js +57 -251
  19. package/dist/esm/api-route-generator.js.map +1 -1
  20. package/dist/esm/executor-generator.d.ts.map +1 -1
  21. package/dist/esm/executor-generator.js +22 -0
  22. package/dist/esm/executor-generator.js.map +1 -1
  23. package/dist/esm/server-runtime-code.d.ts.map +1 -1
  24. package/dist/esm/server-runtime-code.js +67 -0
  25. package/dist/esm/server-runtime-code.js.map +1 -1
  26. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +2 -2
  28. package/src/api-route-generator.ts +57 -251
  29. package/src/executor-generator.ts +22 -0
  30. 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,MA0XF,CAAA;AAED,eAAO,MAAM,kBAAkB,YAAa,eAAe,KAAG,OAE7D,CAAA;AAED,eAAO,MAAM,sCAAsC,YACxC,eAAe,iBACT,MAAM,eACR,sBAAsB,YAC1B,yBAAyB,KACjC,MA0cF,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;AAuTD,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"}
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
- var skipHandle = condResult ? 'false' : 'true';
338
- var segEdgesIf = SEGMENT_CONFIG.edges || [];
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
- // Loop nodes must be handled specially: run the loop body nodes once per
359
- // pass of the loop type (see createLoopSteps), then continue after the loop.
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 loopEdges = SEGMENT_CONFIG.edges.filter(function(e) { return e.source === node.id; });
362
- const loopBodyEdge = loopEdges.find(function(e) { return e.sourceHandle === 'loop'; });
363
- const exitEdge = loopEdges.find(function(e) { return e.sourceHandle === 'exit'; });
364
-
365
- // Collect loop body node IDs by following loop-body edges
366
- const bodyNodeIds = {};
367
- if (loopBodyEdge) {
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
- if (__loopType === 'map') {
435
- loopResults.push(__lastBodyNode ? context[__lastBodyNode.id] : context[node.id]);
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
- context.__loopScopeStack.pop();
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 ssLoopEdges = SEGMENT_CONFIG.edges.filter(function(e) { return e.source === node.id; });
743
- var ssLoopBodyEdge = ssLoopEdges.find(function(e) { return e.sourceHandle === 'loop'; });
744
- var ssBodyNodeIds = {};
745
- if (ssLoopBodyEdge) {
746
- var sslq = [ssLoopBodyEdge.target];
747
- while (sslq.length > 0) {
748
- var sslnid = sslq.shift();
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
- context.__loopScopeStack.pop();
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
- if (bResult && bResult.__customNode) {
1463
- var bcnFn = typeof __customNodes !== 'undefined' ? __customNodes[bResult.customNodeId] : null;
1464
- context[bNode.id] = bcnFn ? await bcnFn(context, bResult.parameters, nodeHandlers) : null;
1465
- continue;
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
- var skipHandle = condResult ? 'false' : 'true';
1486
- var cfgEdges = ${configVar}.edges || [];
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 loopEdges = ${configVar}.edges.filter(function(e) { return e.source === node.id; });
1507
- var loopBodyEdge = loopEdges.find(function(e) { return e.sourceHandle === 'loop'; });
1508
-
1509
- var bodyNodeIds = {};
1510
- if (loopBodyEdge) {
1511
- var lQueue = [loopBodyEdge.target];
1512
- while (lQueue.length > 0) {
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
- if (__wfLoopType === 'map') {
1568
- loopResults.push(__wfLastBodyNode ? context[__wfLastBodyNode.id] : context[node.id]);
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);