@blokjs/runner 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/dist/Blok.js +32 -3
  2. package/dist/Blok.js.map +1 -1
  3. package/dist/Configuration.d.ts +41 -5
  4. package/dist/Configuration.js +215 -92
  5. package/dist/Configuration.js.map +1 -1
  6. package/dist/ForEachNode.d.ts +59 -0
  7. package/dist/ForEachNode.js +522 -0
  8. package/dist/ForEachNode.js.map +1 -0
  9. package/dist/LoopMaxIterationsError.d.ts +11 -0
  10. package/dist/LoopMaxIterationsError.js +18 -0
  11. package/dist/LoopMaxIterationsError.js.map +1 -0
  12. package/dist/LoopNode.d.ts +36 -0
  13. package/dist/LoopNode.js +182 -0
  14. package/dist/LoopNode.js.map +1 -0
  15. package/dist/Runner.d.ts +11 -1
  16. package/dist/Runner.js +9 -2
  17. package/dist/Runner.js.map +1 -1
  18. package/dist/RunnerSteps.js +419 -112
  19. package/dist/RunnerSteps.js.map +1 -1
  20. package/dist/RuntimeAdapterNode.d.ts +2 -1
  21. package/dist/RuntimeAdapterNode.js +2 -2
  22. package/dist/RuntimeAdapterNode.js.map +1 -1
  23. package/dist/RuntimeRegistry.d.ts +23 -2
  24. package/dist/RuntimeRegistry.js +31 -2
  25. package/dist/RuntimeRegistry.js.map +1 -1
  26. package/dist/SubworkflowNode.d.ts +106 -0
  27. package/dist/SubworkflowNode.js +261 -3
  28. package/dist/SubworkflowNode.js.map +1 -1
  29. package/dist/SwitchNode.d.ts +37 -0
  30. package/dist/SwitchNode.js +153 -0
  31. package/dist/SwitchNode.js.map +1 -0
  32. package/dist/TriggerBase.d.ts +50 -0
  33. package/dist/TriggerBase.js +262 -4
  34. package/dist/TriggerBase.js.map +1 -1
  35. package/dist/TryCatchNode.d.ts +32 -0
  36. package/dist/TryCatchNode.js +207 -0
  37. package/dist/TryCatchNode.js.map +1 -0
  38. package/dist/adapters/grpc/GrpcCodec.js +2 -2
  39. package/dist/adapters/grpc/GrpcRuntimeAdapter.d.ts +6 -4
  40. package/dist/adapters/grpc/GrpcRuntimeAdapter.js +6 -4
  41. package/dist/adapters/grpc/GrpcRuntimeAdapter.js.map +1 -1
  42. package/dist/adapters/grpc/types.d.ts +7 -5
  43. package/dist/adapters/grpc/types.js.map +1 -1
  44. package/dist/adapters/transport.d.ts +12 -41
  45. package/dist/adapters/transport.js +21 -70
  46. package/dist/adapters/transport.js.map +1 -1
  47. package/dist/cache/NodeResultCache.js +7 -0
  48. package/dist/cache/NodeResultCache.js.map +1 -1
  49. package/dist/concurrency/NatsKvConcurrencyBackend.js +18 -5
  50. package/dist/concurrency/NatsKvConcurrencyBackend.js.map +1 -1
  51. package/dist/concurrency/RedisConcurrencyBackend.d.ts +64 -0
  52. package/dist/concurrency/RedisConcurrencyBackend.js +374 -0
  53. package/dist/concurrency/RedisConcurrencyBackend.js.map +1 -0
  54. package/dist/concurrency/createConcurrencyBackend.d.ts +1 -0
  55. package/dist/concurrency/createConcurrencyBackend.js +5 -1
  56. package/dist/concurrency/createConcurrencyBackend.js.map +1 -1
  57. package/dist/defineNode.d.ts +8 -0
  58. package/dist/defineNode.js +25 -5
  59. package/dist/defineNode.js.map +1 -1
  60. package/dist/graphql/GraphQLSchemaGenerator.js +1 -1
  61. package/dist/graphql/GraphQLSchemaGenerator.js.map +1 -1
  62. package/dist/index.d.ts +10 -6
  63. package/dist/index.js +13 -9
  64. package/dist/index.js.map +1 -1
  65. package/dist/marketplace/RuntimeCatalog.d.ts +6 -0
  66. package/dist/marketplace/RuntimeCatalog.js.map +1 -1
  67. package/dist/marketplace/RuntimeDiscovery.d.ts +2 -2
  68. package/dist/marketplace/RuntimeDiscovery.js +18 -6
  69. package/dist/marketplace/RuntimeDiscovery.js.map +1 -1
  70. package/dist/monitoring/ConcurrencyMetrics.d.ts +26 -0
  71. package/dist/monitoring/ConcurrencyMetrics.js +36 -4
  72. package/dist/monitoring/ConcurrencyMetrics.js.map +1 -1
  73. package/dist/monitoring/ForEachWaitMetrics.d.ts +22 -0
  74. package/dist/monitoring/ForEachWaitMetrics.js +36 -0
  75. package/dist/monitoring/ForEachWaitMetrics.js.map +1 -0
  76. package/dist/openapi/OpenAPIGenerator.js +7 -2
  77. package/dist/openapi/OpenAPIGenerator.js.map +1 -1
  78. package/dist/runtime/PrimitiveStack.d.ts +64 -0
  79. package/dist/runtime/PrimitiveStack.js +92 -0
  80. package/dist/runtime/PrimitiveStack.js.map +1 -0
  81. package/dist/scheduling/DebounceBackend.d.ts +108 -0
  82. package/dist/scheduling/DebounceBackend.js +23 -0
  83. package/dist/scheduling/DebounceBackend.js.map +1 -0
  84. package/dist/scheduling/DebounceCoordinator.d.ts +65 -12
  85. package/dist/scheduling/DebounceCoordinator.js +234 -13
  86. package/dist/scheduling/DebounceCoordinator.js.map +1 -1
  87. package/dist/scheduling/DeferredRunScheduler.d.ts +28 -0
  88. package/dist/scheduling/DeferredRunScheduler.js +105 -3
  89. package/dist/scheduling/DeferredRunScheduler.js.map +1 -1
  90. package/dist/scheduling/NatsKvDebounceBackend.d.ts +53 -0
  91. package/dist/scheduling/NatsKvDebounceBackend.js +334 -0
  92. package/dist/scheduling/NatsKvDebounceBackend.js.map +1 -0
  93. package/dist/scheduling/RedisDebounceBackend.d.ts +49 -0
  94. package/dist/scheduling/RedisDebounceBackend.js +356 -0
  95. package/dist/scheduling/RedisDebounceBackend.js.map +1 -0
  96. package/dist/scheduling/createDebounceBackend.d.ts +25 -0
  97. package/dist/scheduling/createDebounceBackend.js +39 -0
  98. package/dist/scheduling/createDebounceBackend.js.map +1 -0
  99. package/dist/security/AuditLogger.js +1 -1
  100. package/dist/security/AuditLogger.js.map +1 -1
  101. package/dist/security/AuthMiddleware.d.ts +19 -20
  102. package/dist/security/AuthMiddleware.js +35 -20
  103. package/dist/security/AuthMiddleware.js.map +1 -1
  104. package/dist/security/OAuthProvider.js +2 -2
  105. package/dist/security/OAuthProvider.js.map +1 -1
  106. package/dist/security/SecretManager.js +14 -13
  107. package/dist/security/SecretManager.js.map +1 -1
  108. package/dist/security/index.d.ts +3 -1
  109. package/dist/security/index.js +3 -1
  110. package/dist/security/index.js.map +1 -1
  111. package/dist/testing/TestHarness.d.ts +27 -12
  112. package/dist/testing/TestHarness.js +19 -3
  113. package/dist/testing/TestHarness.js.map +1 -1
  114. package/dist/testing/WorkflowTestRunner.js +0 -7
  115. package/dist/testing/WorkflowTestRunner.js.map +1 -1
  116. package/dist/tracing/InMemoryRunStore.d.ts +14 -1
  117. package/dist/tracing/InMemoryRunStore.js +95 -6
  118. package/dist/tracing/InMemoryRunStore.js.map +1 -1
  119. package/dist/tracing/PostgresRunStore.d.ts +28 -2
  120. package/dist/tracing/PostgresRunStore.js +276 -3
  121. package/dist/tracing/PostgresRunStore.js.map +1 -1
  122. package/dist/tracing/RoutingDiagnostics.d.ts +55 -0
  123. package/dist/tracing/RoutingDiagnostics.js +50 -0
  124. package/dist/tracing/RoutingDiagnostics.js.map +1 -0
  125. package/dist/tracing/RunStore.d.ts +82 -1
  126. package/dist/tracing/RunTracker.d.ts +7 -1
  127. package/dist/tracing/RunTracker.js +23 -0
  128. package/dist/tracing/RunTracker.js.map +1 -1
  129. package/dist/tracing/SqliteRunStore.d.ts +57 -2
  130. package/dist/tracing/SqliteRunStore.js +408 -48
  131. package/dist/tracing/SqliteRunStore.js.map +1 -1
  132. package/dist/tracing/TraceRouter.js +380 -18
  133. package/dist/tracing/TraceRouter.js.map +1 -1
  134. package/dist/tracing/createStore.js +14 -3
  135. package/dist/tracing/createStore.js.map +1 -1
  136. package/dist/tracing/metadataFilter.d.ts +63 -0
  137. package/dist/tracing/metadataFilter.js +224 -0
  138. package/dist/tracing/metadataFilter.js.map +1 -0
  139. package/dist/tracing/types.d.ts +331 -7
  140. package/dist/utils/envAllowlist.d.ts +35 -0
  141. package/dist/utils/envAllowlist.js +113 -0
  142. package/dist/utils/envAllowlist.js.map +1 -0
  143. package/dist/version/RuntimeVersionValidator.d.ts +38 -0
  144. package/dist/version/RuntimeVersionValidator.js +121 -0
  145. package/dist/version/RuntimeVersionValidator.js.map +1 -0
  146. package/dist/visualization/WorkflowVisualizer.js +4 -4
  147. package/dist/visualization/WorkflowVisualizer.js.map +1 -1
  148. package/dist/workflow/PersistenceHelper.d.ts +18 -10
  149. package/dist/workflow/PersistenceHelper.js +35 -9
  150. package/dist/workflow/PersistenceHelper.js.map +1 -1
  151. package/dist/workflow/WorkflowNormalizer.d.ts +19 -1
  152. package/dist/workflow/WorkflowNormalizer.js +469 -19
  153. package/dist/workflow/WorkflowNormalizer.js.map +1 -1
  154. package/dist/workflow/WorkflowRegistry.d.ts +122 -0
  155. package/dist/workflow/WorkflowRegistry.js +121 -0
  156. package/dist/workflow/WorkflowRegistry.js.map +1 -1
  157. package/dist/workflow/sampleBody.d.ts +54 -0
  158. package/dist/workflow/sampleBody.js +320 -0
  159. package/dist/workflow/sampleBody.js.map +1 -0
  160. package/package.json +3 -8
  161. package/dist/adapters/HttpRuntimeAdapter.d.ts +0 -79
  162. package/dist/adapters/HttpRuntimeAdapter.js +0 -233
  163. package/dist/adapters/HttpRuntimeAdapter.js.map +0 -1
@@ -10,7 +10,7 @@ import { parseDuration } from "@blokjs/helper";
10
10
  * ```
11
11
  * {
12
12
  * name, version, trigger,
13
- * steps: [{ name, node, type, active?, stop?, set_var? }],
13
+ * steps: [{ name, node, type, active?, stop? }],
14
14
  * nodes: { [stepName]: { inputs?, conditions? } }
15
15
  * }
16
16
  * ```
@@ -30,7 +30,7 @@ import { parseDuration } from "@blokjs/helper";
30
30
  * ```
31
31
  * {
32
32
  * name, version, trigger, // method "*" normalized to "ANY"
33
- * steps: [{ name, node, type, active, stop, set_var, as, spread, ephemeral, ... }],
33
+ * steps: [{ name, node, type, active, stop, as, spread, ephemeral, ... }],
34
34
  * nodes: { [stepName]: { inputs?, conditions? } }
35
35
  * }
36
36
  * ```
@@ -67,9 +67,33 @@ export function normalizeWorkflow(raw, sourcePath) {
67
67
  // Legacy builder shape — same unwrap.
68
68
  wf = wf._config;
69
69
  }
70
+ // `set_var` removed in v0.5. Reject at load time with a migration hint
71
+ // — silently dropping the field would produce subtly different runtime
72
+ // behaviour (every step now default-stores; legacy `set_var: false`
73
+ // was the only opt-out and is replaced by `ephemeral: true`).
74
+ if (Array.isArray(wf.steps)) {
75
+ assertNoSetVar(wf.steps, sourcePath);
76
+ }
70
77
  const name = typeof wf.name === "string" ? wf.name : "";
71
78
  const version = typeof wf.version === "string" ? wf.version : "1.0.0";
72
79
  const description = typeof wf.description === "string" ? wf.description : undefined;
80
+ // `middleware` is overloaded:
81
+ // - `true` → marker bit, this workflow IS a middleware
82
+ // - `string[]` → workflow-level middleware chain (these run on
83
+ // every request to this workflow, before any
84
+ // trigger-level middleware)
85
+ // The two are mutually exclusive — refuse the conflict at load time
86
+ // rather than letting one silently win.
87
+ const middleware = wf.middleware === true ? true : undefined;
88
+ let appliedMiddleware;
89
+ if (Array.isArray(wf.middleware)) {
90
+ const list = wf.middleware.filter((s) => typeof s === "string" && s.length > 0);
91
+ appliedMiddleware = list.length > 0 ? list : undefined;
92
+ }
93
+ if (middleware === true && appliedMiddleware !== undefined) {
94
+ const suffix = sourcePath ? ` (file: ${sourcePath})` : "";
95
+ throw new Error(`[blok] WorkflowNormalizer: workflow "${name}" sets both \`middleware: true\` (marker) and \`middleware: [...]\` (chain). These are mutually exclusive — pick one.${suffix}`);
96
+ }
73
97
  // --- Trigger normalization (method "*" → "ANY") ---
74
98
  const trigger = normalizeTrigger(wf.trigger, sourcePath);
75
99
  // --- Steps normalization ---
@@ -84,9 +108,16 @@ export function normalizeWorkflow(raw, sourcePath) {
84
108
  const step = rawStep;
85
109
  // v2 branch — { id, branch: { when, then, else? } }
86
110
  if (isPlainObject(step.branch)) {
87
- const { internalStep, nodeConfig } = normalizeBranchStep(step, i);
111
+ const { internalStep, nodeConfig, innerNodes } = normalizeBranchStep(step, i);
88
112
  internalSteps.push(internalStep);
89
113
  internalNodes[internalStep.name] = nodeConfig;
114
+ // Promote every inner step's nodeConfig into the top-level nodes map
115
+ // so BlokService.run can find `ctx.config[innerStep.name].inputs`
116
+ // when the runner descends into the matching arm. Without this,
117
+ // inner steps with `inputs:` defined inline crash with
118
+ // `opts.inputs undefined` because their config was only attached
119
+ // to the inner step instance, not the global lookup map.
120
+ Object.assign(internalNodes, innerNodes);
90
121
  continue;
91
122
  }
92
123
  // v2 sub-workflow — { id, subworkflow: "<name>", inputs?, wait? }
@@ -108,6 +139,38 @@ export function normalizeWorkflow(raw, sourcePath) {
108
139
  internalSteps.push(internalStep);
109
140
  continue;
110
141
  }
142
+ // v0.5 forEach — { id, forEach: { in, as, mode?, concurrency?, do: [...] } }
143
+ if (isPlainObject(step.forEach)) {
144
+ const { internalStep, nodeConfig, innerNodes } = normalizeForEachStep(step, i);
145
+ internalSteps.push(internalStep);
146
+ internalNodes[internalStep.name] = nodeConfig;
147
+ Object.assign(internalNodes, innerNodes);
148
+ continue;
149
+ }
150
+ // v0.5 loop — { id, loop: { while, maxIterations?, do: [...] } }
151
+ if (isPlainObject(step.loop)) {
152
+ const { internalStep, nodeConfig, innerNodes } = normalizeLoopStep(step, i);
153
+ internalSteps.push(internalStep);
154
+ internalNodes[internalStep.name] = nodeConfig;
155
+ Object.assign(internalNodes, innerNodes);
156
+ continue;
157
+ }
158
+ // v0.5 switch — { id, switch: { on, cases: [{when, do}], default? } }
159
+ if (isPlainObject(step.switch)) {
160
+ const { internalStep, nodeConfig, innerNodes } = normalizeSwitchStep(step, i);
161
+ internalSteps.push(internalStep);
162
+ internalNodes[internalStep.name] = nodeConfig;
163
+ Object.assign(internalNodes, innerNodes);
164
+ continue;
165
+ }
166
+ // v0.5 tryCatch — { id, tryCatch: { try, catch, finally? } }
167
+ if (isPlainObject(step.tryCatch)) {
168
+ const { internalStep, nodeConfig, innerNodes } = normalizeTryCatchStep(step, i);
169
+ internalSteps.push(internalStep);
170
+ internalNodes[internalStep.name] = nodeConfig;
171
+ Object.assign(internalNodes, innerNodes);
172
+ continue;
173
+ }
111
174
  // v2 regular — { id, use, inputs?, as?, spread?, ephemeral?, ... }
112
175
  // or v1 regular — { name, node, type } + nodes[name].inputs
113
176
  const { internalStep, nodeConfig } = normalizeRegularStep(step, nodesInput, i);
@@ -133,6 +196,8 @@ export function normalizeWorkflow(raw, sourcePath) {
133
196
  trigger,
134
197
  steps: internalSteps,
135
198
  nodes: internalNodes,
199
+ ...(middleware ? { middleware } : {}),
200
+ ...(appliedMiddleware ? { appliedMiddleware } : {}),
136
201
  };
137
202
  }
138
203
  // =============================================================================
@@ -157,10 +222,9 @@ function normalizeRegularStep(step, nodesInput, index) {
157
222
  const v1NodeConfig = isPlainObject(nodesInput[id]) ? nodesInput[id] : null;
158
223
  const v1Inputs = v1NodeConfig?.inputs && isPlainObject(v1NodeConfig.inputs) ? v1NodeConfig.inputs : null;
159
224
  const inputs = inlineInputs ?? v1Inputs;
160
- // Persistence knobs — v2 first, legacy `set_var` mapped second.
161
- const ephemeralExplicit = step.ephemeral === true;
162
- const ephemeralFromLegacy = step.set_var === false;
163
- const ephemeral = ephemeralExplicit || ephemeralFromLegacy;
225
+ // Persistence knobs — v2 only. Legacy `set_var` is rejected upstream
226
+ // in `normalizeWorkflow` via `assertNoSetVar`.
227
+ const ephemeral = step.ephemeral === true;
164
228
  const as = pickString(step.as);
165
229
  const spread = step.spread === true;
166
230
  // `as` and `spread` are mutually exclusive — caught at schema level too,
@@ -174,7 +238,6 @@ function normalizeRegularStep(step, nodesInput, index) {
174
238
  type,
175
239
  active: step.active === undefined ? true : Boolean(step.active),
176
240
  stop: step.stop === true,
177
- set_var: typeof step.set_var === "boolean" ? step.set_var : undefined,
178
241
  as,
179
242
  spread,
180
243
  ephemeral,
@@ -242,37 +305,49 @@ function normalizeBranchStep(step, index) {
242
305
  // `nodesInput` because v2 branches inline `inputs` on each nested step.
243
306
  const thenInternal = [];
244
307
  const elseInternal = [];
308
+ const innerNodes = {};
245
309
  for (let i = 0; i < thenSteps.length; i++) {
246
310
  const s = thenSteps[i];
247
311
  if (!isPlainObject(s))
248
312
  continue;
249
313
  if (isPlainObject(s.branch)) {
250
- const { internalStep } = normalizeBranchStep(s, i);
251
- thenInternal.push(internalStep);
314
+ const { internalStep: nestedStep, nodeConfig: nestedConfig, innerNodes: nestedInner, } = normalizeBranchStep(s, i);
315
+ thenInternal.push(nestedStep);
316
+ innerNodes[nestedStep.name] = nestedConfig;
317
+ Object.assign(innerNodes, nestedInner);
252
318
  continue;
253
319
  }
254
- const { internalStep, nodeConfig } = normalizeRegularStep(s, {}, i);
320
+ const { internalStep: regularStep, nodeConfig } = normalizeRegularStep(s, {}, i);
255
321
  // Inline inputs on the step itself so nested-flow execution finds them.
256
322
  // (RunnerSteps recursively executes flow nodes' inner steps.)
257
323
  if (nodeConfig?.inputs) {
258
- internalStep.inputs = nodeConfig.inputs;
324
+ regularStep.inputs = nodeConfig.inputs;
259
325
  }
260
- thenInternal.push(internalStep);
326
+ // Also surface the nodeConfig in the bubbled-up innerNodes map so
327
+ // BlokService.run can read inputs via `ctx.config[step.name]` when
328
+ // the inner step actually executes.
329
+ if (nodeConfig)
330
+ innerNodes[regularStep.name] = nodeConfig;
331
+ thenInternal.push(regularStep);
261
332
  }
262
333
  for (let i = 0; i < elseSteps.length; i++) {
263
334
  const s = elseSteps[i];
264
335
  if (!isPlainObject(s))
265
336
  continue;
266
337
  if (isPlainObject(s.branch)) {
267
- const { internalStep } = normalizeBranchStep(s, i);
268
- elseInternal.push(internalStep);
338
+ const { internalStep: nestedStep, nodeConfig: nestedConfig, innerNodes: nestedInner, } = normalizeBranchStep(s, i);
339
+ elseInternal.push(nestedStep);
340
+ innerNodes[nestedStep.name] = nestedConfig;
341
+ Object.assign(innerNodes, nestedInner);
269
342
  continue;
270
343
  }
271
- const { internalStep, nodeConfig } = normalizeRegularStep(s, {}, i);
344
+ const { internalStep: regularStep, nodeConfig } = normalizeRegularStep(s, {}, i);
272
345
  if (nodeConfig?.inputs) {
273
- internalStep.inputs = nodeConfig.inputs;
346
+ regularStep.inputs = nodeConfig.inputs;
274
347
  }
275
- elseInternal.push(internalStep);
348
+ if (nodeConfig)
349
+ innerNodes[regularStep.name] = nodeConfig;
350
+ elseInternal.push(regularStep);
276
351
  }
277
352
  const conditions = [{ type: "if", condition: when, steps: thenInternal }];
278
353
  if (elseInternal.length > 0) {
@@ -287,7 +362,7 @@ function normalizeBranchStep(step, index) {
287
362
  flow: true,
288
363
  };
289
364
  const nodeConfig = { conditions };
290
- return { internalStep, nodeConfig };
365
+ return { internalStep, nodeConfig, innerNodes };
291
366
  }
292
367
  const SUBWORKFLOW_NODE_REF = "@blokjs/subworkflow";
293
368
  /**
@@ -358,6 +433,23 @@ function normalizeSubworkflowStep(step, index) {
358
433
  if (typeof step.maxDuration === "number" || typeof step.maxDuration === "string") {
359
434
  internalStep.maxDuration = step.maxDuration;
360
435
  }
436
+ // G3 polymorphic-dispatch safety net — narrow the registry lookup to a
437
+ // fixed set when `subworkflow` is an expression (or just to harden a
438
+ // literal). Filter to non-empty strings here so SubworkflowNode can
439
+ // trust the shape and skip a defensive check on the hot path.
440
+ if (Array.isArray(step.allowList)) {
441
+ const cleaned = step.allowList.filter((s) => typeof s === "string" && s.length > 0);
442
+ if (cleaned.length > 0) {
443
+ internalStep.allowList = cleaned;
444
+ }
445
+ }
446
+ // G2 (v0.6) — dispatch strategy. Unknown / missing values fall
447
+ // through as "in-process" inside SubworkflowNode.run, but pass the
448
+ // raw string so the resolver can validate + threading bugs surface
449
+ // at config load instead of at run time.
450
+ if (step.dispatch === "in-process" || step.dispatch === "http-self") {
451
+ internalStep.dispatch = step.dispatch;
452
+ }
361
453
  // Inputs land on nodeConfig so the blueprint mapper resolves
362
454
  // $.<path> / js/... refs before SubworkflowNode reads them via
363
455
  // `ctx.config[step.name]`.
@@ -425,6 +517,305 @@ function normalizeWaitStep(step, index) {
425
517
  waitUntil,
426
518
  };
427
519
  }
520
+ // v0.5 forEach reference for the internal step's `node` field.
521
+ const FOR_EACH_NODE_REF = "@blokjs/forEach";
522
+ const LOOP_NODE_REF = "@blokjs/loop";
523
+ const SWITCH_NODE_REF = "@blokjs/switch";
524
+ const TRY_CATCH_NODE_REF = "@blokjs/tryCatch";
525
+ /**
526
+ * Normalize a v0.5 forEach step into the internal shape. Inner steps
527
+ * are recursively normalized via `normalizeRegularStep` so they get
528
+ * their inputs inlined; their nodeConfigs bubble up via `innerNodes`
529
+ * for the top-level `internalNodes` map (same pattern as branch).
530
+ */
531
+ function normalizeForEachStep(step, index) {
532
+ const id = pickString(step.id);
533
+ if (!id) {
534
+ throw new Error(`[blok] WorkflowNormalizer: forEach step at index ${index} is missing \`id\`.`);
535
+ }
536
+ const fe = step.forEach;
537
+ const inField = fe.in;
538
+ if (inField === undefined) {
539
+ throw new Error(`[blok] WorkflowNormalizer: forEach step "${id}" is missing \`in\`.`);
540
+ }
541
+ const as = pickString(fe.as);
542
+ if (!as) {
543
+ throw new Error(`[blok] WorkflowNormalizer: forEach step "${id}" is missing \`as\` (per-iteration variable name).`);
544
+ }
545
+ const mode = fe.mode === "parallel" ? "parallel" : "sequential";
546
+ const concurrency = typeof fe.concurrency === "number" && fe.concurrency > 0 ? fe.concurrency : 10;
547
+ const doSteps = Array.isArray(fe.do) ? fe.do : [];
548
+ // v0.6 Phase 3 — parallel forEach + wait composition warning. When
549
+ // a wait fires inside a parallel iteration, peer in-flight iterations
550
+ // are cancelled and re-launched on resume. Re-launches re-execute
551
+ // side effects unless inner steps have `idempotencyKey` (or are
552
+ // naturally side-effect-free). This is a per-author-contract gotcha
553
+ // (see `docs/c/devtools/parallel-foreach-wait-spec.mdx#author-contract`).
554
+ // Warn at load time so authors notice; don't BLOCK the workflow —
555
+ // some authors deliberately accept the retry behavior (e.g. pure
556
+ // fetches) or have their own idempotency layer.
557
+ if (mode === "parallel" && doStepsContainWait(doSteps)) {
558
+ const nonIdempotentInnerSteps = doSteps
559
+ .filter((s) => isPlainObject(s))
560
+ .filter((s) => !isWaitStep(s) && s.idempotencyKey === undefined)
561
+ .map((s) => pickString(s.id) ?? "(unnamed)");
562
+ if (nonIdempotentInnerSteps.length > 0) {
563
+ const stepList = nonIdempotentInnerSteps.map((n) => `"${n}"`).join(", ");
564
+ console.warn(`[blok][normalizer] forEach "${id}" runs in parallel mode with a wait inside the iteration body. When the wait fires, peer iterations are cancelled and re-launched from scratch on resume. Inner steps without an \`idempotencyKey\` will re-execute their side effects on each re-launch: ${stepList}. Add \`idempotencyKey\` to side-effecting steps, OR confirm the steps are side-effect-free. See docs/c/devtools/parallel-foreach-wait-spec.mdx for the author contract.`);
565
+ }
566
+ }
567
+ const { innerInternal, innerNodes } = normalizeStepBlock(doSteps);
568
+ const internalStep = {
569
+ name: id,
570
+ node: FOR_EACH_NODE_REF,
571
+ type: "forEach",
572
+ active: step.active === undefined ? true : Boolean(step.active),
573
+ stop: step.stop === true,
574
+ };
575
+ // nodeConfig — top-level `steps` triggers Configuration's
576
+ // isFlowWithProperties path which materializes into NodeBase[].
577
+ const nodeConfig = {
578
+ in: inField,
579
+ as,
580
+ mode,
581
+ concurrency,
582
+ steps: innerInternal,
583
+ };
584
+ return { internalStep, nodeConfig, innerNodes };
585
+ }
586
+ /**
587
+ * v0.6 Phase 3 — recognise a wait step in the raw (pre-normalization)
588
+ * shape. Two encodings: v0.5 `{ id, wait: {for|until} }` OR legacy
589
+ * `{ id, type: "wait", waitForMs|waitUntil }`. Used by
590
+ * `normalizeForEachStep` to emit the parallel + wait warning.
591
+ */
592
+ function isWaitStep(step) {
593
+ if (isPlainObject(step.wait))
594
+ return true;
595
+ if (step.type === "wait")
596
+ return true;
597
+ return false;
598
+ }
599
+ /**
600
+ * v0.6 Phase 3 — scan the doSteps array (one level deep) for a wait
601
+ * step. Doesn't recurse into nested primitives (forEach inside forEach,
602
+ * tryCatch inside forEach) — those are out of scope for this warning;
603
+ * Phase 4 nested-primitive work will extend.
604
+ */
605
+ function doStepsContainWait(doSteps) {
606
+ for (const s of doSteps) {
607
+ if (isPlainObject(s) && isWaitStep(s))
608
+ return true;
609
+ }
610
+ return false;
611
+ }
612
+ /**
613
+ * Normalize a v0.5 loop step into the internal shape. Same inner-step
614
+ * propagation pattern as forEach.
615
+ */
616
+ function normalizeLoopStep(step, index) {
617
+ const id = pickString(step.id);
618
+ if (!id) {
619
+ throw new Error(`[blok] WorkflowNormalizer: loop step at index ${index} is missing \`id\`.`);
620
+ }
621
+ const lp = step.loop;
622
+ const whileExpr = pickString(lp.while);
623
+ if (!whileExpr) {
624
+ throw new Error(`[blok] WorkflowNormalizer: loop step "${id}" is missing \`while\` (the JS condition string).`);
625
+ }
626
+ const maxIterations = typeof lp.maxIterations === "number" && lp.maxIterations > 0 ? lp.maxIterations : 1000;
627
+ const doSteps = Array.isArray(lp.do) ? lp.do : [];
628
+ const { innerInternal, innerNodes } = normalizeStepBlock(doSteps);
629
+ const internalStep = {
630
+ name: id,
631
+ node: LOOP_NODE_REF,
632
+ type: "loop",
633
+ active: step.active === undefined ? true : Boolean(step.active),
634
+ stop: step.stop === true,
635
+ };
636
+ const nodeConfig = {
637
+ while: whileExpr,
638
+ maxIterations,
639
+ steps: innerInternal,
640
+ };
641
+ return { internalStep, nodeConfig, innerNodes };
642
+ }
643
+ /**
644
+ * Helper used by `normalizeSwitchStep` — converts an array of authored
645
+ * step shapes (the `do` block of a case or the `default` block) into
646
+ * resolved InternalSteps + a merged innerNodes map. Mirrors the inner
647
+ * loop in `normalizeForEachStep` / `normalizeLoopStep`, recursing into
648
+ * nested branch / forEach / loop / switch as needed.
649
+ */
650
+ function normalizeStepBlock(rawSteps) {
651
+ const innerInternal = [];
652
+ const innerNodes = {};
653
+ for (let i = 0; i < rawSteps.length; i++) {
654
+ const s = rawSteps[i];
655
+ if (!isPlainObject(s))
656
+ continue;
657
+ if (isPlainObject(s.branch)) {
658
+ const { internalStep: nestedStep, nodeConfig: nestedConfig, innerNodes: nestedInner, } = normalizeBranchStep(s, i);
659
+ innerInternal.push(nestedStep);
660
+ innerNodes[nestedStep.name] = nestedConfig;
661
+ Object.assign(innerNodes, nestedInner);
662
+ continue;
663
+ }
664
+ if (isPlainObject(s.wait)) {
665
+ const nestedStep = normalizeWaitStep(s, i);
666
+ innerInternal.push(nestedStep);
667
+ continue;
668
+ }
669
+ if (isPlainObject(s.forEach)) {
670
+ const { internalStep: nestedStep, nodeConfig: nestedConfig, innerNodes: nestedInner, } = normalizeForEachStep(s, i);
671
+ innerInternal.push(nestedStep);
672
+ innerNodes[nestedStep.name] = nestedConfig;
673
+ Object.assign(innerNodes, nestedInner);
674
+ continue;
675
+ }
676
+ if (isPlainObject(s.loop)) {
677
+ const { internalStep: nestedStep, nodeConfig: nestedConfig, innerNodes: nestedInner, } = normalizeLoopStep(s, i);
678
+ innerInternal.push(nestedStep);
679
+ innerNodes[nestedStep.name] = nestedConfig;
680
+ Object.assign(innerNodes, nestedInner);
681
+ continue;
682
+ }
683
+ if (isPlainObject(s.switch)) {
684
+ const { internalStep: nestedStep, nodeConfig: nestedConfig, innerNodes: nestedInner, } = normalizeSwitchStep(s, i);
685
+ innerInternal.push(nestedStep);
686
+ innerNodes[nestedStep.name] = nestedConfig;
687
+ Object.assign(innerNodes, nestedInner);
688
+ continue;
689
+ }
690
+ if (isPlainObject(s.tryCatch)) {
691
+ const { internalStep: nestedStep, nodeConfig: nestedConfig, innerNodes: nestedInner, } = normalizeTryCatchStep(s, i);
692
+ innerInternal.push(nestedStep);
693
+ innerNodes[nestedStep.name] = nestedConfig;
694
+ Object.assign(innerNodes, nestedInner);
695
+ continue;
696
+ }
697
+ if (typeof s.subworkflow === "string") {
698
+ const { internalStep: nestedStep, nodeConfig: nestedConfig } = normalizeSubworkflowStep(s, i);
699
+ innerInternal.push(nestedStep);
700
+ if (nestedConfig)
701
+ innerNodes[nestedStep.name] = nestedConfig;
702
+ continue;
703
+ }
704
+ const { internalStep: regularStep, nodeConfig } = normalizeRegularStep(s, {}, i);
705
+ if (nodeConfig?.inputs) {
706
+ regularStep.inputs = nodeConfig.inputs;
707
+ }
708
+ if (nodeConfig)
709
+ innerNodes[regularStep.name] = nodeConfig;
710
+ innerInternal.push(regularStep);
711
+ }
712
+ return { innerInternal, innerNodes };
713
+ }
714
+ /**
715
+ * Normalize a v0.5 switch step into the internal shape. The cases and
716
+ * optional default each carry their own inner-step list — Configuration
717
+ * resolves them via a dedicated branch in `getNodes()` (mirrors the
718
+ * tryCatch path: each sub-block becomes its own resolved Flow).
719
+ *
720
+ * SwitchNode at run time reads the resolved nodeConfig:
721
+ * { on, cases: [{when, steps: NodeBase[]}], default?: NodeBase[] }
722
+ * and runs the matched case (or default) through a child Runner.
723
+ */
724
+ function normalizeSwitchStep(step, index) {
725
+ const id = pickString(step.id);
726
+ if (!id) {
727
+ throw new Error(`[blok] WorkflowNormalizer: switch step at index ${index} is missing \`id\`.`);
728
+ }
729
+ const sw = step.switch;
730
+ if (sw.on === undefined) {
731
+ throw new Error(`[blok] WorkflowNormalizer: switch step "${id}" is missing \`on\` (the value to match against).`);
732
+ }
733
+ const rawCases = Array.isArray(sw.cases) ? sw.cases : [];
734
+ if (rawCases.length === 0) {
735
+ throw new Error(`[blok] WorkflowNormalizer: switch step "${id}" has no \`cases\` (need at least one).`);
736
+ }
737
+ const cases = [];
738
+ const innerNodes = {};
739
+ for (let ci = 0; ci < rawCases.length; ci++) {
740
+ const c = rawCases[ci];
741
+ if (!isPlainObject(c)) {
742
+ throw new Error(`[blok] WorkflowNormalizer: switch step "${id}" cases[${ci}] is not an object.`);
743
+ }
744
+ const cobj = c;
745
+ if (cobj.when === undefined) {
746
+ throw new Error(`[blok] WorkflowNormalizer: switch step "${id}" cases[${ci}] is missing \`when\`.`);
747
+ }
748
+ const doSteps = Array.isArray(cobj.do) ? cobj.do : [];
749
+ const { innerInternal, innerNodes: caseInner } = normalizeStepBlock(doSteps);
750
+ Object.assign(innerNodes, caseInner);
751
+ cases.push({ when: cobj.when, steps: innerInternal });
752
+ }
753
+ let defaultSteps;
754
+ if (Array.isArray(sw.default)) {
755
+ const { innerInternal, innerNodes: defaultInner } = normalizeStepBlock(sw.default);
756
+ Object.assign(innerNodes, defaultInner);
757
+ defaultSteps = innerInternal;
758
+ }
759
+ const internalStep = {
760
+ name: id,
761
+ node: SWITCH_NODE_REF,
762
+ type: "switch",
763
+ active: step.active === undefined ? true : Boolean(step.active),
764
+ stop: step.stop === true,
765
+ };
766
+ const nodeConfig = {
767
+ on: sw.on,
768
+ cases,
769
+ ...(defaultSteps !== undefined ? { default: defaultSteps } : {}),
770
+ };
771
+ return { internalStep, nodeConfig, innerNodes };
772
+ }
773
+ /**
774
+ * Normalize a v0.5 tryCatch step into the internal shape. Each of `try`,
775
+ * `catch`, and optional `finally` carries its own inner-step list —
776
+ * Configuration resolves them via a dedicated branch in `getNodes()` so
777
+ * each block becomes its own resolved Flow (steps: NodeBase[]).
778
+ *
779
+ * TryCatchNode at run time reads the resolved nodeConfig:
780
+ * { try: NodeBase[], catch: NodeBase[], finally?: NodeBase[] }
781
+ * and runs them according to JS-like try/catch/finally semantics.
782
+ */
783
+ function normalizeTryCatchStep(step, index) {
784
+ const id = pickString(step.id);
785
+ if (!id) {
786
+ throw new Error(`[blok] WorkflowNormalizer: tryCatch step at index ${index} is missing \`id\`.`);
787
+ }
788
+ const tc = step.tryCatch;
789
+ if (!Array.isArray(tc.try) || tc.try.length === 0) {
790
+ throw new Error(`[blok] WorkflowNormalizer: tryCatch step "${id}" requires a non-empty \`try\` block.`);
791
+ }
792
+ if (!Array.isArray(tc.catch) || tc.catch.length === 0) {
793
+ throw new Error(`[blok] WorkflowNormalizer: tryCatch step "${id}" requires a non-empty \`catch\` block.`);
794
+ }
795
+ const innerNodes = {};
796
+ const tryBlock = normalizeStepBlock(tc.try);
797
+ Object.assign(innerNodes, tryBlock.innerNodes);
798
+ const catchBlock = normalizeStepBlock(tc.catch);
799
+ Object.assign(innerNodes, catchBlock.innerNodes);
800
+ let finallyBlock;
801
+ if (Array.isArray(tc.finally)) {
802
+ finallyBlock = normalizeStepBlock(tc.finally);
803
+ Object.assign(innerNodes, finallyBlock.innerNodes);
804
+ }
805
+ const internalStep = {
806
+ name: id,
807
+ node: TRY_CATCH_NODE_REF,
808
+ type: "tryCatch",
809
+ active: step.active === undefined ? true : Boolean(step.active),
810
+ stop: step.stop === true,
811
+ };
812
+ const nodeConfig = {
813
+ try: tryBlock.innerInternal,
814
+ catch: catchBlock.innerInternal,
815
+ ...(finallyBlock !== undefined ? { finally: finallyBlock.innerInternal } : {}),
816
+ };
817
+ return { internalStep, nodeConfig, innerNodes };
818
+ }
428
819
  function normalizeTrigger(rawTrigger, sourcePath) {
429
820
  if (!isPlainObject(rawTrigger))
430
821
  return {};
@@ -483,4 +874,63 @@ function pickString(value) {
483
874
  export function _resetWildcardWarningCache() {
484
875
  _wildcardWarnedFiles = new Set();
485
876
  }
877
+ /**
878
+ * `set_var` is no longer accepted on workflow steps. Walk the raw step
879
+ * tree (top-level steps + every nested sub-pipeline) and throw with a
880
+ * migration hint on the first occurrence. The walk handles branch,
881
+ * forEach, loop, switch, and tryCatch sub-pipelines so a rejected
882
+ * field at any depth is caught at load time rather than after the
883
+ * partial-normalization point.
884
+ */
885
+ function assertNoSetVar(steps, sourcePath) {
886
+ for (const raw of steps) {
887
+ if (!isPlainObject(raw))
888
+ continue;
889
+ const step = raw;
890
+ if (Object.prototype.hasOwnProperty.call(step, "set_var")) {
891
+ const id = pickString(step.id) ?? pickString(step.name) ?? "<unnamed>";
892
+ const suffix = sourcePath ? ` (file: ${sourcePath})` : "";
893
+ throw new Error(`[blok] WorkflowNormalizer: step "${id}" uses \`set_var\`, which was removed in v0.5. Replace \`set_var: false\` with \`ephemeral: true\` and drop \`set_var: true\` (v2 default-stores every step's output). Run \`blokctl migrate workflows\` to convert v1 workflows automatically.${suffix}`);
894
+ }
895
+ // Recurse into nested sub-pipelines.
896
+ if (isPlainObject(step.branch)) {
897
+ const branch = step.branch;
898
+ if (Array.isArray(branch.then))
899
+ assertNoSetVar(branch.then, sourcePath);
900
+ if (Array.isArray(branch.else))
901
+ assertNoSetVar(branch.else, sourcePath);
902
+ }
903
+ if (isPlainObject(step.forEach)) {
904
+ const fe = step.forEach;
905
+ if (Array.isArray(fe.do))
906
+ assertNoSetVar(fe.do, sourcePath);
907
+ }
908
+ if (isPlainObject(step.loop)) {
909
+ const lp = step.loop;
910
+ if (Array.isArray(lp.do))
911
+ assertNoSetVar(lp.do, sourcePath);
912
+ }
913
+ if (isPlainObject(step.switch)) {
914
+ const sw = step.switch;
915
+ if (Array.isArray(sw.cases)) {
916
+ for (const c of sw.cases) {
917
+ if (isPlainObject(c) && Array.isArray(c.do)) {
918
+ assertNoSetVar(c.do, sourcePath);
919
+ }
920
+ }
921
+ }
922
+ if (Array.isArray(sw.default))
923
+ assertNoSetVar(sw.default, sourcePath);
924
+ }
925
+ if (isPlainObject(step.tryCatch)) {
926
+ const tc = step.tryCatch;
927
+ if (Array.isArray(tc.try))
928
+ assertNoSetVar(tc.try, sourcePath);
929
+ if (Array.isArray(tc.catch))
930
+ assertNoSetVar(tc.catch, sourcePath);
931
+ if (Array.isArray(tc.finally))
932
+ assertNoSetVar(tc.finally, sourcePath);
933
+ }
934
+ }
935
+ }
486
936
  //# sourceMappingURL=WorkflowNormalizer.js.map