@blokjs/runner 0.2.2 → 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 (213) hide show
  1. package/dist/Blok.js +32 -3
  2. package/dist/Blok.js.map +1 -1
  3. package/dist/Configuration.d.ts +59 -5
  4. package/dist/Configuration.js +366 -96
  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/PayloadTooLargeError.d.ts +19 -0
  16. package/dist/PayloadTooLargeError.js +29 -0
  17. package/dist/PayloadTooLargeError.js.map +1 -0
  18. package/dist/RunCancelledError.d.ts +17 -0
  19. package/dist/RunCancelledError.js +25 -0
  20. package/dist/RunCancelledError.js.map +1 -0
  21. package/dist/Runner.d.ts +11 -1
  22. package/dist/Runner.js +9 -2
  23. package/dist/Runner.js.map +1 -1
  24. package/dist/RunnerSteps.js +648 -44
  25. package/dist/RunnerSteps.js.map +1 -1
  26. package/dist/RuntimeAdapterNode.d.ts +2 -1
  27. package/dist/RuntimeAdapterNode.js +2 -2
  28. package/dist/RuntimeAdapterNode.js.map +1 -1
  29. package/dist/RuntimeRegistry.d.ts +23 -2
  30. package/dist/RuntimeRegistry.js +31 -2
  31. package/dist/RuntimeRegistry.js.map +1 -1
  32. package/dist/SubworkflowNode.d.ts +181 -0
  33. package/dist/SubworkflowNode.js +479 -0
  34. package/dist/SubworkflowNode.js.map +1 -0
  35. package/dist/SwitchNode.d.ts +37 -0
  36. package/dist/SwitchNode.js +153 -0
  37. package/dist/SwitchNode.js.map +1 -0
  38. package/dist/TriggerBase.d.ts +178 -0
  39. package/dist/TriggerBase.js +1032 -5
  40. package/dist/TriggerBase.js.map +1 -1
  41. package/dist/TryCatchNode.d.ts +32 -0
  42. package/dist/TryCatchNode.js +207 -0
  43. package/dist/TryCatchNode.js.map +1 -0
  44. package/dist/WaitDispatchRequest.d.ts +38 -0
  45. package/dist/WaitDispatchRequest.js +13 -0
  46. package/dist/WaitDispatchRequest.js.map +1 -0
  47. package/dist/WaitNode.d.ts +23 -0
  48. package/dist/WaitNode.js +26 -0
  49. package/dist/WaitNode.js.map +1 -0
  50. package/dist/adapters/grpc/GrpcCodec.js +2 -2
  51. package/dist/adapters/grpc/GrpcRuntimeAdapter.d.ts +6 -4
  52. package/dist/adapters/grpc/GrpcRuntimeAdapter.js +6 -4
  53. package/dist/adapters/grpc/GrpcRuntimeAdapter.js.map +1 -1
  54. package/dist/adapters/grpc/types.d.ts +7 -5
  55. package/dist/adapters/grpc/types.js.map +1 -1
  56. package/dist/adapters/transport.d.ts +12 -41
  57. package/dist/adapters/transport.js +21 -70
  58. package/dist/adapters/transport.js.map +1 -1
  59. package/dist/cache/NodeResultCache.js +7 -0
  60. package/dist/cache/NodeResultCache.js.map +1 -1
  61. package/dist/concurrency/ConcurrencyBackend.d.ts +61 -0
  62. package/dist/concurrency/ConcurrencyBackend.js +20 -0
  63. package/dist/concurrency/ConcurrencyBackend.js.map +1 -0
  64. package/dist/concurrency/ConcurrencyLimitError.d.ts +37 -0
  65. package/dist/concurrency/ConcurrencyLimitError.js +16 -0
  66. package/dist/concurrency/ConcurrencyLimitError.js.map +1 -0
  67. package/dist/concurrency/NatsKvConcurrencyBackend.d.ts +64 -0
  68. package/dist/concurrency/NatsKvConcurrencyBackend.js +310 -0
  69. package/dist/concurrency/NatsKvConcurrencyBackend.js.map +1 -0
  70. package/dist/concurrency/QueueExpiredError.d.ts +40 -0
  71. package/dist/concurrency/QueueExpiredError.js +15 -0
  72. package/dist/concurrency/QueueExpiredError.js.map +1 -0
  73. package/dist/concurrency/RedisConcurrencyBackend.d.ts +64 -0
  74. package/dist/concurrency/RedisConcurrencyBackend.js +374 -0
  75. package/dist/concurrency/RedisConcurrencyBackend.js.map +1 -0
  76. package/dist/concurrency/createConcurrencyBackend.d.ts +24 -0
  77. package/dist/concurrency/createConcurrencyBackend.js +38 -0
  78. package/dist/concurrency/createConcurrencyBackend.js.map +1 -0
  79. package/dist/concurrency/readConcurrencyConfig.d.ts +60 -0
  80. package/dist/concurrency/readConcurrencyConfig.js +60 -0
  81. package/dist/concurrency/readConcurrencyConfig.js.map +1 -0
  82. package/dist/defineNode.d.ts +8 -0
  83. package/dist/defineNode.js +25 -5
  84. package/dist/defineNode.js.map +1 -1
  85. package/dist/graphql/GraphQLSchemaGenerator.js +1 -1
  86. package/dist/graphql/GraphQLSchemaGenerator.js.map +1 -1
  87. package/dist/idempotency/resolveIdempotencyKey.d.ts +20 -0
  88. package/dist/idempotency/resolveIdempotencyKey.js +37 -0
  89. package/dist/idempotency/resolveIdempotencyKey.js.map +1 -0
  90. package/dist/index.d.ts +30 -6
  91. package/dist/index.js +55 -6
  92. package/dist/index.js.map +1 -1
  93. package/dist/marketplace/RuntimeCatalog.d.ts +6 -0
  94. package/dist/marketplace/RuntimeCatalog.js.map +1 -1
  95. package/dist/marketplace/RuntimeDiscovery.d.ts +2 -2
  96. package/dist/marketplace/RuntimeDiscovery.js +18 -6
  97. package/dist/marketplace/RuntimeDiscovery.js.map +1 -1
  98. package/dist/monitoring/ConcurrencyMetrics.d.ts +82 -0
  99. package/dist/monitoring/ConcurrencyMetrics.js +139 -0
  100. package/dist/monitoring/ConcurrencyMetrics.js.map +1 -0
  101. package/dist/monitoring/ForEachWaitMetrics.d.ts +22 -0
  102. package/dist/monitoring/ForEachWaitMetrics.js +36 -0
  103. package/dist/monitoring/ForEachWaitMetrics.js.map +1 -0
  104. package/dist/monitoring/JanitorMetrics.d.ts +27 -0
  105. package/dist/monitoring/JanitorMetrics.js +48 -0
  106. package/dist/monitoring/JanitorMetrics.js.map +1 -0
  107. package/dist/openapi/OpenAPIGenerator.js +7 -2
  108. package/dist/openapi/OpenAPIGenerator.js.map +1 -1
  109. package/dist/runtime/PrimitiveStack.d.ts +64 -0
  110. package/dist/runtime/PrimitiveStack.js +92 -0
  111. package/dist/runtime/PrimitiveStack.js.map +1 -0
  112. package/dist/scheduling/DebounceBackend.d.ts +108 -0
  113. package/dist/scheduling/DebounceBackend.js +23 -0
  114. package/dist/scheduling/DebounceBackend.js.map +1 -0
  115. package/dist/scheduling/DebounceCoordinator.d.ts +141 -0
  116. package/dist/scheduling/DebounceCoordinator.js +362 -0
  117. package/dist/scheduling/DebounceCoordinator.js.map +1 -0
  118. package/dist/scheduling/DeferredDispatchSignal.d.ts +50 -0
  119. package/dist/scheduling/DeferredDispatchSignal.js +14 -0
  120. package/dist/scheduling/DeferredDispatchSignal.js.map +1 -0
  121. package/dist/scheduling/DeferredRunScheduler.d.ts +96 -0
  122. package/dist/scheduling/DeferredRunScheduler.js +256 -0
  123. package/dist/scheduling/DeferredRunScheduler.js.map +1 -0
  124. package/dist/scheduling/NatsKvDebounceBackend.d.ts +53 -0
  125. package/dist/scheduling/NatsKvDebounceBackend.js +334 -0
  126. package/dist/scheduling/NatsKvDebounceBackend.js.map +1 -0
  127. package/dist/scheduling/RedisDebounceBackend.d.ts +49 -0
  128. package/dist/scheduling/RedisDebounceBackend.js +356 -0
  129. package/dist/scheduling/RedisDebounceBackend.js.map +1 -0
  130. package/dist/scheduling/createDebounceBackend.d.ts +25 -0
  131. package/dist/scheduling/createDebounceBackend.js +39 -0
  132. package/dist/scheduling/createDebounceBackend.js.map +1 -0
  133. package/dist/scheduling/readSchedulingConfig.d.ts +24 -0
  134. package/dist/scheduling/readSchedulingConfig.js +52 -0
  135. package/dist/scheduling/readSchedulingConfig.js.map +1 -0
  136. package/dist/security/AuditLogger.js +1 -1
  137. package/dist/security/AuditLogger.js.map +1 -1
  138. package/dist/security/AuthMiddleware.d.ts +19 -20
  139. package/dist/security/AuthMiddleware.js +35 -20
  140. package/dist/security/AuthMiddleware.js.map +1 -1
  141. package/dist/security/OAuthProvider.js +2 -2
  142. package/dist/security/OAuthProvider.js.map +1 -1
  143. package/dist/security/SecretManager.js +14 -13
  144. package/dist/security/SecretManager.js.map +1 -1
  145. package/dist/security/index.d.ts +3 -1
  146. package/dist/security/index.js +3 -1
  147. package/dist/security/index.js.map +1 -1
  148. package/dist/testing/TestHarness.d.ts +27 -12
  149. package/dist/testing/TestHarness.js +19 -3
  150. package/dist/testing/TestHarness.js.map +1 -1
  151. package/dist/testing/WorkflowTestRunner.js +0 -7
  152. package/dist/testing/WorkflowTestRunner.js.map +1 -1
  153. package/dist/timeouts/StepTimeoutError.d.ts +22 -0
  154. package/dist/timeouts/StepTimeoutError.js +31 -0
  155. package/dist/timeouts/StepTimeoutError.js.map +1 -0
  156. package/dist/tracing/InMemoryRunStore.d.ts +41 -1
  157. package/dist/tracing/InMemoryRunStore.js +239 -0
  158. package/dist/tracing/InMemoryRunStore.js.map +1 -1
  159. package/dist/tracing/Janitor.d.ts +70 -0
  160. package/dist/tracing/Janitor.js +150 -0
  161. package/dist/tracing/Janitor.js.map +1 -0
  162. package/dist/tracing/PostgresRunStore.d.ts +57 -1
  163. package/dist/tracing/PostgresRunStore.js +711 -6
  164. package/dist/tracing/PostgresRunStore.js.map +1 -1
  165. package/dist/tracing/RoutingDiagnostics.d.ts +55 -0
  166. package/dist/tracing/RoutingDiagnostics.js +50 -0
  167. package/dist/tracing/RoutingDiagnostics.js.map +1 -0
  168. package/dist/tracing/RunStore.d.ts +181 -1
  169. package/dist/tracing/RunTracker.d.ts +244 -9
  170. package/dist/tracing/RunTracker.js +594 -1
  171. package/dist/tracing/RunTracker.js.map +1 -1
  172. package/dist/tracing/SqliteRunStore.d.ts +79 -2
  173. package/dist/tracing/SqliteRunStore.js +775 -16
  174. package/dist/tracing/SqliteRunStore.js.map +1 -1
  175. package/dist/tracing/TraceRouter.d.ts +20 -2
  176. package/dist/tracing/TraceRouter.js +612 -6
  177. package/dist/tracing/TraceRouter.js.map +1 -1
  178. package/dist/tracing/createStore.js +14 -3
  179. package/dist/tracing/createStore.js.map +1 -1
  180. package/dist/tracing/metadataFilter.d.ts +63 -0
  181. package/dist/tracing/metadataFilter.js +224 -0
  182. package/dist/tracing/metadataFilter.js.map +1 -0
  183. package/dist/tracing/sanitize.d.ts +11 -0
  184. package/dist/tracing/sanitize.js +29 -0
  185. package/dist/tracing/sanitize.js.map +1 -1
  186. package/dist/tracing/types.d.ts +672 -2
  187. package/dist/utils/createChildContext.d.ts +32 -0
  188. package/dist/utils/createChildContext.js +113 -0
  189. package/dist/utils/createChildContext.js.map +1 -0
  190. package/dist/utils/envAllowlist.d.ts +35 -0
  191. package/dist/utils/envAllowlist.js +113 -0
  192. package/dist/utils/envAllowlist.js.map +1 -0
  193. package/dist/version/RuntimeVersionValidator.d.ts +38 -0
  194. package/dist/version/RuntimeVersionValidator.js +121 -0
  195. package/dist/version/RuntimeVersionValidator.js.map +1 -0
  196. package/dist/visualization/WorkflowVisualizer.js +4 -4
  197. package/dist/visualization/WorkflowVisualizer.js.map +1 -1
  198. package/dist/workflow/PersistenceHelper.d.ts +18 -10
  199. package/dist/workflow/PersistenceHelper.js +35 -9
  200. package/dist/workflow/PersistenceHelper.js.map +1 -1
  201. package/dist/workflow/WorkflowNormalizer.d.ts +48 -42
  202. package/dist/workflow/WorkflowNormalizer.js +650 -18
  203. package/dist/workflow/WorkflowNormalizer.js.map +1 -1
  204. package/dist/workflow/WorkflowRegistry.d.ts +186 -0
  205. package/dist/workflow/WorkflowRegistry.js +202 -0
  206. package/dist/workflow/WorkflowRegistry.js.map +1 -0
  207. package/dist/workflow/sampleBody.d.ts +54 -0
  208. package/dist/workflow/sampleBody.js +320 -0
  209. package/dist/workflow/sampleBody.js.map +1 -0
  210. package/package.json +3 -8
  211. package/dist/adapters/HttpRuntimeAdapter.d.ts +0 -79
  212. package/dist/adapters/HttpRuntimeAdapter.js +0 -233
  213. package/dist/adapters/HttpRuntimeAdapter.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { parseDuration } from "@blokjs/helper";
1
2
  /**
2
3
  * WorkflowNormalizer — accepts v1 or v2 workflow shapes and projects both
3
4
  * to the single canonical internal shape that `Configuration.getSteps` /
@@ -9,7 +10,7 @@
9
10
  * ```
10
11
  * {
11
12
  * name, version, trigger,
12
- * steps: [{ name, node, type, active?, stop?, set_var? }],
13
+ * steps: [{ name, node, type, active?, stop? }],
13
14
  * nodes: { [stepName]: { inputs?, conditions? } }
14
15
  * }
15
16
  * ```
@@ -29,7 +30,7 @@
29
30
  * ```
30
31
  * {
31
32
  * name, version, trigger, // method "*" normalized to "ANY"
32
- * steps: [{ name, node, type, active, stop, set_var, as, spread, ephemeral, ... }],
33
+ * steps: [{ name, node, type, active, stop, as, spread, ephemeral, ... }],
33
34
  * nodes: { [stepName]: { inputs?, conditions? } }
34
35
  * }
35
36
  * ```
@@ -66,9 +67,33 @@ export function normalizeWorkflow(raw, sourcePath) {
66
67
  // Legacy builder shape — same unwrap.
67
68
  wf = wf._config;
68
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
+ }
69
77
  const name = typeof wf.name === "string" ? wf.name : "";
70
78
  const version = typeof wf.version === "string" ? wf.version : "1.0.0";
71
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
+ }
72
97
  // --- Trigger normalization (method "*" → "ANY") ---
73
98
  const trigger = normalizeTrigger(wf.trigger, sourcePath);
74
99
  // --- Steps normalization ---
@@ -83,9 +108,67 @@ export function normalizeWorkflow(raw, sourcePath) {
83
108
  const step = rawStep;
84
109
  // v2 branch — { id, branch: { when, then, else? } }
85
110
  if (isPlainObject(step.branch)) {
86
- const { internalStep, nodeConfig } = normalizeBranchStep(step, i);
111
+ const { internalStep, nodeConfig, innerNodes } = normalizeBranchStep(step, i);
87
112
  internalSteps.push(internalStep);
88
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);
121
+ continue;
122
+ }
123
+ // v2 sub-workflow — { id, subworkflow: "<name>", inputs?, wait? }
124
+ // Discriminator is the presence of a non-empty `subworkflow` string.
125
+ // Resolves to a SubworkflowNode that looks up the child in the
126
+ // WorkflowRegistry at run time.
127
+ if (typeof step.subworkflow === "string" && step.subworkflow.length > 0) {
128
+ const { internalStep, nodeConfig } = normalizeSubworkflowStep(step, i);
129
+ internalSteps.push(internalStep);
130
+ if (nodeConfig)
131
+ internalNodes[internalStep.name] = nodeConfig;
132
+ continue;
133
+ }
134
+ // v2 wait — { id, wait: { for?, until? } } (PR 4).
135
+ // Discriminator: `wait` is an object (sub-workflow uses `wait: boolean`).
136
+ if (isPlainObject(step.wait) &&
137
+ (step.wait.for !== undefined || step.wait.until !== undefined)) {
138
+ const internalStep = normalizeWaitStep(step, i);
139
+ internalSteps.push(internalStep);
140
+ continue;
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);
89
172
  continue;
90
173
  }
91
174
  // v2 regular — { id, use, inputs?, as?, spread?, ephemeral?, ... }
@@ -113,6 +196,8 @@ export function normalizeWorkflow(raw, sourcePath) {
113
196
  trigger,
114
197
  steps: internalSteps,
115
198
  nodes: internalNodes,
199
+ ...(middleware ? { middleware } : {}),
200
+ ...(appliedMiddleware ? { appliedMiddleware } : {}),
116
201
  };
117
202
  }
118
203
  // =============================================================================
@@ -137,10 +222,9 @@ function normalizeRegularStep(step, nodesInput, index) {
137
222
  const v1NodeConfig = isPlainObject(nodesInput[id]) ? nodesInput[id] : null;
138
223
  const v1Inputs = v1NodeConfig?.inputs && isPlainObject(v1NodeConfig.inputs) ? v1NodeConfig.inputs : null;
139
224
  const inputs = inlineInputs ?? v1Inputs;
140
- // Persistence knobs — v2 first, legacy `set_var` mapped second.
141
- const ephemeralExplicit = step.ephemeral === true;
142
- const ephemeralFromLegacy = step.set_var === false;
143
- 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;
144
228
  const as = pickString(step.as);
145
229
  const spread = step.spread === true;
146
230
  // `as` and `spread` are mutually exclusive — caught at schema level too,
@@ -154,13 +238,38 @@ function normalizeRegularStep(step, nodesInput, index) {
154
238
  type,
155
239
  active: step.active === undefined ? true : Boolean(step.active),
156
240
  stop: step.stop === true,
157
- set_var: typeof step.set_var === "boolean" ? step.set_var : undefined,
158
241
  as,
159
242
  spread,
160
243
  ephemeral,
161
244
  };
162
245
  if (typeof step.stream_logs === "boolean")
163
246
  internalStep.stream_logs = step.stream_logs;
247
+ // Idempotency cache + retry — pass through verbatim. The runner reads
248
+ // these in RunnerSteps to wrap step.process() with cache-check + retry-
249
+ // loop. They never reach PersistenceHelper.applyStepOutput; caching
250
+ // layers ABOVE that.
251
+ if (typeof step.idempotencyKey === "string" && step.idempotencyKey.length > 0) {
252
+ internalStep.idempotencyKey = step.idempotencyKey;
253
+ }
254
+ if (typeof step.idempotencyKeyTTL === "number" && Number.isFinite(step.idempotencyKeyTTL)) {
255
+ internalStep.idempotencyKeyTTL = step.idempotencyKeyTTL;
256
+ }
257
+ if (isPlainObject(step.retry)) {
258
+ const r = step.retry;
259
+ if (typeof r.maxAttempts === "number" && Number.isInteger(r.maxAttempts)) {
260
+ const retry = { maxAttempts: r.maxAttempts };
261
+ if (typeof r.minTimeoutInMs === "number")
262
+ retry.minTimeoutInMs = r.minTimeoutInMs;
263
+ if (typeof r.maxTimeoutInMs === "number")
264
+ retry.maxTimeoutInMs = r.maxTimeoutInMs;
265
+ if (typeof r.factor === "number")
266
+ retry.factor = r.factor;
267
+ internalStep.retry = retry;
268
+ }
269
+ }
270
+ if (typeof step.maxDuration === "number" || typeof step.maxDuration === "string") {
271
+ internalStep.maxDuration = step.maxDuration;
272
+ }
164
273
  // Build node config — only include `inputs` if present.
165
274
  let nodeConfig = null;
166
275
  if (inputs) {
@@ -196,37 +305,49 @@ function normalizeBranchStep(step, index) {
196
305
  // `nodesInput` because v2 branches inline `inputs` on each nested step.
197
306
  const thenInternal = [];
198
307
  const elseInternal = [];
308
+ const innerNodes = {};
199
309
  for (let i = 0; i < thenSteps.length; i++) {
200
310
  const s = thenSteps[i];
201
311
  if (!isPlainObject(s))
202
312
  continue;
203
313
  if (isPlainObject(s.branch)) {
204
- const { internalStep } = normalizeBranchStep(s, i);
205
- 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);
206
318
  continue;
207
319
  }
208
- const { internalStep, nodeConfig } = normalizeRegularStep(s, {}, i);
320
+ const { internalStep: regularStep, nodeConfig } = normalizeRegularStep(s, {}, i);
209
321
  // Inline inputs on the step itself so nested-flow execution finds them.
210
322
  // (RunnerSteps recursively executes flow nodes' inner steps.)
211
323
  if (nodeConfig?.inputs) {
212
- internalStep.inputs = nodeConfig.inputs;
324
+ regularStep.inputs = nodeConfig.inputs;
213
325
  }
214
- 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);
215
332
  }
216
333
  for (let i = 0; i < elseSteps.length; i++) {
217
334
  const s = elseSteps[i];
218
335
  if (!isPlainObject(s))
219
336
  continue;
220
337
  if (isPlainObject(s.branch)) {
221
- const { internalStep } = normalizeBranchStep(s, i);
222
- 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);
223
342
  continue;
224
343
  }
225
- const { internalStep, nodeConfig } = normalizeRegularStep(s, {}, i);
344
+ const { internalStep: regularStep, nodeConfig } = normalizeRegularStep(s, {}, i);
226
345
  if (nodeConfig?.inputs) {
227
- internalStep.inputs = nodeConfig.inputs;
346
+ regularStep.inputs = nodeConfig.inputs;
228
347
  }
229
- elseInternal.push(internalStep);
348
+ if (nodeConfig)
349
+ innerNodes[regularStep.name] = nodeConfig;
350
+ elseInternal.push(regularStep);
230
351
  }
231
352
  const conditions = [{ type: "if", condition: when, steps: thenInternal }];
232
353
  if (elseInternal.length > 0) {
@@ -241,8 +362,460 @@ function normalizeBranchStep(step, index) {
241
362
  flow: true,
242
363
  };
243
364
  const nodeConfig = { conditions };
365
+ return { internalStep, nodeConfig, innerNodes };
366
+ }
367
+ const SUBWORKFLOW_NODE_REF = "@blokjs/subworkflow";
368
+ /**
369
+ * Normalize a v2 sub-workflow step into the canonical InternalStep
370
+ * shape. Resolves to a `SubworkflowNode` at run time
371
+ * (Configuration.nodeTypes.subworkflow).
372
+ *
373
+ * Inputs are placed on `nodeConfig.inputs` so the existing
374
+ * blueprint-mapper resolution path resolves `$.state.<id>` /
375
+ * `$.req.body.<key>` refs into concrete values BEFORE the
376
+ * sub-workflow node runs (mirrors how regular steps work).
377
+ */
378
+ function normalizeSubworkflowStep(step, index) {
379
+ const id = pickString(step.id);
380
+ if (!id) {
381
+ throw new Error(`[blok] WorkflowNormalizer: sub-workflow step at index ${index} is missing \`id\`.`);
382
+ }
383
+ const subworkflow = pickString(step.subworkflow);
384
+ if (!subworkflow) {
385
+ throw new Error(`[blok] WorkflowNormalizer: sub-workflow step "${id}" is missing \`subworkflow\` (workflow name to invoke).`);
386
+ }
387
+ // `wait: false` (fire-and-forget) is now supported. The async dispatch
388
+ // branch lives in SubworkflowNode.run; the field is threaded through
389
+ // onto InternalStep below for the resolver to copy onto the
390
+ // SubworkflowNode instance.
391
+ // Persistence + retry + idempotency knobs — pass through verbatim
392
+ // (mirrors normalizeRegularStep). `as` and `spread` mutual exclusion
393
+ // is also enforced at the schema level; defensive check here.
394
+ const ephemeral = step.ephemeral === true;
395
+ const as = pickString(step.as);
396
+ const spread = step.spread === true;
397
+ if (as && spread) {
398
+ throw new Error(`[blok] WorkflowNormalizer: sub-workflow step "${id}" sets both \`as\` and \`spread\` — they are mutually exclusive.`);
399
+ }
400
+ const internalStep = {
401
+ name: id,
402
+ node: SUBWORKFLOW_NODE_REF,
403
+ type: "subworkflow",
404
+ active: step.active === undefined ? true : Boolean(step.active),
405
+ stop: step.stop === true,
406
+ as,
407
+ spread,
408
+ ephemeral,
409
+ subworkflow,
410
+ // Default `wait: true` when omitted. `wait: false` triggers the
411
+ // async dispatch branch in SubworkflowNode.run.
412
+ wait: step.wait === undefined ? true : Boolean(step.wait),
413
+ };
414
+ if (typeof step.idempotencyKey === "string" && step.idempotencyKey.length > 0) {
415
+ internalStep.idempotencyKey = step.idempotencyKey;
416
+ }
417
+ if (typeof step.idempotencyKeyTTL === "number" && Number.isFinite(step.idempotencyKeyTTL)) {
418
+ internalStep.idempotencyKeyTTL = step.idempotencyKeyTTL;
419
+ }
420
+ if (isPlainObject(step.retry)) {
421
+ const r = step.retry;
422
+ if (typeof r.maxAttempts === "number" && Number.isInteger(r.maxAttempts)) {
423
+ const retry = { maxAttempts: r.maxAttempts };
424
+ if (typeof r.minTimeoutInMs === "number")
425
+ retry.minTimeoutInMs = r.minTimeoutInMs;
426
+ if (typeof r.maxTimeoutInMs === "number")
427
+ retry.maxTimeoutInMs = r.maxTimeoutInMs;
428
+ if (typeof r.factor === "number")
429
+ retry.factor = r.factor;
430
+ internalStep.retry = retry;
431
+ }
432
+ }
433
+ if (typeof step.maxDuration === "number" || typeof step.maxDuration === "string") {
434
+ internalStep.maxDuration = step.maxDuration;
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
+ }
453
+ // Inputs land on nodeConfig so the blueprint mapper resolves
454
+ // $.<path> / js/... refs before SubworkflowNode reads them via
455
+ // `ctx.config[step.name]`.
456
+ const inlineInputs = isPlainObject(step.inputs) ? step.inputs : null;
457
+ const nodeConfig = inlineInputs ? { inputs: inlineInputs } : null;
244
458
  return { internalStep, nodeConfig };
245
459
  }
460
+ const WAIT_NODE_REF = "@blokjs/wait";
461
+ /**
462
+ * PR 4 — normalize a v2 wait step.
463
+ *
464
+ * Wait steps are intercepted by `RunnerSteps` BEFORE `step.process` is
465
+ * invoked (the wait IS the runner-level deferral); the resolved node is
466
+ * a no-op placeholder. The runner reads `waitForMs` / `waitUntil` off
467
+ * the InternalStep to decide how long to wait.
468
+ *
469
+ * `wait.for` (duration string or number) is parsed to milliseconds via
470
+ * `parseDuration`. `wait.until` is left as-is — the runner resolves
471
+ * $-proxy expressions against the live ctx at first-pass invocation.
472
+ */
473
+ function normalizeWaitStep(step, index) {
474
+ const id = pickString(step.id);
475
+ if (!id) {
476
+ throw new Error(`[blok] WorkflowNormalizer: wait step at index ${index} is missing \`id\`.`);
477
+ }
478
+ const waitObj = step.wait;
479
+ const hasFor = waitObj.for !== undefined;
480
+ const hasUntil = waitObj.until !== undefined;
481
+ if (hasFor === hasUntil) {
482
+ throw new Error(`[blok] WorkflowNormalizer: wait step "${id}" must set exactly one of \`wait.for\` or \`wait.until\`.`);
483
+ }
484
+ let waitForMs;
485
+ let waitUntil;
486
+ if (hasFor) {
487
+ const raw = waitObj.for;
488
+ if (typeof raw === "number") {
489
+ waitForMs = raw;
490
+ }
491
+ else if (typeof raw === "string") {
492
+ // parseDuration may throw on invalid grammar — let it surface.
493
+ waitForMs = parseDuration(raw);
494
+ }
495
+ else {
496
+ throw new Error(`[blok] WorkflowNormalizer: wait step "${id}" has invalid \`wait.for\` (must be number ms or duration string).`);
497
+ }
498
+ }
499
+ if (hasUntil) {
500
+ const raw = waitObj.until;
501
+ if (typeof raw !== "number" && typeof raw !== "string") {
502
+ throw new Error(`[blok] WorkflowNormalizer: wait step "${id}" has invalid \`wait.until\` (must be number ms or string).`);
503
+ }
504
+ waitUntil = raw;
505
+ }
506
+ const ephemeral = step.ephemeral === true;
507
+ const as = pickString(step.as);
508
+ return {
509
+ name: id,
510
+ node: WAIT_NODE_REF,
511
+ type: "wait",
512
+ active: step.active === undefined ? true : step.active === true,
513
+ stop: step.stop === true,
514
+ as,
515
+ ephemeral,
516
+ waitForMs,
517
+ waitUntil,
518
+ };
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
+ }
246
819
  function normalizeTrigger(rawTrigger, sourcePath) {
247
820
  if (!isPlainObject(rawTrigger))
248
821
  return {};
@@ -301,4 +874,63 @@ function pickString(value) {
301
874
  export function _resetWildcardWarningCache() {
302
875
  _wildcardWarnedFiles = new Set();
303
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
+ }
304
936
  //# sourceMappingURL=WorkflowNormalizer.js.map