@synergenius/flow-weaver 0.17.6 → 0.17.8

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 (52) hide show
  1. package/dist/api/parse.d.ts +5 -0
  2. package/dist/api/parse.js +4 -0
  3. package/dist/ast/types.d.ts +2 -0
  4. package/dist/cli/commands/compile.js +2 -1
  5. package/dist/cli/commands/init.js +15 -9
  6. package/dist/cli/commands/validate.js +1 -1
  7. package/dist/cli/exports.d.ts +17 -0
  8. package/dist/cli/exports.js +23 -0
  9. package/dist/cli/flow-weaver.mjs +59021 -62127
  10. package/dist/cli/templates/index.js +8 -1
  11. package/dist/extensions/index.d.ts +10 -6
  12. package/dist/extensions/index.js +11 -6
  13. package/dist/generated-version.d.ts +1 -1
  14. package/dist/generated-version.js +1 -1
  15. package/dist/generator/index.d.ts +11 -0
  16. package/dist/generator/index.js +11 -0
  17. package/dist/parser.d.ts +7 -0
  18. package/dist/parser.js +29 -0
  19. package/package.json +11 -7
  20. package/dist/extensions/cicd/base-target.d.ts +0 -110
  21. package/dist/extensions/cicd/base-target.js +0 -397
  22. package/dist/extensions/cicd/detection.d.ts +0 -33
  23. package/dist/extensions/cicd/detection.js +0 -88
  24. package/dist/extensions/cicd/docs/cicd.md +0 -395
  25. package/dist/extensions/cicd/index.d.ts +0 -15
  26. package/dist/extensions/cicd/index.js +0 -15
  27. package/dist/extensions/cicd/register.d.ts +0 -11
  28. package/dist/extensions/cicd/register.js +0 -62
  29. package/dist/extensions/cicd/rules.d.ts +0 -30
  30. package/dist/extensions/cicd/rules.js +0 -288
  31. package/dist/extensions/cicd/tag-handler.d.ts +0 -14
  32. package/dist/extensions/cicd/tag-handler.js +0 -504
  33. package/dist/extensions/cicd/templates/cicd-docker.d.ts +0 -9
  34. package/dist/extensions/cicd/templates/cicd-docker.js +0 -110
  35. package/dist/extensions/cicd/templates/cicd-matrix.d.ts +0 -9
  36. package/dist/extensions/cicd/templates/cicd-matrix.js +0 -112
  37. package/dist/extensions/cicd/templates/cicd-multi-env.d.ts +0 -9
  38. package/dist/extensions/cicd/templates/cicd-multi-env.js +0 -126
  39. package/dist/extensions/cicd/templates/cicd-test-deploy.d.ts +0 -11
  40. package/dist/extensions/cicd/templates/cicd-test-deploy.js +0 -156
  41. package/dist/extensions/inngest/dev-mode.d.ts +0 -9
  42. package/dist/extensions/inngest/dev-mode.js +0 -213
  43. package/dist/extensions/inngest/generator.d.ts +0 -53
  44. package/dist/extensions/inngest/generator.js +0 -1176
  45. package/dist/extensions/inngest/index.d.ts +0 -2
  46. package/dist/extensions/inngest/index.js +0 -2
  47. package/dist/extensions/inngest/register.d.ts +0 -6
  48. package/dist/extensions/inngest/register.js +0 -23
  49. package/dist/extensions/inngest/templates/ai-agent-durable.d.ts +0 -8
  50. package/dist/extensions/inngest/templates/ai-agent-durable.js +0 -334
  51. package/dist/extensions/inngest/templates/ai-pipeline-durable.d.ts +0 -8
  52. package/dist/extensions/inngest/templates/ai-pipeline-durable.js +0 -326
@@ -1,1176 +0,0 @@
1
- /**
2
- * Inngest Deep Code Generator
3
- *
4
- * Generates Inngest function code with per-node `step.run()` wrapping,
5
- * giving each node individual durability, retries, and checkpointing.
6
- *
7
- * This is a standalone generator alongside unified.ts — it shares the
8
- * control-flow analysis layer but produces fundamentally different output:
9
- * - Local `const` variables instead of `ctx.setVariable()/getVariable()`
10
- * - `step.run()` per non-expression node for durability
11
- * - `Promise.all()` for parallel independent nodes
12
- * - Indexed `step.run()` for forEach/scoped iteration
13
- * - Branching chain flattening for multi-way routing
14
- *
15
- * @module generator/inngest
16
- */
17
- import { toValidIdentifier } from '../../generator/code-utils.js';
18
- import { buildControlFlowGraph, detectBranchingChains, findAllBranchingNodes, findNodesInBranch, performKahnsTopologicalSort, isPerPortScopedChild, } from '../../generator/control-flow.js';
19
- import { RESERVED_PORT_NAMES, isStartNode, isExitNode, isExecutePort, isSuccessPort, isFailurePort, } from '../../constants.js';
20
- // ---------------------------------------------------------------------------
21
- // Built-in Node Detection
22
- // ---------------------------------------------------------------------------
23
- const BUILTIN_IMPORT_PREFIX = '@synergenius/flow-weaver/built-in-nodes';
24
- const BUILT_IN_HANDLERS = {
25
- delay: 'delay',
26
- waitForEvent: 'waitForEvent',
27
- waitForAgent: 'waitForAgent',
28
- invokeWorkflow: 'invokeWorkflow',
29
- };
30
- /**
31
- * Check if a node type is a built-in node.
32
- * Returns the built-in name (e.g. 'delay') or false.
33
- */
34
- function isBuiltInNode(nodeType) {
35
- // Primary: check import source
36
- if (nodeType.importSource?.startsWith(BUILTIN_IMPORT_PREFIX)) {
37
- return nodeType.functionName in BUILT_IN_HANDLERS ? nodeType.functionName : false;
38
- }
39
- // Fallback for test fixtures / same-file definitions:
40
- // Check if function name matches AND the node has the exact built-in signature
41
- if (nodeType.functionName in BUILT_IN_HANDLERS) {
42
- return verifyBuiltInSignature(nodeType) ? nodeType.functionName : false;
43
- }
44
- return false;
45
- }
46
- /**
47
- * Verify a node type has the exact built-in signature by checking its input port names.
48
- */
49
- function verifyBuiltInSignature(nodeType) {
50
- const inputNames = Object.keys(nodeType.inputs).filter((n) => n !== 'execute');
51
- switch (nodeType.functionName) {
52
- case 'delay':
53
- return inputNames.length === 1 && inputNames[0] === 'duration';
54
- case 'waitForEvent':
55
- return inputNames.includes('eventName');
56
- case 'invokeWorkflow':
57
- return inputNames.includes('functionId') && inputNames.includes('payload');
58
- case 'waitForAgent':
59
- return inputNames.includes('agentId') && inputNames.includes('context');
60
- default:
61
- return false;
62
- }
63
- }
64
- // ---------------------------------------------------------------------------
65
- // Typed Event Schema (Feature 1)
66
- // ---------------------------------------------------------------------------
67
- /**
68
- * Generate Zod event schema from workflow start ports (populated by @param).
69
- */
70
- function generateEventSchema(workflow, eventName) {
71
- const lines = [];
72
- const schemaFields = [];
73
- // workflow.startPorts maps @param annotations
74
- for (const [name, port] of Object.entries(workflow.startPorts || {})) {
75
- if (name === 'execute')
76
- continue; // Skip execute port
77
- const zodType = mapTypeToZod(port.tsType || port.dataType);
78
- schemaFields.push(` ${name}: ${zodType},`);
79
- }
80
- if (schemaFields.length === 0)
81
- return [];
82
- const varName = toValidIdentifier(workflow.functionName) + 'Event';
83
- lines.push(`const ${varName} = {`);
84
- lines.push(` name: '${eventName}',`);
85
- lines.push(` schema: z.object({`);
86
- lines.push(` data: z.object({`);
87
- lines.push(...schemaFields);
88
- lines.push(` }),`);
89
- lines.push(` }),`);
90
- lines.push(`};`);
91
- lines.push('');
92
- return lines;
93
- }
94
- /**
95
- * Map TypeScript/Flow Weaver types to Zod schema types.
96
- */
97
- function mapTypeToZod(type) {
98
- if (!type)
99
- return 'z.unknown()';
100
- const t = type.trim().toLowerCase();
101
- if (t === 'string')
102
- return 'z.string()';
103
- if (t === 'number')
104
- return 'z.number()';
105
- if (t === 'boolean')
106
- return 'z.boolean()';
107
- if (t === 'string[]')
108
- return 'z.array(z.string())';
109
- if (t === 'number[]')
110
- return 'z.array(z.number())';
111
- if (t === 'object' || t.startsWith('record<'))
112
- return 'z.record(z.unknown())';
113
- return `z.unknown() /* ${type} */`;
114
- }
115
- // ---------------------------------------------------------------------------
116
- // Helpers
117
- // ---------------------------------------------------------------------------
118
- /** Convert camelCase/PascalCase to kebab-case for Inngest IDs */
119
- function toKebabCase(name) {
120
- return name
121
- .replace(/([a-z])([A-Z])/g, '$1-$2')
122
- .replace(/[^a-zA-Z0-9-]/g, '-')
123
- .replace(/-+/g, '-')
124
- .toLowerCase();
125
- }
126
- /**
127
- * Resolve a port's value source for a given node instance.
128
- *
129
- * Unlike `buildNodeArgumentsWithContext` in code-utils.ts which emits
130
- * `ctx.getVariable()` calls, this returns a plain JS expression referencing
131
- * local `const` variables produced by earlier `step.run()` calls.
132
- */
133
- function resolvePortValue(portName, instanceId, nodeType, workflow, _nodeTypes) {
134
- const safeId = toValidIdentifier(instanceId);
135
- const portDef = nodeType.inputs[portName];
136
- const instance = workflow.instances.find((i) => i.id === instanceId);
137
- // Check for instance-level expression override
138
- const instancePortConfig = instance?.config?.portConfigs?.find((pc) => pc.portName === portName && (pc.direction == null || pc.direction === 'INPUT'));
139
- if (instancePortConfig?.expression !== undefined) {
140
- const expr = String(instancePortConfig.expression);
141
- const isFunction = expr.includes('=>') || expr.trim().startsWith('function');
142
- if (isFunction) {
143
- return `await (${expr})()`;
144
- }
145
- return expr;
146
- }
147
- // Check for connections
148
- const connections = workflow.connections.filter((conn) => conn.to.node === instanceId && conn.to.port === portName
149
- && !conn.from.scope && !conn.to.scope);
150
- if (connections.length > 0) {
151
- if (connections.length === 1) {
152
- const conn = connections[0];
153
- const sourceNode = conn.from.node;
154
- const sourcePort = conn.from.port;
155
- if (isStartNode(sourceNode)) {
156
- return `event.data.${sourcePort}`;
157
- }
158
- const safeSource = toValidIdentifier(sourceNode);
159
- return `${safeSource}_result.${sourcePort}`;
160
- }
161
- // Multiple connections — use first non-undefined (fan-in)
162
- const attempts = connections.map((conn) => {
163
- const sourceNode = conn.from.node;
164
- const sourcePort = conn.from.port;
165
- if (isStartNode(sourceNode)) {
166
- return `event.data.${sourcePort}`;
167
- }
168
- const safeSource = toValidIdentifier(sourceNode);
169
- return `${safeSource}_result?.${sourcePort}`;
170
- });
171
- return attempts.join(' ?? ');
172
- }
173
- // Check for node type expression
174
- if (portDef?.expression) {
175
- const expr = portDef.expression;
176
- const isFunction = expr.includes('=>') || expr.trim().startsWith('function');
177
- if (isFunction) {
178
- return `await (${expr})()`;
179
- }
180
- return expr;
181
- }
182
- // Default value
183
- if (portDef?.default !== undefined) {
184
- return JSON.stringify(portDef.default);
185
- }
186
- // Optional port
187
- if (portDef?.optional) {
188
- return 'undefined';
189
- }
190
- // No source — undefined with comment
191
- return `undefined /* no source for ${safeId}.${portName} */`;
192
- }
193
- /**
194
- * Build the argument list for calling a node function.
195
- * Returns array of JS expression strings to pass as function arguments.
196
- */
197
- function buildNodeArgs(instanceId, nodeType, workflow, nodeTypes) {
198
- const args = [];
199
- // Handle execute port (first arg for non-expression nodes)
200
- if (!nodeType.expression) {
201
- const executeConns = workflow.connections.filter((conn) => conn.to.node === instanceId && conn.to.port === 'execute'
202
- && !conn.from.scope && !conn.to.scope);
203
- if (executeConns.length > 0) {
204
- const conn = executeConns[0];
205
- if (isStartNode(conn.from.node)) {
206
- args.push('true');
207
- }
208
- else {
209
- // Delay nodes (step.sleep) have no result variable — use literal values
210
- const sourceNt = getNodeType(conn.from.node, workflow, nodeTypes);
211
- if (sourceNt && isBuiltInNode(sourceNt) === 'delay') {
212
- args.push(conn.from.port === 'onSuccess' ? 'true' : 'false');
213
- }
214
- else {
215
- const safeSource = toValidIdentifier(conn.from.node);
216
- args.push(`${safeSource}_result.${conn.from.port}`);
217
- }
218
- }
219
- }
220
- else {
221
- args.push('true');
222
- }
223
- }
224
- // Handle data ports
225
- for (const portName of Object.keys(nodeType.inputs)) {
226
- if (isExecutePort(portName))
227
- continue;
228
- if (nodeType.inputs[portName].scope)
229
- continue; // Skip scoped ports
230
- const value = resolvePortValue(portName, instanceId, nodeType, workflow, nodeTypes);
231
- args.push(value);
232
- }
233
- return args;
234
- }
235
- /**
236
- * Look up a node type for an instance.
237
- */
238
- function getNodeType(instanceId, workflow, nodeTypes) {
239
- const instance = workflow.instances.find((i) => i.id === instanceId);
240
- if (!instance)
241
- return undefined;
242
- return nodeTypes.find((nt) => nt.name === instance.nodeType || nt.functionName === instance.nodeType);
243
- }
244
- // ---------------------------------------------------------------------------
245
- // Parallel Detection
246
- // ---------------------------------------------------------------------------
247
- /**
248
- * Detect parallelizable groups within a list of node IDs.
249
- *
250
- * For each node, compute its direct predecessors within the given set.
251
- * Nodes with identical predecessor sets can execute in parallel.
252
- *
253
- * Returns an ordered list of groups preserving topological order.
254
- */
255
- function detectParallelInList(nodeIds, workflow, nodeTypes) {
256
- if (nodeIds.length <= 1)
257
- return nodeIds.map((n) => [n]);
258
- // Build mini-CFG: for each node, which other nodes in the list feed into it?
259
- const predecessors = new Map();
260
- const nodeSet = new Set(nodeIds);
261
- for (const nodeId of nodeIds) {
262
- const preds = new Set();
263
- for (const conn of workflow.connections) {
264
- if (conn.to.node !== nodeId)
265
- continue;
266
- if (conn.from.scope || conn.to.scope)
267
- continue;
268
- const fromNode = conn.from.node;
269
- if (nodeSet.has(fromNode)) {
270
- preds.add(fromNode);
271
- }
272
- }
273
- predecessors.set(nodeId, preds);
274
- }
275
- // Group by predecessor set (serialized for comparison)
276
- const groups = [];
277
- const processed = new Set();
278
- for (const nodeId of nodeIds) {
279
- if (processed.has(nodeId))
280
- continue;
281
- const preds = predecessors.get(nodeId);
282
- const predsKey = Array.from(preds).sort().join(',');
283
- const parallel = [nodeId];
284
- for (const other of nodeIds) {
285
- if (other === nodeId || processed.has(other))
286
- continue;
287
- const otherPreds = predecessors.get(other);
288
- const otherKey = Array.from(otherPreds).sort().join(',');
289
- if (predsKey === otherKey) {
290
- parallel.push(other);
291
- }
292
- }
293
- for (const n of parallel) {
294
- processed.add(n);
295
- }
296
- groups.push(parallel);
297
- }
298
- return groups;
299
- }
300
- // ---------------------------------------------------------------------------
301
- // Code Generation — Core Emitters
302
- // ---------------------------------------------------------------------------
303
- /**
304
- * Generate a step.run() call for a durable node.
305
- */
306
- function generateStepRunCall(instanceId, nodeType, workflow, nodeTypes, indent) {
307
- const args = buildNodeArgs(instanceId, nodeType, workflow, nodeTypes);
308
- const safeId = toValidIdentifier(instanceId);
309
- const fnCall = `${nodeType.functionName}(${args.join(', ')})`;
310
- const awaitPrefix = nodeType.isAsync ? 'await ' : '';
311
- return `${indent}${safeId}_result = await step.run('${instanceId}', async () => {\n` +
312
- `${indent} return ${awaitPrefix}${fnCall};\n` +
313
- `${indent}});`;
314
- }
315
- /**
316
- * Generate an inline call for an expression node (no step.run wrapper).
317
- * Coercion nodes (variant === 'COERCION') emit inline JS expressions
318
- * (e.g. String(value)) instead of function calls.
319
- */
320
- function generateExpressionCall(instanceId, nodeType, workflow, nodeTypes, indent) {
321
- const args = buildNodeArgs(instanceId, nodeType, workflow, nodeTypes);
322
- const safeId = toValidIdentifier(instanceId);
323
- if (nodeType.variant === 'COERCION') {
324
- const coerceExprMap = {
325
- __fw_toString: 'String',
326
- __fw_toNumber: 'Number',
327
- __fw_toBoolean: 'Boolean',
328
- __fw_toJSON: 'JSON.stringify',
329
- __fw_parseJSON: 'JSON.parse',
330
- };
331
- const coerceExpr = coerceExprMap[nodeType.functionName] || 'String';
332
- const valueArg = args[0] || 'undefined';
333
- return `${indent}${safeId}_result = ${coerceExpr}(${valueArg});`;
334
- }
335
- const fnCall = `${nodeType.functionName}(${args.join(', ')})`;
336
- const awaitPrefix = nodeType.isAsync ? 'await ' : '';
337
- return `${indent}${safeId}_result = ${awaitPrefix}${fnCall};`;
338
- }
339
- /**
340
- * Emit the step.run or expression call for a single node.
341
- * Built-in nodes (delay, waitForEvent, invokeWorkflow) are emitted with their
342
- * corresponding Inngest step primitives instead of step.run().
343
- */
344
- function emitNodeCall(nodeId, nodeType, workflow, nodeTypes, indent, lines) {
345
- const builtIn = isBuiltInNode(nodeType);
346
- if (builtIn === 'delay') {
347
- const args = buildNodeArgs(nodeId, nodeType, workflow, nodeTypes);
348
- const durationArg = args[1]; // args[0] is execute=true, args[1] is duration
349
- lines.push(`${indent}await step.sleep('${nodeId}', ${durationArg});`);
350
- lines.push('');
351
- return;
352
- }
353
- if (builtIn === 'waitForEvent') {
354
- const safeId = toValidIdentifier(nodeId);
355
- const args = buildNodeArgs(nodeId, nodeType, workflow, nodeTypes);
356
- const eventNameArg = args[1]; // execute=args[0], eventName=args[1]
357
- const matchArg = args[2]; // optional
358
- const timeoutArg = args[3]; // optional
359
- lines.push(`${indent}const ${safeId}_raw = await step.waitForEvent('${nodeId}', {`);
360
- lines.push(`${indent} event: ${eventNameArg},`);
361
- if (matchArg && matchArg !== 'undefined') {
362
- lines.push(`${indent} match: ${matchArg},`);
363
- }
364
- if (timeoutArg && timeoutArg !== 'undefined') {
365
- lines.push(`${indent} timeout: ${timeoutArg},`);
366
- }
367
- lines.push(`${indent}});`);
368
- lines.push(`${indent}${safeId}_result = ${safeId}_raw`);
369
- lines.push(`${indent} ? { onSuccess: true, onFailure: false, eventData: ${safeId}_raw.data }`);
370
- lines.push(`${indent} : { onSuccess: false, onFailure: true, eventData: {} };`);
371
- lines.push('');
372
- return;
373
- }
374
- if (builtIn === 'waitForAgent') {
375
- const safeId = toValidIdentifier(nodeId);
376
- const args = buildNodeArgs(nodeId, nodeType, workflow, nodeTypes);
377
- const agentIdArg = args[1]; // execute=args[0], agentId=args[1]
378
- // Map waitForAgent to step.waitForEvent with agent-scoped event name
379
- lines.push(`${indent}const ${safeId}_raw = await step.waitForEvent('${nodeId}', {`);
380
- lines.push(`${indent} event: \`agent/\${${agentIdArg}}\`,`);
381
- lines.push(`${indent} timeout: '7d',`);
382
- lines.push(`${indent}});`);
383
- lines.push(`${indent}${safeId}_result = ${safeId}_raw`);
384
- lines.push(`${indent} ? { onSuccess: true, onFailure: false, agentResult: ${safeId}_raw.data ?? {} }`);
385
- lines.push(`${indent} : { onSuccess: false, onFailure: true, agentResult: {} };`);
386
- lines.push('');
387
- return;
388
- }
389
- if (builtIn === 'invokeWorkflow') {
390
- const safeId = toValidIdentifier(nodeId);
391
- const args = buildNodeArgs(nodeId, nodeType, workflow, nodeTypes);
392
- const functionIdArg = args[1];
393
- const payloadArg = args[2];
394
- const timeoutArg = args[3];
395
- lines.push(`${indent}try {`);
396
- lines.push(`${indent} ${safeId}_result = await step.invoke('${nodeId}', {`);
397
- lines.push(`${indent} function: ${functionIdArg},`);
398
- lines.push(`${indent} data: ${payloadArg},`);
399
- if (timeoutArg && timeoutArg !== 'undefined') {
400
- lines.push(`${indent} timeout: ${timeoutArg},`);
401
- }
402
- lines.push(`${indent} });`);
403
- lines.push(`${indent} ${safeId}_result = { onSuccess: true, onFailure: false, result: ${safeId}_result };`);
404
- lines.push(`${indent}} catch (err) {`);
405
- lines.push(`${indent} ${safeId}_result = { onSuccess: false, onFailure: true, result: {} };`);
406
- lines.push(`${indent}}`);
407
- lines.push('');
408
- return;
409
- }
410
- if (nodeType.expression) {
411
- lines.push(generateExpressionCall(nodeId, nodeType, workflow, nodeTypes, indent));
412
- }
413
- else {
414
- lines.push(generateStepRunCall(nodeId, nodeType, workflow, nodeTypes, indent));
415
- }
416
- lines.push('');
417
- }
418
- /**
419
- * Ensure all expression node dependencies for a given node are emitted before it.
420
- * Expression nodes are pure functions that can be safely emitted inline wherever needed.
421
- */
422
- function ensureExpressionDependencies(nodeId, workflow, nodeTypes, indent, lines, generatedNodes) {
423
- for (const conn of workflow.connections) {
424
- if (conn.to.node !== nodeId)
425
- continue;
426
- if (conn.from.scope || conn.to.scope)
427
- continue;
428
- const fromNode = conn.from.node;
429
- if (isStartNode(fromNode) || isExitNode(fromNode))
430
- continue;
431
- if (generatedNodes.has(fromNode))
432
- continue;
433
- const nt = getNodeType(fromNode, workflow, nodeTypes);
434
- if (!nt || !nt.expression)
435
- continue;
436
- // Recursively ensure this expression's own dependencies first
437
- ensureExpressionDependencies(fromNode, workflow, nodeTypes, indent, lines, generatedNodes);
438
- if (generatedNodes.has(fromNode))
439
- continue;
440
- // Emit the expression node inline
441
- generatedNodes.add(fromNode);
442
- lines.push(generateExpressionCall(fromNode, nt, workflow, nodeTypes, indent));
443
- lines.push('');
444
- }
445
- }
446
- /**
447
- * Emit the if/else branching body for a branching node (without the step.run call).
448
- * Used after Promise.all to emit branching bodies separately from the step execution.
449
- */
450
- function emitBranchingBody(nodeId, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes) {
451
- const safeId = toValidIdentifier(nodeId);
452
- const region = branchRegions.get(nodeId);
453
- if (!region)
454
- return;
455
- const hasSuccessBranch = region.successNodes.size > 0;
456
- const hasFailureBranch = region.failureNodes.size > 0;
457
- if (!hasSuccessBranch && !hasFailureBranch)
458
- return;
459
- // Delay nodes (step.sleep) always succeed — emit success branch directly, no if/else
460
- const branchNodeType = getNodeType(nodeId, workflow, nodeTypes);
461
- if (branchNodeType && isBuiltInNode(branchNodeType) === 'delay') {
462
- if (hasSuccessBranch) {
463
- const successOrder = getOrderedNodes(Array.from(region.successNodes), workflow, nodeTypes);
464
- generateNodeBlock(successOrder, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
465
- }
466
- return;
467
- }
468
- lines.push(`${indent}if (${safeId}_result.onSuccess) {`);
469
- if (hasSuccessBranch) {
470
- const successOrder = getOrderedNodes(Array.from(region.successNodes), workflow, nodeTypes);
471
- generateNodeBlock(successOrder, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent + ' ', lines, generatedNodes);
472
- }
473
- if (hasFailureBranch) {
474
- lines.push(`${indent}} else {`);
475
- const failureOrder = getOrderedNodes(Array.from(region.failureNodes), workflow, nodeTypes);
476
- generateNodeBlock(failureOrder, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent + ' ', lines, generatedNodes);
477
- lines.push(`${indent}}`);
478
- }
479
- else {
480
- lines.push(`${indent}}`);
481
- }
482
- }
483
- /**
484
- * Emit a Promise.all block for 2+ parallel nodes.
485
- * Delay nodes are excluded and emitted separately (step.sleep returns void).
486
- * waitForEvent/invokeWorkflow get their Inngest step primitives instead of step.run.
487
- */
488
- function emitPromiseAll(nodeIds, workflow, nodeTypes, indent, lines, generatedNodes) {
489
- // Separate delay nodes from the group (step.sleep returns void, breaks destructuring)
490
- const delayNodes = [];
491
- const nonDelayNodes = [];
492
- for (const nodeId of nodeIds) {
493
- const nt = getNodeType(nodeId, workflow, nodeTypes);
494
- if (nt && isBuiltInNode(nt) === 'delay') {
495
- delayNodes.push(nodeId);
496
- }
497
- else {
498
- nonDelayNodes.push(nodeId);
499
- }
500
- }
501
- // Emit delay nodes sequentially first
502
- for (const delayId of delayNodes) {
503
- generatedNodes.add(delayId);
504
- const nt = getNodeType(delayId, workflow, nodeTypes);
505
- emitNodeCall(delayId, nt, workflow, nodeTypes, indent, lines);
506
- }
507
- // If only 1 non-delay node remains, emit it directly
508
- if (nonDelayNodes.length === 1) {
509
- const nodeId = nonDelayNodes[0];
510
- generatedNodes.add(nodeId);
511
- const nt = getNodeType(nodeId, workflow, nodeTypes);
512
- emitNodeCall(nodeId, nt, workflow, nodeTypes, indent, lines);
513
- return;
514
- }
515
- if (nonDelayNodes.length === 0)
516
- return;
517
- const destructured = [];
518
- const stepCalls = [];
519
- for (const nodeId of nonDelayNodes) {
520
- generatedNodes.add(nodeId);
521
- const nt = getNodeType(nodeId, workflow, nodeTypes);
522
- const safeId = toValidIdentifier(nodeId);
523
- const builtIn = isBuiltInNode(nt);
524
- if (builtIn === 'waitForEvent') {
525
- const args = buildNodeArgs(nodeId, nt, workflow, nodeTypes);
526
- const eventNameArg = args[1];
527
- const matchArg = args[2];
528
- const timeoutArg = args[3];
529
- let waitCall = `${indent} step.waitForEvent('${nodeId}', { event: ${eventNameArg}`;
530
- if (matchArg && matchArg !== 'undefined')
531
- waitCall += `, match: ${matchArg}`;
532
- if (timeoutArg && timeoutArg !== 'undefined')
533
- waitCall += `, timeout: ${timeoutArg}`;
534
- waitCall += ` })`;
535
- stepCalls.push(waitCall);
536
- }
537
- else if (builtIn === 'waitForAgent') {
538
- const args = buildNodeArgs(nodeId, nt, workflow, nodeTypes);
539
- const agentIdArg = args[1];
540
- stepCalls.push(`${indent} step.waitForEvent('${nodeId}', { event: \`agent/\${${agentIdArg}}\`, timeout: '7d' })`);
541
- }
542
- else if (builtIn === 'invokeWorkflow') {
543
- const args = buildNodeArgs(nodeId, nt, workflow, nodeTypes);
544
- const functionIdArg = args[1];
545
- const payloadArg = args[2];
546
- const timeoutArg = args[3];
547
- let invokeCall = `${indent} step.invoke('${nodeId}', { function: ${functionIdArg}, data: ${payloadArg}`;
548
- if (timeoutArg && timeoutArg !== 'undefined')
549
- invokeCall += `, timeout: ${timeoutArg}`;
550
- invokeCall += ` })`;
551
- stepCalls.push(invokeCall);
552
- }
553
- else if (nt.variant === 'COERCION') {
554
- const coerceExprMap = {
555
- __fw_toString: 'String',
556
- __fw_toNumber: 'Number',
557
- __fw_toBoolean: 'Boolean',
558
- __fw_toJSON: 'JSON.stringify',
559
- __fw_parseJSON: 'JSON.parse',
560
- };
561
- const coerceExpr = coerceExprMap[nt.functionName] || 'String';
562
- const args = buildNodeArgs(nodeId, nt, workflow, nodeTypes);
563
- const valueArg = args[0] || 'undefined';
564
- stepCalls.push(`${indent} Promise.resolve(${coerceExpr}(${valueArg}))`);
565
- }
566
- else if (nt.expression) {
567
- const args = buildNodeArgs(nodeId, nt, workflow, nodeTypes);
568
- const fnCall = `${nt.functionName}(${args.join(', ')})`;
569
- stepCalls.push(`${indent} Promise.resolve(${fnCall})`);
570
- }
571
- else {
572
- const args = buildNodeArgs(nodeId, nt, workflow, nodeTypes);
573
- const fnCall = `${nt.functionName}(${args.join(', ')})`;
574
- const awaitPrefix = nt.isAsync ? 'await ' : '';
575
- stepCalls.push(`${indent} step.run('${nodeId}', async () => ${awaitPrefix}${fnCall})`);
576
- }
577
- destructured.push(`${safeId}_result`);
578
- }
579
- lines.push(`${indent}[${destructured.join(', ')}] = await Promise.all([`);
580
- lines.push(stepCalls.join(',\n'));
581
- lines.push(`${indent}]);`);
582
- lines.push('');
583
- }
584
- // ---------------------------------------------------------------------------
585
- // Code Generation — forEach / Scoped Iteration
586
- // ---------------------------------------------------------------------------
587
- /**
588
- * Generate code for forEach/scoped iteration patterns.
589
- *
590
- * Per-port scoped children execute inside a loop with indexed step names
591
- * for per-item durability: step.run(`processItem-${i}`, ...).
592
- */
593
- function generateForEachScope(parentId, parentNodeType, scopeName, workflow, nodeTypes, indent, lines) {
594
- const safeParent = toValidIdentifier(parentId);
595
- // Find child instances in this scope
596
- const childInstances = workflow.instances.filter((inst) => {
597
- if (!inst.parent)
598
- return false;
599
- return inst.parent.id === parentId && inst.parent.scope === scopeName;
600
- });
601
- if (childInstances.length === 0)
602
- return;
603
- // Find the output port that provides items to iterate
604
- // Look for scoped output ports (these are parameters TO children)
605
- const scopedOutputPorts = Object.entries(parentNodeType.outputs).filter(([_name, portDef]) => portDef.scope === scopeName);
606
- // The 'item' port (or similar) carries the current iteration value
607
- const itemPort = scopedOutputPorts.find(([name]) => name !== 'start' && name !== 'success' && name !== 'failure');
608
- if (!itemPort) {
609
- // No item port — just a simple callback scope, not forEach
610
- lines.push(`${indent}// Scope '${scopeName}' for ${parentId} (callback pattern)`);
611
- for (const child of childInstances) {
612
- const childNt = getNodeType(child.id, workflow, nodeTypes);
613
- if (!childNt)
614
- continue;
615
- lines.push(`${indent}const ${toValidIdentifier(child.id)}_result = await step.run('${child.id}', async () => {`);
616
- const args = buildNodeArgs(child.id, childNt, workflow, nodeTypes);
617
- const fnCall = `${childNt.functionName}(${args.join(', ')})`;
618
- const awaitPrefix = childNt.isAsync ? 'await ' : '';
619
- lines.push(`${indent} return ${awaitPrefix}${fnCall};`);
620
- lines.push(`${indent}});`);
621
- lines.push('');
622
- }
623
- return;
624
- }
625
- // Find the source that provides the array to iterate
626
- // The parent's result should have the items
627
- const [itemPortName] = itemPort;
628
- const arraySource = `${safeParent}_result.${itemPortName}`;
629
- // Generate the loop with indexed step names
630
- lines.push(`${indent}const ${safeParent}_${scopeName}_results = [];`);
631
- lines.push(`${indent}for (let __i__ = 0; __i__ < ${arraySource}.length; __i__++) {`);
632
- lines.push(`${indent} const __item__ = ${arraySource}[__i__];`);
633
- for (const child of childInstances) {
634
- const childNt = getNodeType(child.id, workflow, nodeTypes);
635
- if (!childNt)
636
- continue;
637
- const safeChild = toValidIdentifier(child.id);
638
- // Build args, replacing the scoped connection with __item__
639
- const args = [];
640
- if (!childNt.expression) {
641
- args.push('true'); // execute = true
642
- }
643
- for (const portName of Object.keys(childNt.inputs)) {
644
- if (isExecutePort(portName))
645
- continue;
646
- if (childNt.inputs[portName].scope)
647
- continue;
648
- // Check if this port connects from the scope's item port
649
- const scopedConn = workflow.connections.find((conn) => conn.to.node === child.id && conn.to.port === portName
650
- && conn.from.node === parentId && conn.from.port === itemPortName);
651
- if (scopedConn) {
652
- args.push('__item__');
653
- }
654
- else {
655
- args.push(resolvePortValue(portName, child.id, childNt, workflow, nodeTypes));
656
- }
657
- }
658
- const fnCall = `${childNt.functionName}(${args.join(', ')})`;
659
- const awaitPrefix = childNt.isAsync ? 'await ' : '';
660
- if (childNt.expression) {
661
- lines.push(`${indent} const ${safeChild}_result = ${awaitPrefix}${fnCall};`);
662
- }
663
- else {
664
- lines.push(`${indent} const ${safeChild}_result = await step.run(\`${child.id}-\${__i__}\`, async () => {`);
665
- lines.push(`${indent} return ${awaitPrefix}${fnCall};`);
666
- lines.push(`${indent} });`);
667
- }
668
- lines.push(`${indent} ${safeParent}_${scopeName}_results.push(${safeChild}_result);`);
669
- }
670
- lines.push(`${indent}}`);
671
- lines.push('');
672
- }
673
- // ---------------------------------------------------------------------------
674
- // Code Generation — Block Generation with Parallelism & Branching
675
- // ---------------------------------------------------------------------------
676
- /**
677
- * Generate code for a set of nodes, handling branching, parallelism, and chains.
678
- *
679
- * This is the recursive core called for top-level nodes and branch bodies.
680
- */
681
- function generateNodeBlock(nodeIds, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes) {
682
- // Filter to ungenerated real nodes
683
- const remaining = nodeIds.filter((n) => !generatedNodes.has(n) && !isStartNode(n) && !isExitNode(n));
684
- if (remaining.length === 0)
685
- return;
686
- // Detect parallelism within this block
687
- const groups = detectParallelInList(remaining, workflow, nodeTypes);
688
- for (const group of groups) {
689
- const eligible = group.filter((n) => !generatedNodes.has(n));
690
- if (eligible.length === 0)
691
- continue;
692
- if (eligible.length >= 2) {
693
- // Check if all are expression nodes (no need for Promise.all)
694
- const allExpr = eligible.every((n) => {
695
- const nt = getNodeType(n, workflow, nodeTypes);
696
- return nt?.expression;
697
- });
698
- if (allExpr) {
699
- for (const nodeId of eligible) {
700
- emitSingleNode(nodeId, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
701
- }
702
- }
703
- else {
704
- // Separate expression nodes, chain heads, and parallelizable step.run nodes
705
- const exprNodes = eligible.filter((n) => {
706
- const nt = getNodeType(n, workflow, nodeTypes);
707
- return nt?.expression;
708
- });
709
- const chainHeadNodes = eligible.filter((n) => {
710
- const nt = getNodeType(n, workflow, nodeTypes);
711
- return nt && !nt.expression && branchingChains.has(n);
712
- });
713
- const parallelStepNodes = eligible.filter((n) => {
714
- const nt = getNodeType(n, workflow, nodeTypes);
715
- return nt && !nt.expression && !branchingChains.has(n);
716
- });
717
- // Emit expression nodes individually first (may be data dependencies)
718
- for (const nodeId of exprNodes) {
719
- emitSingleNode(nodeId, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
720
- }
721
- // Ensure expression dependencies for all parallel step nodes
722
- for (const nodeId of parallelStepNodes) {
723
- ensureExpressionDependencies(nodeId, workflow, nodeTypes, indent, lines, generatedNodes);
724
- }
725
- // Emit step.run nodes via Promise.all (includes branching nodes)
726
- if (parallelStepNodes.length >= 2) {
727
- emitPromiseAll(parallelStepNodes, workflow, nodeTypes, indent, lines, generatedNodes);
728
- // After Promise.all, emit branching bodies for any branching nodes
729
- for (const nodeId of parallelStepNodes) {
730
- if (branchingNodes.has(nodeId) && branchRegions.has(nodeId)) {
731
- emitBranchingBody(nodeId, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
732
- }
733
- }
734
- }
735
- else {
736
- for (const nodeId of parallelStepNodes) {
737
- emitSingleNode(nodeId, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
738
- }
739
- }
740
- // Emit chain head nodes sequentially (they manage their own chain)
741
- for (const nodeId of chainHeadNodes) {
742
- emitSingleNode(nodeId, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
743
- }
744
- }
745
- }
746
- else {
747
- // Single node — standard emission
748
- for (const nodeId of eligible) {
749
- emitSingleNode(nodeId, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
750
- }
751
- }
752
- }
753
- }
754
- /**
755
- * Emit code for a single node — dispatches to the right emitter.
756
- */
757
- function emitSingleNode(nodeId, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes) {
758
- if (generatedNodes.has(nodeId))
759
- return;
760
- const nodeType = getNodeType(nodeId, workflow, nodeTypes);
761
- if (!nodeType)
762
- return;
763
- // Skip chain members — they'll be emitted by their chain head
764
- if (chainMembers.has(nodeId))
765
- return;
766
- // Ensure expression node dependencies are emitted first
767
- ensureExpressionDependencies(nodeId, workflow, nodeTypes, indent, lines, generatedNodes);
768
- generatedNodes.add(nodeId);
769
- // Check if this is the head of a branching chain
770
- const chain = branchingChains.get(nodeId);
771
- if (chain && branchingNodes.has(nodeId)) {
772
- generateBranchingChain(chain, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
773
- return;
774
- }
775
- // Check for forEach/scoped children
776
- const hasPerPortScopedChildren = workflow.instances.some((inst) => inst.parent && inst.parent.id === nodeId
777
- && isPerPortScopedChild(inst, workflow, nodeTypes));
778
- if (branchingNodes.has(nodeId) && branchRegions.has(nodeId)) {
779
- generateBranchingNode(nodeId, nodeType, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
780
- }
781
- else {
782
- emitNodeCall(nodeId, nodeType, workflow, nodeTypes, indent, lines);
783
- }
784
- // Emit scoped children (forEach) after the parent node
785
- if (hasPerPortScopedChildren) {
786
- const scopeNames = new Set();
787
- if (nodeType.scope)
788
- scopeNames.add(nodeType.scope);
789
- if (nodeType.scopes) {
790
- for (const s of nodeType.scopes)
791
- scopeNames.add(s);
792
- }
793
- // Also collect from port definitions
794
- for (const portDef of Object.values(nodeType.outputs)) {
795
- if (portDef.scope)
796
- scopeNames.add(portDef.scope);
797
- }
798
- for (const portDef of Object.values(nodeType.inputs)) {
799
- if (portDef.scope)
800
- scopeNames.add(portDef.scope);
801
- }
802
- scopeNames.forEach((scopeName) => {
803
- generateForEachScope(nodeId, nodeType, scopeName, workflow, nodeTypes, indent, lines);
804
- });
805
- }
806
- }
807
- /**
808
- * Generate code for a branching node (has onSuccess/onFailure connections).
809
- */
810
- function generateBranchingNode(nodeId, nodeType, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes) {
811
- // Generate the step.run / expression call for this node
812
- emitNodeCall(nodeId, nodeType, workflow, nodeTypes, indent, lines);
813
- // Emit the if/else branching body
814
- emitBranchingBody(nodeId, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
815
- }
816
- /**
817
- * Generate a chain of branching nodes as flat if/else if/else.
818
- *
819
- * Chains are sequential branching nodes where one direction has exactly one
820
- * branching child and the other has zero. Flattening reduces nesting depth.
821
- *
822
- * Generated structure:
823
- * step.run(A)
824
- * if (!A.onSuccess) { ...A failure body... }
825
- * step.run(B) // B is in A's success path
826
- * else if (!B.onSuccess) { ...B failure body... }
827
- * else { ...last node's success body... }
828
- */
829
- function generateBranchingChain(chain, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes) {
830
- for (let i = 0; i < chain.length; i++) {
831
- const nodeId = chain[i];
832
- const safeId = toValidIdentifier(nodeId);
833
- const nodeType = getNodeType(nodeId, workflow, nodeTypes);
834
- // Ensure expression dependencies before emitting this chain node
835
- ensureExpressionDependencies(nodeId, workflow, nodeTypes, indent, lines, generatedNodes);
836
- generatedNodes.add(nodeId);
837
- // Emit step.run / expression call for this chain node
838
- if (nodeType) {
839
- emitNodeCall(nodeId, nodeType, workflow, nodeTypes, indent, lines);
840
- }
841
- const region = branchRegions.get(nodeId);
842
- if (!region)
843
- continue;
844
- const hasSuccessBranch = region.successNodes.size > 0;
845
- const hasFailureBranch = region.failureNodes.size > 0;
846
- if (!hasSuccessBranch && !hasFailureBranch)
847
- continue;
848
- const nextInChain = i + 1 < chain.length ? chain[i + 1] : null;
849
- const isLast = !nextInChain;
850
- const chainViaSuccess = nextInChain && region.successNodes.has(nextInChain);
851
- const chainViaFailure = nextInChain && region.failureNodes.has(nextInChain);
852
- if (isLast) {
853
- // Delay nodes (step.sleep) always succeed — emit success branch directly, no if/else
854
- if (nodeType && isBuiltInNode(nodeType) === 'delay') {
855
- if (hasSuccessBranch) {
856
- const successOrder = getOrderedNodes(Array.from(region.successNodes), workflow, nodeTypes);
857
- generateNodeBlock(successOrder, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
858
- }
859
- }
860
- else if (hasSuccessBranch || hasFailureBranch) {
861
- // Last node in chain — emit standard branching for both sides
862
- lines.push(`${indent}if (${safeId}_result.onSuccess) {`);
863
- if (hasSuccessBranch) {
864
- const successOrder = getOrderedNodes(Array.from(region.successNodes), workflow, nodeTypes);
865
- generateNodeBlock(successOrder, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent + ' ', lines, generatedNodes);
866
- }
867
- if (hasFailureBranch) {
868
- lines.push(`${indent}} else {`);
869
- const failureOrder = getOrderedNodes(Array.from(region.failureNodes), workflow, nodeTypes);
870
- generateNodeBlock(failureOrder, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent + ' ', lines, generatedNodes);
871
- }
872
- lines.push(`${indent}}`);
873
- }
874
- }
875
- else if (chainViaSuccess) {
876
- // Chain continues through success path; emit failure branch if it exists
877
- if (hasFailureBranch) {
878
- lines.push(`${indent}if (!${safeId}_result.onSuccess) {`);
879
- const failureOrder = getOrderedNodes(Array.from(region.failureNodes), workflow, nodeTypes);
880
- generateNodeBlock(failureOrder, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent + ' ', lines, generatedNodes);
881
- lines.push(`${indent}}`);
882
- }
883
- // Built-in nodes (delay, waitForEvent, invokeWorkflow) bypass the execute flag,
884
- // so the chain continuation must be explicitly guarded.
885
- const nextNt = getNodeType(nextInChain, workflow, nodeTypes);
886
- if (nextNt && isBuiltInNode(nextNt)) {
887
- lines.push(`${indent}if (${safeId}_result.onSuccess) {`);
888
- const remainingChain = chain.slice(i + 1);
889
- generateBranchingChain(remainingChain, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent + ' ', lines, generatedNodes);
890
- lines.push(`${indent}}`);
891
- return; // Remaining chain already generated inside guard
892
- }
893
- // For normal nodes, execute flag handles it — continue flat
894
- }
895
- else if (chainViaFailure) {
896
- // Chain continues through failure path; emit success branch if it exists
897
- if (hasSuccessBranch) {
898
- lines.push(`${indent}if (${safeId}_result.onSuccess) {`);
899
- const successOrder = getOrderedNodes(Array.from(region.successNodes), workflow, nodeTypes);
900
- generateNodeBlock(successOrder, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent + ' ', lines, generatedNodes);
901
- lines.push(`${indent}}`);
902
- }
903
- // Built-in nodes bypass the execute flag — guard the chain continuation
904
- const nextNtF = getNodeType(nextInChain, workflow, nodeTypes);
905
- if (nextNtF && isBuiltInNode(nextNtF)) {
906
- lines.push(`${indent}if (!${safeId}_result.onSuccess) {`);
907
- const remainingChain = chain.slice(i + 1);
908
- generateBranchingChain(remainingChain, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent + ' ', lines, generatedNodes);
909
- lines.push(`${indent}}`);
910
- return; // Remaining chain already generated inside guard
911
- }
912
- // For normal nodes, execute flag handles it — continue flat
913
- }
914
- }
915
- }
916
- /**
917
- * Order nodes by topological sort within a subset.
918
- */
919
- function getOrderedNodes(nodeIds, workflow, nodeTypes) {
920
- if (nodeIds.length <= 1)
921
- return nodeIds;
922
- const cfg = buildControlFlowGraph(workflow, nodeTypes);
923
- const fullOrder = performKahnsTopologicalSort(cfg);
924
- const nodeSet = new Set(nodeIds);
925
- return fullOrder.filter((n) => nodeSet.has(n));
926
- }
927
- /**
928
- * Collect exit port values and build the return statement.
929
- */
930
- function generateReturnStatement(workflow, indent) {
931
- const exitConnections = workflow.connections.filter((conn) => isExitNode(conn.to.node));
932
- if (exitConnections.length === 0) {
933
- return `${indent}return {};`;
934
- }
935
- const props = [];
936
- const seen = new Set();
937
- for (const conn of exitConnections) {
938
- const exitPort = conn.to.port;
939
- if (seen.has(exitPort))
940
- continue;
941
- seen.add(exitPort);
942
- const sourceNode = conn.from.node;
943
- const sourcePort = conn.from.port;
944
- if (isStartNode(sourceNode)) {
945
- props.push(`${exitPort}: event.data.${sourcePort}`);
946
- }
947
- else {
948
- const safeSource = toValidIdentifier(sourceNode);
949
- props.push(`${exitPort}: ${safeSource}_result?.${sourcePort}`);
950
- }
951
- }
952
- return `${indent}return { ${props.join(', ')} };`;
953
- }
954
- // ---------------------------------------------------------------------------
955
- // Main Generator
956
- // ---------------------------------------------------------------------------
957
- /**
958
- * Generate an Inngest function from a workflow AST.
959
- *
960
- * Produces a complete TypeScript module with:
961
- * - Import statements (Inngest SDK + node type functions)
962
- * - `inngest.createFunction()` with per-node `step.run()` calls
963
- * - Parallel execution via `Promise.all` where safe
964
- * - Branching via if/else for onSuccess/onFailure
965
- * - Chain flattening for sequential branching (3-way routing)
966
- * - Indexed `step.run()` for forEach iteration
967
- *
968
- * @param workflow - The workflow AST to generate from
969
- * @param nodeTypes - All available node type definitions
970
- * @param options - Generation options (service name, trigger, retries, etc.)
971
- * @returns Complete TypeScript source code string
972
- */
973
- export function generateInngestFunction(workflow, nodeTypes, options) {
974
- const serviceName = options?.serviceName ?? toKebabCase(workflow.functionName);
975
- const functionId = toKebabCase(workflow.functionName);
976
- const triggerEvent = options?.triggerEvent ?? `fw/${functionId}.execute`;
977
- const retries = workflow.options?.retries ?? options?.retries ?? 3;
978
- const lines = [];
979
- // -- Imports --
980
- lines.push(`import { Inngest } from 'inngest';`);
981
- if (options?.typedEvents) {
982
- lines.push(`import { z } from 'zod';`);
983
- }
984
- if (options?.serveHandler && options?.framework) {
985
- const importMap = {
986
- next: 'inngest/next',
987
- express: 'inngest/express',
988
- hono: 'inngest/hono',
989
- fastify: 'inngest/fastify',
990
- remix: 'inngest/remix',
991
- };
992
- lines.push(`import { serve } from '${importMap[options.framework]}';`);
993
- }
994
- lines.push('');
995
- // Collect node type imports (deduplicate by function name)
996
- const importedFunctions = new Set();
997
- for (const instance of workflow.instances) {
998
- if (isPerPortScopedChild(instance, workflow, nodeTypes))
999
- continue;
1000
- const nodeType = nodeTypes.find((nt) => nt.name === instance.nodeType || nt.functionName === instance.nodeType);
1001
- if (nodeType && !importedFunctions.has(nodeType.functionName)) {
1002
- if (isBuiltInNode(nodeType))
1003
- continue; // Skip built-in nodes — no user import
1004
- importedFunctions.add(nodeType.functionName);
1005
- lines.push(`import { ${nodeType.functionName} } from './node-types/${nodeType.functionName}.js';`);
1006
- }
1007
- }
1008
- lines.push('');
1009
- // -- Inngest client --
1010
- lines.push(`const inngest = new Inngest({ id: '${serviceName}' });`);
1011
- lines.push('');
1012
- // -- Typed event schema (Feature 1) --
1013
- if (options?.typedEvents) {
1014
- const schemaEventName = workflow.options?.trigger?.event ?? triggerEvent;
1015
- const schemaLines = generateEventSchema(workflow, schemaEventName);
1016
- if (schemaLines.length > 0) {
1017
- lines.push(...schemaLines);
1018
- }
1019
- }
1020
- // -- Function definition --
1021
- const fnVar = `${toValidIdentifier(workflow.functionName)}Fn`;
1022
- const configEntries = [
1023
- `id: '${functionId}'`,
1024
- `retries: ${retries}`,
1025
- ];
1026
- // Add timeout from workflow options
1027
- if (workflow.options?.timeout) {
1028
- configEntries.push(`timeouts: { finish: '${workflow.options.timeout}' }`);
1029
- }
1030
- // Add throttle from workflow options
1031
- if (workflow.options?.throttle) {
1032
- const t = workflow.options.throttle;
1033
- const throttleConfig = [`limit: ${t.limit}`];
1034
- if (t.period)
1035
- throttleConfig.push(`period: '${t.period}'`);
1036
- configEntries.push(`throttle: { ${throttleConfig.join(', ')} }`);
1037
- }
1038
- // Add cancelOn from workflow options
1039
- if (workflow.options?.cancelOn) {
1040
- const c = workflow.options.cancelOn;
1041
- const cancelConfig = [`event: '${c.event}'`];
1042
- if (c.match) {
1043
- cancelConfig.push(`match: '${c.match}'`);
1044
- }
1045
- if (c.timeout) {
1046
- cancelConfig.push(`timeout: '${c.timeout}'`);
1047
- }
1048
- configEntries.push(`cancelOn: [{ ${cancelConfig.join(', ')} }]`);
1049
- }
1050
- if (options?.functionConfig) {
1051
- for (const [key, value] of Object.entries(options.functionConfig)) {
1052
- if (key !== 'id' && key !== 'retries') {
1053
- configEntries.push(`${key}: ${JSON.stringify(value)}`);
1054
- }
1055
- }
1056
- }
1057
- lines.push(`export const ${fnVar} = inngest.createFunction(`);
1058
- lines.push(` { ${configEntries.join(', ')} },`);
1059
- // Trigger emission (Feature 2)
1060
- const trigger = workflow.options?.trigger;
1061
- if (trigger?.cron && trigger?.event) {
1062
- lines.push(` { event: '${trigger.event}' },`);
1063
- }
1064
- else if (trigger?.cron) {
1065
- lines.push(` { cron: '${trigger.cron}' },`);
1066
- }
1067
- else {
1068
- const eventName = trigger?.event ?? triggerEvent;
1069
- lines.push(` { event: '${eventName}' },`);
1070
- }
1071
- lines.push(` async ({ event, step }) => {`);
1072
- // -- Build control flow --
1073
- const cfg = buildControlFlowGraph(workflow, nodeTypes);
1074
- const executionOrder = performKahnsTopologicalSort(cfg);
1075
- const branchingNodes = findAllBranchingNodes(workflow, nodeTypes);
1076
- // For Inngest: remove trivially branching nodes that only connect success/failure to Exit.
1077
- // These don't need if/else blocks — they just route status to the workflow exit.
1078
- // (The unified generator keeps them for catch block error handling.)
1079
- for (const nodeId of [...branchingNodes]) {
1080
- const hasNonExitBranch = workflow.connections.some((conn) => conn.from.node === nodeId &&
1081
- (isSuccessPort(conn.from.port) || isFailurePort(conn.from.port)) &&
1082
- !isExitNode(conn.to.node));
1083
- if (!hasNonExitBranch) {
1084
- branchingNodes.delete(nodeId);
1085
- }
1086
- }
1087
- const allInstanceIds = new Set(workflow.instances.map((i) => i.id));
1088
- const branchRegions = new Map();
1089
- branchingNodes.forEach((branchInstanceId) => {
1090
- const successNodes = findNodesInBranch(branchInstanceId, RESERVED_PORT_NAMES.ON_SUCCESS, workflow, allInstanceIds, branchingNodes, nodeTypes);
1091
- const failureNodes = findNodesInBranch(branchInstanceId, RESERVED_PORT_NAMES.ON_FAILURE, workflow, allInstanceIds, branchingNodes, nodeTypes);
1092
- branchRegions.set(branchInstanceId, { successNodes, failureNodes });
1093
- });
1094
- // Handle multi-branch membership: promote nodes in multiple branches
1095
- const instancesInMultipleBranches = new Set();
1096
- allInstanceIds.forEach((instanceId) => {
1097
- let branchCount = 0;
1098
- branchRegions.forEach((region) => {
1099
- if (region.successNodes.has(instanceId) || region.failureNodes.has(instanceId)) {
1100
- branchCount++;
1101
- }
1102
- });
1103
- if (branchCount > 1) {
1104
- instancesInMultipleBranches.add(instanceId);
1105
- }
1106
- });
1107
- branchRegions.forEach((region) => {
1108
- instancesInMultipleBranches.forEach((instanceId) => {
1109
- region.successNodes.delete(instanceId);
1110
- region.failureNodes.delete(instanceId);
1111
- });
1112
- });
1113
- // Detect branching chains for if/else if flattening
1114
- const branchingChains = detectBranchingChains(branchingNodes, branchRegions);
1115
- // Identify chain members (non-heads) that are emitted by their chain head
1116
- const chainMembers = new Set();
1117
- branchingChains.forEach((chain) => {
1118
- for (let i = 1; i < chain.length; i++) {
1119
- chainMembers.add(chain[i]);
1120
- }
1121
- });
1122
- // Compute nodes in any branch (not top-level)
1123
- const nodesInAnyBranch = new Set();
1124
- branchRegions.forEach((region) => {
1125
- region.successNodes.forEach((n) => nodesInAnyBranch.add(n));
1126
- region.failureNodes.forEach((n) => nodesInAnyBranch.add(n));
1127
- });
1128
- // Filter execution order to top-level nodes
1129
- const topLevelNodes = executionOrder.filter((n) => !isStartNode(n) &&
1130
- !isExitNode(n) &&
1131
- !nodesInAnyBranch.has(n) &&
1132
- !chainMembers.has(n));
1133
- // -- Generate node execution code --
1134
- const indent = ' ';
1135
- const generatedNodes = new Set();
1136
- // Pre-declare result variables so they're accessible from all scopes (branch bodies, return statement)
1137
- // Skip delay nodes — step.sleep() returns void, no result variable needed
1138
- const resultVarNames = [];
1139
- for (const instance of workflow.instances) {
1140
- if (isStartNode(instance.id) || isExitNode(instance.id))
1141
- continue;
1142
- if (isPerPortScopedChild(instance, workflow, nodeTypes))
1143
- continue;
1144
- const nt = getNodeType(instance.id, workflow, nodeTypes);
1145
- if (nt && isBuiltInNode(nt) === 'delay')
1146
- continue;
1147
- resultVarNames.push(toValidIdentifier(instance.id) + '_result');
1148
- }
1149
- if (resultVarNames.length > 0) {
1150
- lines.push(`${indent}let ${resultVarNames.map((v) => v + ': any').join(', ')};`);
1151
- lines.push('');
1152
- }
1153
- generateNodeBlock(topLevelNodes, workflow, nodeTypes, branchingNodes, branchRegions, branchingChains, chainMembers, indent, lines, generatedNodes);
1154
- // -- Return statement --
1155
- lines.push(generateReturnStatement(workflow, indent));
1156
- lines.push(' }');
1157
- lines.push(');');
1158
- lines.push('');
1159
- // -- Serve handler (Feature 7) --
1160
- if (options?.serveHandler && options?.framework) {
1161
- const framework = options.framework;
1162
- lines.push(`// --- Serve handler (${framework}) ---`);
1163
- if (framework === 'next') {
1164
- lines.push(`export const { GET, POST, PUT } = serve({`);
1165
- }
1166
- else {
1167
- lines.push(`export const handler = serve({`);
1168
- }
1169
- lines.push(` client: inngest,`);
1170
- lines.push(` functions: [${fnVar}],`);
1171
- lines.push(`});`);
1172
- lines.push('');
1173
- }
1174
- return lines.join('\n');
1175
- }
1176
- //# sourceMappingURL=generator.js.map