@blokjs/runner 0.2.1 → 0.4.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.
- package/dist/Blok.js +11 -11
- package/dist/Blok.js.map +1 -1
- package/dist/Configuration.d.ts +39 -2
- package/dist/Configuration.js +337 -28
- package/dist/Configuration.js.map +1 -1
- package/dist/ConfigurationResolver.d.ts +9 -0
- package/dist/ConfigurationResolver.js +17 -1
- package/dist/ConfigurationResolver.js.map +1 -1
- package/dist/PayloadTooLargeError.d.ts +19 -0
- package/dist/PayloadTooLargeError.js +29 -0
- package/dist/PayloadTooLargeError.js.map +1 -0
- package/dist/RunCancelledError.d.ts +17 -0
- package/dist/RunCancelledError.js +25 -0
- package/dist/RunCancelledError.js.map +1 -0
- package/dist/RunnerSteps.js +363 -23
- package/dist/RunnerSteps.js.map +1 -1
- package/dist/RuntimeAdapterNode.d.ts +32 -2
- package/dist/RuntimeAdapterNode.js +122 -27
- package/dist/RuntimeAdapterNode.js.map +1 -1
- package/dist/SubworkflowNode.d.ts +75 -0
- package/dist/SubworkflowNode.js +221 -0
- package/dist/SubworkflowNode.js.map +1 -0
- package/dist/TriggerBase.d.ts +128 -0
- package/dist/TriggerBase.js +808 -6
- package/dist/TriggerBase.js.map +1 -1
- package/dist/WaitDispatchRequest.d.ts +38 -0
- package/dist/WaitDispatchRequest.js +13 -0
- package/dist/WaitDispatchRequest.js.map +1 -0
- package/dist/WaitNode.d.ts +23 -0
- package/dist/WaitNode.js +26 -0
- package/dist/WaitNode.js.map +1 -0
- package/dist/adapters/BunRuntimeAdapter.d.ts +1 -0
- package/dist/adapters/BunRuntimeAdapter.js +1 -0
- package/dist/adapters/BunRuntimeAdapter.js.map +1 -1
- package/dist/adapters/DockerRuntimeAdapter.d.ts +2 -1
- package/dist/adapters/DockerRuntimeAdapter.js +10 -1
- package/dist/adapters/DockerRuntimeAdapter.js.map +1 -1
- package/dist/adapters/HttpRuntimeAdapter.d.ts +26 -5
- package/dist/adapters/HttpRuntimeAdapter.js +97 -16
- package/dist/adapters/HttpRuntimeAdapter.js.map +1 -1
- package/dist/adapters/NodeJsRuntimeAdapter.d.ts +1 -0
- package/dist/adapters/NodeJsRuntimeAdapter.js +1 -0
- package/dist/adapters/NodeJsRuntimeAdapter.js.map +1 -1
- package/dist/adapters/RuntimeAdapter.d.ts +17 -0
- package/dist/adapters/WasmRuntimeAdapter.d.ts +1 -0
- package/dist/adapters/WasmRuntimeAdapter.js +1 -0
- package/dist/adapters/WasmRuntimeAdapter.js.map +1 -1
- package/dist/adapters/grpc/GrpcChannelOptions.d.ts +31 -0
- package/dist/adapters/grpc/GrpcChannelOptions.js +68 -0
- package/dist/adapters/grpc/GrpcChannelOptions.js.map +1 -0
- package/dist/adapters/grpc/GrpcClientPool.d.ts +43 -0
- package/dist/adapters/grpc/GrpcClientPool.js +89 -0
- package/dist/adapters/grpc/GrpcClientPool.js.map +1 -0
- package/dist/adapters/grpc/GrpcCodec.d.ts +226 -0
- package/dist/adapters/grpc/GrpcCodec.js +275 -0
- package/dist/adapters/grpc/GrpcCodec.js.map +1 -0
- package/dist/adapters/grpc/GrpcErrors.d.ts +59 -0
- package/dist/adapters/grpc/GrpcErrors.js +190 -0
- package/dist/adapters/grpc/GrpcErrors.js.map +1 -0
- package/dist/adapters/grpc/GrpcHealthChecker.d.ts +69 -0
- package/dist/adapters/grpc/GrpcHealthChecker.js +96 -0
- package/dist/adapters/grpc/GrpcHealthChecker.js.map +1 -0
- package/dist/adapters/grpc/GrpcRuntimeAdapter.d.ts +98 -0
- package/dist/adapters/grpc/GrpcRuntimeAdapter.js +478 -0
- package/dist/adapters/grpc/GrpcRuntimeAdapter.js.map +1 -0
- package/dist/adapters/grpc/index.d.ts +13 -0
- package/dist/adapters/grpc/index.js +14 -0
- package/dist/adapters/grpc/index.js.map +1 -0
- package/dist/adapters/grpc/proto/blok/runtime/v1/runtime.proto +302 -0
- package/dist/adapters/grpc/types.d.ts +97 -0
- package/dist/adapters/grpc/types.js +41 -0
- package/dist/adapters/grpc/types.js.map +1 -0
- package/dist/adapters/transport.d.ts +108 -0
- package/dist/adapters/transport.js +196 -0
- package/dist/adapters/transport.js.map +1 -0
- package/dist/concurrency/ConcurrencyBackend.d.ts +61 -0
- package/dist/concurrency/ConcurrencyBackend.js +20 -0
- package/dist/concurrency/ConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/ConcurrencyLimitError.d.ts +37 -0
- package/dist/concurrency/ConcurrencyLimitError.js +16 -0
- package/dist/concurrency/ConcurrencyLimitError.js.map +1 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.d.ts +64 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.js +297 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/QueueExpiredError.d.ts +40 -0
- package/dist/concurrency/QueueExpiredError.js +15 -0
- package/dist/concurrency/QueueExpiredError.js.map +1 -0
- package/dist/concurrency/createConcurrencyBackend.d.ts +23 -0
- package/dist/concurrency/createConcurrencyBackend.js +34 -0
- package/dist/concurrency/createConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/readConcurrencyConfig.d.ts +60 -0
- package/dist/concurrency/readConcurrencyConfig.js +60 -0
- package/dist/concurrency/readConcurrencyConfig.js.map +1 -0
- package/dist/idempotency/resolveIdempotencyKey.d.ts +20 -0
- package/dist/idempotency/resolveIdempotencyKey.js +37 -0
- package/dist/idempotency/resolveIdempotencyKey.js.map +1 -0
- package/dist/index.d.ts +35 -3
- package/dist/index.js +61 -2
- package/dist/index.js.map +1 -1
- package/dist/monitoring/ConcurrencyMetrics.d.ts +56 -0
- package/dist/monitoring/ConcurrencyMetrics.js +107 -0
- package/dist/monitoring/ConcurrencyMetrics.js.map +1 -0
- package/dist/monitoring/JanitorMetrics.d.ts +27 -0
- package/dist/monitoring/JanitorMetrics.js +48 -0
- package/dist/monitoring/JanitorMetrics.js.map +1 -0
- package/dist/scheduling/DebounceCoordinator.d.ts +88 -0
- package/dist/scheduling/DebounceCoordinator.js +141 -0
- package/dist/scheduling/DebounceCoordinator.js.map +1 -0
- package/dist/scheduling/DeferredDispatchSignal.d.ts +50 -0
- package/dist/scheduling/DeferredDispatchSignal.js +14 -0
- package/dist/scheduling/DeferredDispatchSignal.js.map +1 -0
- package/dist/scheduling/DeferredRunScheduler.d.ts +68 -0
- package/dist/scheduling/DeferredRunScheduler.js +154 -0
- package/dist/scheduling/DeferredRunScheduler.js.map +1 -0
- package/dist/scheduling/readSchedulingConfig.d.ts +24 -0
- package/dist/scheduling/readSchedulingConfig.js +52 -0
- package/dist/scheduling/readSchedulingConfig.js.map +1 -0
- package/dist/testing/WorkflowTestRunner.js +12 -0
- package/dist/testing/WorkflowTestRunner.js.map +1 -1
- package/dist/timeouts/StepTimeoutError.d.ts +22 -0
- package/dist/timeouts/StepTimeoutError.js +31 -0
- package/dist/timeouts/StepTimeoutError.js.map +1 -0
- package/dist/tracing/InMemoryRunStore.d.ts +28 -1
- package/dist/tracing/InMemoryRunStore.js +150 -0
- package/dist/tracing/InMemoryRunStore.js.map +1 -1
- package/dist/tracing/Janitor.d.ts +70 -0
- package/dist/tracing/Janitor.js +150 -0
- package/dist/tracing/Janitor.js.map +1 -0
- package/dist/tracing/PostgresRunStore.d.ts +30 -0
- package/dist/tracing/PostgresRunStore.js +435 -3
- package/dist/tracing/PostgresRunStore.js.map +1 -1
- package/dist/tracing/RunStore.d.ts +100 -1
- package/dist/tracing/RunTracker.d.ts +261 -11
- package/dist/tracing/RunTracker.js +691 -11
- package/dist/tracing/RunTracker.js.map +1 -1
- package/dist/tracing/SqliteRunStore.d.ts +23 -1
- package/dist/tracing/SqliteRunStore.js +421 -6
- package/dist/tracing/SqliteRunStore.js.map +1 -1
- package/dist/tracing/TraceRouter.d.ts +20 -2
- package/dist/tracing/TraceRouter.js +494 -9
- package/dist/tracing/TraceRouter.js.map +1 -1
- package/dist/tracing/sanitize.d.ts +11 -0
- package/dist/tracing/sanitize.js +29 -0
- package/dist/tracing/sanitize.js.map +1 -1
- package/dist/tracing/types.d.ts +429 -11
- package/dist/types/GlobalOptions.d.ts +9 -2
- package/dist/utils/createChildContext.d.ts +32 -0
- package/dist/utils/createChildContext.js +113 -0
- package/dist/utils/createChildContext.js.map +1 -0
- package/dist/workflow/PersistenceHelper.d.ts +46 -0
- package/dist/workflow/PersistenceHelper.js +57 -0
- package/dist/workflow/PersistenceHelper.js.map +1 -0
- package/dist/workflow/WorkflowNormalizer.d.ts +79 -0
- package/dist/workflow/WorkflowNormalizer.js +486 -0
- package/dist/workflow/WorkflowNormalizer.js.map +1 -0
- package/dist/workflow/WorkflowRegistry.d.ts +64 -0
- package/dist/workflow/WorkflowRegistry.js +81 -0
- package/dist/workflow/WorkflowRegistry.js.map +1 -0
- package/package.json +10 -7
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Context } from "@blokjs/shared";
|
|
2
|
+
/**
|
|
3
|
+
* The shape of a step instance the helper needs to make a persistence
|
|
4
|
+
* decision. Both `BlokService` and `RuntimeAdapterNode` satisfy this.
|
|
5
|
+
*
|
|
6
|
+
* Read-only — the helper never mutates the step.
|
|
7
|
+
*/
|
|
8
|
+
export interface PersistableStep {
|
|
9
|
+
/** Step's stable identifier (today's `name` field on NodeBase). */
|
|
10
|
+
readonly name: string;
|
|
11
|
+
/** Optional alias — store at `state[as]` instead of `state[name]`. */
|
|
12
|
+
readonly as?: string;
|
|
13
|
+
/** When true, shallow-merge result.data keys into state. */
|
|
14
|
+
readonly spread?: boolean;
|
|
15
|
+
/** When true, skip persistence entirely. */
|
|
16
|
+
readonly ephemeral?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Legacy v1 flag. `false` is interpreted as `ephemeral: true`. `true`
|
|
19
|
+
* is a no-op (the new default already persists).
|
|
20
|
+
*/
|
|
21
|
+
readonly set_var?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The shape of an execution result the helper needs to read. Both
|
|
25
|
+
* `ResponseContext` and `ExecutionResult` carry `.data`.
|
|
26
|
+
*/
|
|
27
|
+
export interface PersistableResult {
|
|
28
|
+
readonly data?: unknown;
|
|
29
|
+
readonly success?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Apply a step's output to `ctx.state` according to the step's persistence
|
|
33
|
+
* knobs.
|
|
34
|
+
*
|
|
35
|
+
* **Rules** (in order):
|
|
36
|
+
* 1. `ephemeral: true` → no-op. Output is only available via `ctx.prev`.
|
|
37
|
+
* 2. Legacy `set_var: false` → treated as `ephemeral: true` (back-compat).
|
|
38
|
+
* 3. `spread: true` AND `data` is a plain object → shallow-merge data's
|
|
39
|
+
* top-level keys into `ctx.state`.
|
|
40
|
+
* 4. Default → `ctx.state[as ?? name] = data`.
|
|
41
|
+
*
|
|
42
|
+
* Mutates `ctx.state` in place. Always safe to call (no-op on missing data
|
|
43
|
+
* unless `spread` is set, which is an authoring-time error to combine with
|
|
44
|
+
* non-object output and is detected at workflow load time, not here).
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyStepOutput(ctx: Context, step: PersistableStep, result: PersistableResult): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply a step's output to `ctx.state` according to the step's persistence
|
|
3
|
+
* knobs.
|
|
4
|
+
*
|
|
5
|
+
* **Rules** (in order):
|
|
6
|
+
* 1. `ephemeral: true` → no-op. Output is only available via `ctx.prev`.
|
|
7
|
+
* 2. Legacy `set_var: false` → treated as `ephemeral: true` (back-compat).
|
|
8
|
+
* 3. `spread: true` AND `data` is a plain object → shallow-merge data's
|
|
9
|
+
* top-level keys into `ctx.state`.
|
|
10
|
+
* 4. Default → `ctx.state[as ?? name] = data`.
|
|
11
|
+
*
|
|
12
|
+
* Mutates `ctx.state` in place. Always safe to call (no-op on missing data
|
|
13
|
+
* unless `spread` is set, which is an authoring-time error to combine with
|
|
14
|
+
* non-object output and is detected at workflow load time, not here).
|
|
15
|
+
*/
|
|
16
|
+
export function applyStepOutput(ctx, step, result) {
|
|
17
|
+
// Rule 1 & 2 — opt-out paths
|
|
18
|
+
if (step.ephemeral === true)
|
|
19
|
+
return;
|
|
20
|
+
if (step.set_var === false)
|
|
21
|
+
return;
|
|
22
|
+
// Defensive: ensure state exists (TriggerBase initializes it, but
|
|
23
|
+
// some legacy code paths construct ctx by hand).
|
|
24
|
+
if (!ctx.state || typeof ctx.state !== "object") {
|
|
25
|
+
ctx.state = {};
|
|
26
|
+
}
|
|
27
|
+
const state = ctx.state;
|
|
28
|
+
const data = result?.data;
|
|
29
|
+
// Rule 3 — spread
|
|
30
|
+
if (step.spread === true) {
|
|
31
|
+
if (isPlainObject(data)) {
|
|
32
|
+
Object.assign(state, data);
|
|
33
|
+
}
|
|
34
|
+
// Non-object data with `spread: true` is silently ignored at
|
|
35
|
+
// runtime. The workflow normalizer warns at load time so the
|
|
36
|
+
// author is aware.
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Rule 4 — default-store
|
|
40
|
+
if (data === undefined)
|
|
41
|
+
return;
|
|
42
|
+
const key = step.as ?? step.name;
|
|
43
|
+
if (!key)
|
|
44
|
+
return; // defensive: anonymous step (shouldn't happen post-normalizer)
|
|
45
|
+
state[key] = data;
|
|
46
|
+
}
|
|
47
|
+
function isPlainObject(value) {
|
|
48
|
+
if (value === null || value === undefined)
|
|
49
|
+
return false;
|
|
50
|
+
if (typeof value !== "object")
|
|
51
|
+
return false;
|
|
52
|
+
if (Array.isArray(value))
|
|
53
|
+
return false;
|
|
54
|
+
const proto = Object.getPrototypeOf(value);
|
|
55
|
+
return proto === null || proto === Object.prototype;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=PersistenceHelper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PersistenceHelper.js","sourceRoot":"","sources":["../../src/workflow/PersistenceHelper.ts"],"names":[],"mappings":"AAiCA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY,EAAE,IAAqB,EAAE,MAAyB;IAC7F,6BAA6B;IAC7B,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI;QAAE,OAAO;IACpC,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK;QAAE,OAAO;IAEnC,kEAAkE;IAClE,iDAAiD;IACjD,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,GAA0C,CAAC,KAAK,GAAG,EAAE,CAAC;IACxD,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,KAAgC,CAAC;IACnD,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,CAAC;IAE1B,kBAAkB;IAClB,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;QAC1B,IAAI,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC5B,CAAC;QACD,6DAA6D;QAC7D,6DAA6D;QAC7D,mBAAmB;QACnB,OAAO;IACR,CAAC;IAED,yBAAyB;IACzB,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC;IACjC,IAAI,CAAC,GAAG;QAAE,OAAO,CAAC,+DAA+D;IACjF,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;AACnB,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACpC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACxD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC3C,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC,SAAS,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
interface RetryConfig {
|
|
2
|
+
maxAttempts: number;
|
|
3
|
+
minTimeoutInMs?: number;
|
|
4
|
+
maxTimeoutInMs?: number;
|
|
5
|
+
factor?: number;
|
|
6
|
+
}
|
|
7
|
+
interface InternalStep {
|
|
8
|
+
name: string;
|
|
9
|
+
node: string;
|
|
10
|
+
type: string;
|
|
11
|
+
active?: boolean;
|
|
12
|
+
stop?: boolean;
|
|
13
|
+
set_var?: boolean;
|
|
14
|
+
as?: string;
|
|
15
|
+
spread?: boolean;
|
|
16
|
+
ephemeral?: boolean;
|
|
17
|
+
stream_logs?: boolean;
|
|
18
|
+
flow?: boolean;
|
|
19
|
+
idempotencyKey?: string;
|
|
20
|
+
idempotencyKeyTTL?: number;
|
|
21
|
+
retry?: RetryConfig;
|
|
22
|
+
subworkflow?: string;
|
|
23
|
+
wait?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Tier 2 quick-wins — per-attempt execution timeout. Number (ms) or
|
|
26
|
+
* duration string (`"30s"`, etc.). Configuration thread-through
|
|
27
|
+
* normalizes to milliseconds via `parseDuration`.
|
|
28
|
+
*/
|
|
29
|
+
maxDuration?: number | string;
|
|
30
|
+
/**
|
|
31
|
+
* PR 4 — `wait.for(duration)` / `wait.until(date)` step.
|
|
32
|
+
*
|
|
33
|
+
* Discriminates by `type === "wait"` and the presence of either
|
|
34
|
+
* `waitForMs` (numeric ms after parseDuration) or `waitUntil` (number
|
|
35
|
+
* ms-since-epoch OR string for $-proxy / ISO).
|
|
36
|
+
*
|
|
37
|
+
* `wait?: boolean` above is the sub-workflow `wait: true|false` flag
|
|
38
|
+
* — separate concern, separate field.
|
|
39
|
+
*/
|
|
40
|
+
waitForMs?: number;
|
|
41
|
+
waitUntil?: number | string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
interface InternalNodeConfig {
|
|
45
|
+
inputs?: Record<string, unknown>;
|
|
46
|
+
conditions?: InternalCondition[];
|
|
47
|
+
steps?: InternalStep[];
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
interface InternalCondition {
|
|
51
|
+
type: "if" | "else";
|
|
52
|
+
condition?: string;
|
|
53
|
+
steps: InternalStep[];
|
|
54
|
+
}
|
|
55
|
+
export interface InternalWorkflow {
|
|
56
|
+
name: string;
|
|
57
|
+
version: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
trigger: Record<string, unknown>;
|
|
60
|
+
steps: InternalStep[];
|
|
61
|
+
nodes: Record<string, InternalNodeConfig>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Convert any accepted workflow shape into the canonical internal shape.
|
|
65
|
+
*
|
|
66
|
+
* Mutates a deep copy — the caller's object is never modified.
|
|
67
|
+
*
|
|
68
|
+
* @param raw - parsed workflow object (from JSON.parse, dynamic import,
|
|
69
|
+
* or the v1 builder pipeline)
|
|
70
|
+
* @param sourcePath - optional path used in deprecation warnings
|
|
71
|
+
*/
|
|
72
|
+
export declare function normalizeWorkflow(raw: unknown, sourcePath?: string): InternalWorkflow;
|
|
73
|
+
/**
|
|
74
|
+
* Test-only — reset the per-process wildcard warning cache.
|
|
75
|
+
*
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
export declare function _resetWildcardWarningCache(): void;
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { parseDuration } from "@blokjs/helper";
|
|
2
|
+
/**
|
|
3
|
+
* WorkflowNormalizer — accepts v1 or v2 workflow shapes and projects both
|
|
4
|
+
* to the single canonical internal shape that `Configuration.getSteps` /
|
|
5
|
+
* `Configuration.getNodes` already consume.
|
|
6
|
+
*
|
|
7
|
+
* **Input shapes accepted**
|
|
8
|
+
*
|
|
9
|
+
* v1 (legacy):
|
|
10
|
+
* ```
|
|
11
|
+
* {
|
|
12
|
+
* name, version, trigger,
|
|
13
|
+
* steps: [{ name, node, type, active?, stop?, set_var? }],
|
|
14
|
+
* nodes: { [stepName]: { inputs?, conditions? } }
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* v2 (canonical):
|
|
19
|
+
* ```
|
|
20
|
+
* {
|
|
21
|
+
* name, version, trigger,
|
|
22
|
+
* steps: [
|
|
23
|
+
* { id, use, type?, inputs?, as?, spread?, ephemeral?, active?, stop? }
|
|
24
|
+
* | { id, branch: { when, then, else? } }
|
|
25
|
+
* ]
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* **Output shape** (always v1-compatible internal):
|
|
30
|
+
* ```
|
|
31
|
+
* {
|
|
32
|
+
* name, version, trigger, // method "*" normalized to "ANY"
|
|
33
|
+
* steps: [{ name, node, type, active, stop, set_var, as, spread, ephemeral, ... }],
|
|
34
|
+
* nodes: { [stepName]: { inputs?, conditions? } }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* **Why a single internal shape?** The runner core (RunnerSteps,
|
|
39
|
+
* Configuration, Blok.run, etc.) is unchanged. Normalization is purely
|
|
40
|
+
* an authoring-layer concern. Old workflows keep running; new authoring
|
|
41
|
+
* shapes get translated transparently.
|
|
42
|
+
*/
|
|
43
|
+
const IF_ELSE_NODE_REF = "@blokjs/if-else";
|
|
44
|
+
let _wildcardWarnedFiles = new Set();
|
|
45
|
+
/**
|
|
46
|
+
* Convert any accepted workflow shape into the canonical internal shape.
|
|
47
|
+
*
|
|
48
|
+
* Mutates a deep copy — the caller's object is never modified.
|
|
49
|
+
*
|
|
50
|
+
* @param raw - parsed workflow object (from JSON.parse, dynamic import,
|
|
51
|
+
* or the v1 builder pipeline)
|
|
52
|
+
* @param sourcePath - optional path used in deprecation warnings
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeWorkflow(raw, sourcePath) {
|
|
55
|
+
if (!isPlainObject(raw)) {
|
|
56
|
+
const suffix = sourcePath ? ` (file: ${sourcePath})` : "";
|
|
57
|
+
throw new Error(`[blok] WorkflowNormalizer: expected an object, got ${typeof raw}${suffix}`);
|
|
58
|
+
}
|
|
59
|
+
// Unwrap v2 builder envelopes — `workflow()` returns `{_blokV2: true, _config: {...}}`.
|
|
60
|
+
// The legacy `Workflow()` builder also produces a `_config` field; both shapes
|
|
61
|
+
// carry their workflow definition under that key.
|
|
62
|
+
let wf = raw;
|
|
63
|
+
if (wf._blokV2 === true && isPlainObject(wf._config)) {
|
|
64
|
+
wf = wf._config;
|
|
65
|
+
}
|
|
66
|
+
else if (isPlainObject(wf._config) && wf.name === undefined && wf.steps === undefined) {
|
|
67
|
+
// Legacy builder shape — same unwrap.
|
|
68
|
+
wf = wf._config;
|
|
69
|
+
}
|
|
70
|
+
const name = typeof wf.name === "string" ? wf.name : "";
|
|
71
|
+
const version = typeof wf.version === "string" ? wf.version : "1.0.0";
|
|
72
|
+
const description = typeof wf.description === "string" ? wf.description : undefined;
|
|
73
|
+
// --- Trigger normalization (method "*" → "ANY") ---
|
|
74
|
+
const trigger = normalizeTrigger(wf.trigger, sourcePath);
|
|
75
|
+
// --- Steps normalization ---
|
|
76
|
+
const stepsInput = Array.isArray(wf.steps) ? wf.steps : [];
|
|
77
|
+
const nodesInput = isPlainObject(wf.nodes) ? wf.nodes : {};
|
|
78
|
+
const internalSteps = [];
|
|
79
|
+
const internalNodes = {};
|
|
80
|
+
for (let i = 0; i < stepsInput.length; i++) {
|
|
81
|
+
const rawStep = stepsInput[i];
|
|
82
|
+
if (!isPlainObject(rawStep))
|
|
83
|
+
continue;
|
|
84
|
+
const step = rawStep;
|
|
85
|
+
// v2 branch — { id, branch: { when, then, else? } }
|
|
86
|
+
if (isPlainObject(step.branch)) {
|
|
87
|
+
const { internalStep, nodeConfig } = normalizeBranchStep(step, i);
|
|
88
|
+
internalSteps.push(internalStep);
|
|
89
|
+
internalNodes[internalStep.name] = nodeConfig;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// v2 sub-workflow — { id, subworkflow: "<name>", inputs?, wait? }
|
|
93
|
+
// Discriminator is the presence of a non-empty `subworkflow` string.
|
|
94
|
+
// Resolves to a SubworkflowNode that looks up the child in the
|
|
95
|
+
// WorkflowRegistry at run time.
|
|
96
|
+
if (typeof step.subworkflow === "string" && step.subworkflow.length > 0) {
|
|
97
|
+
const { internalStep, nodeConfig } = normalizeSubworkflowStep(step, i);
|
|
98
|
+
internalSteps.push(internalStep);
|
|
99
|
+
if (nodeConfig)
|
|
100
|
+
internalNodes[internalStep.name] = nodeConfig;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// v2 wait — { id, wait: { for?, until? } } (PR 4).
|
|
104
|
+
// Discriminator: `wait` is an object (sub-workflow uses `wait: boolean`).
|
|
105
|
+
if (isPlainObject(step.wait) &&
|
|
106
|
+
(step.wait.for !== undefined || step.wait.until !== undefined)) {
|
|
107
|
+
const internalStep = normalizeWaitStep(step, i);
|
|
108
|
+
internalSteps.push(internalStep);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// v2 regular — { id, use, inputs?, as?, spread?, ephemeral?, ... }
|
|
112
|
+
// or v1 regular — { name, node, type } + nodes[name].inputs
|
|
113
|
+
const { internalStep, nodeConfig } = normalizeRegularStep(step, nodesInput, i);
|
|
114
|
+
internalSteps.push(internalStep);
|
|
115
|
+
if (nodeConfig)
|
|
116
|
+
internalNodes[internalStep.name] = nodeConfig;
|
|
117
|
+
}
|
|
118
|
+
// --- Carry over any v1 nodes that didn't have a matching step ---
|
|
119
|
+
// (rare, but possible for legacy workflows with helper sub-flows
|
|
120
|
+
// declared at the top level)
|
|
121
|
+
for (const key of Object.keys(nodesInput)) {
|
|
122
|
+
if (internalNodes[key] !== undefined)
|
|
123
|
+
continue;
|
|
124
|
+
const value = nodesInput[key];
|
|
125
|
+
if (isPlainObject(value)) {
|
|
126
|
+
internalNodes[key] = value;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
name,
|
|
131
|
+
version,
|
|
132
|
+
description,
|
|
133
|
+
trigger,
|
|
134
|
+
steps: internalSteps,
|
|
135
|
+
nodes: internalNodes,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// Internals
|
|
140
|
+
// =============================================================================
|
|
141
|
+
function normalizeRegularStep(step, nodesInput, index) {
|
|
142
|
+
// Identity — `id` (v2) wins, fallback to `name` (v1).
|
|
143
|
+
const id = pickString(step.id) ?? pickString(step.name);
|
|
144
|
+
if (!id) {
|
|
145
|
+
throw new Error(`[blok] WorkflowNormalizer: step at index ${index} has neither \`id\` (v2) nor \`name\` (v1).`);
|
|
146
|
+
}
|
|
147
|
+
// Node reference — `use` (v2) wins, fallback to `node` (v1).
|
|
148
|
+
const nodeRef = pickString(step.use) ?? pickString(step.node);
|
|
149
|
+
if (!nodeRef) {
|
|
150
|
+
throw new Error(`[blok] WorkflowNormalizer: step "${id}" has neither \`use\` (v2) nor \`node\` (v1).`);
|
|
151
|
+
}
|
|
152
|
+
// Type — explicit `type` wins; otherwise inferred from the node ref.
|
|
153
|
+
const explicitType = pickString(step.type);
|
|
154
|
+
const type = explicitType ?? inferStepType(nodeRef);
|
|
155
|
+
// Inputs — v2 inlines on the step; v1 lives at workflow.nodes[name].inputs.
|
|
156
|
+
const inlineInputs = isPlainObject(step.inputs) ? step.inputs : null;
|
|
157
|
+
const v1NodeConfig = isPlainObject(nodesInput[id]) ? nodesInput[id] : null;
|
|
158
|
+
const v1Inputs = v1NodeConfig?.inputs && isPlainObject(v1NodeConfig.inputs) ? v1NodeConfig.inputs : null;
|
|
159
|
+
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;
|
|
164
|
+
const as = pickString(step.as);
|
|
165
|
+
const spread = step.spread === true;
|
|
166
|
+
// `as` and `spread` are mutually exclusive — caught at schema level too,
|
|
167
|
+
// repeated here so JSON workflows that bypass Zod still fail loudly.
|
|
168
|
+
if (as && spread) {
|
|
169
|
+
throw new Error(`[blok] WorkflowNormalizer: step "${id}" sets both \`as\` and \`spread\` — they are mutually exclusive.`);
|
|
170
|
+
}
|
|
171
|
+
const internalStep = {
|
|
172
|
+
name: id,
|
|
173
|
+
node: nodeRef,
|
|
174
|
+
type,
|
|
175
|
+
active: step.active === undefined ? true : Boolean(step.active),
|
|
176
|
+
stop: step.stop === true,
|
|
177
|
+
set_var: typeof step.set_var === "boolean" ? step.set_var : undefined,
|
|
178
|
+
as,
|
|
179
|
+
spread,
|
|
180
|
+
ephemeral,
|
|
181
|
+
};
|
|
182
|
+
if (typeof step.stream_logs === "boolean")
|
|
183
|
+
internalStep.stream_logs = step.stream_logs;
|
|
184
|
+
// Idempotency cache + retry — pass through verbatim. The runner reads
|
|
185
|
+
// these in RunnerSteps to wrap step.process() with cache-check + retry-
|
|
186
|
+
// loop. They never reach PersistenceHelper.applyStepOutput; caching
|
|
187
|
+
// layers ABOVE that.
|
|
188
|
+
if (typeof step.idempotencyKey === "string" && step.idempotencyKey.length > 0) {
|
|
189
|
+
internalStep.idempotencyKey = step.idempotencyKey;
|
|
190
|
+
}
|
|
191
|
+
if (typeof step.idempotencyKeyTTL === "number" && Number.isFinite(step.idempotencyKeyTTL)) {
|
|
192
|
+
internalStep.idempotencyKeyTTL = step.idempotencyKeyTTL;
|
|
193
|
+
}
|
|
194
|
+
if (isPlainObject(step.retry)) {
|
|
195
|
+
const r = step.retry;
|
|
196
|
+
if (typeof r.maxAttempts === "number" && Number.isInteger(r.maxAttempts)) {
|
|
197
|
+
const retry = { maxAttempts: r.maxAttempts };
|
|
198
|
+
if (typeof r.minTimeoutInMs === "number")
|
|
199
|
+
retry.minTimeoutInMs = r.minTimeoutInMs;
|
|
200
|
+
if (typeof r.maxTimeoutInMs === "number")
|
|
201
|
+
retry.maxTimeoutInMs = r.maxTimeoutInMs;
|
|
202
|
+
if (typeof r.factor === "number")
|
|
203
|
+
retry.factor = r.factor;
|
|
204
|
+
internalStep.retry = retry;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (typeof step.maxDuration === "number" || typeof step.maxDuration === "string") {
|
|
208
|
+
internalStep.maxDuration = step.maxDuration;
|
|
209
|
+
}
|
|
210
|
+
// Build node config — only include `inputs` if present.
|
|
211
|
+
let nodeConfig = null;
|
|
212
|
+
if (inputs) {
|
|
213
|
+
nodeConfig = { inputs };
|
|
214
|
+
// Carry over any legacy v1 node-config fields that aren't `inputs`
|
|
215
|
+
// (some workflows attach `outputs`, `mapper`, etc.).
|
|
216
|
+
if (v1NodeConfig) {
|
|
217
|
+
for (const k of Object.keys(v1NodeConfig)) {
|
|
218
|
+
if (k === "inputs")
|
|
219
|
+
continue;
|
|
220
|
+
nodeConfig[k] = v1NodeConfig[k];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (v1NodeConfig) {
|
|
225
|
+
nodeConfig = { ...v1NodeConfig };
|
|
226
|
+
}
|
|
227
|
+
return { internalStep, nodeConfig };
|
|
228
|
+
}
|
|
229
|
+
function normalizeBranchStep(step, index) {
|
|
230
|
+
const id = pickString(step.id);
|
|
231
|
+
if (!id) {
|
|
232
|
+
throw new Error(`[blok] WorkflowNormalizer: branch step at index ${index} is missing \`id\`.`);
|
|
233
|
+
}
|
|
234
|
+
const branch = step.branch;
|
|
235
|
+
const when = pickString(branch.when);
|
|
236
|
+
if (!when) {
|
|
237
|
+
throw new Error(`[blok] WorkflowNormalizer: branch step "${id}" is missing \`when\` (must be a non-empty string).`);
|
|
238
|
+
}
|
|
239
|
+
const thenSteps = Array.isArray(branch.then) ? branch.then : [];
|
|
240
|
+
const elseSteps = Array.isArray(branch.else) ? branch.else : [];
|
|
241
|
+
// Normalize each branch's nested steps recursively. Use empty
|
|
242
|
+
// `nodesInput` because v2 branches inline `inputs` on each nested step.
|
|
243
|
+
const thenInternal = [];
|
|
244
|
+
const elseInternal = [];
|
|
245
|
+
for (let i = 0; i < thenSteps.length; i++) {
|
|
246
|
+
const s = thenSteps[i];
|
|
247
|
+
if (!isPlainObject(s))
|
|
248
|
+
continue;
|
|
249
|
+
if (isPlainObject(s.branch)) {
|
|
250
|
+
const { internalStep } = normalizeBranchStep(s, i);
|
|
251
|
+
thenInternal.push(internalStep);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const { internalStep, nodeConfig } = normalizeRegularStep(s, {}, i);
|
|
255
|
+
// Inline inputs on the step itself so nested-flow execution finds them.
|
|
256
|
+
// (RunnerSteps recursively executes flow nodes' inner steps.)
|
|
257
|
+
if (nodeConfig?.inputs) {
|
|
258
|
+
internalStep.inputs = nodeConfig.inputs;
|
|
259
|
+
}
|
|
260
|
+
thenInternal.push(internalStep);
|
|
261
|
+
}
|
|
262
|
+
for (let i = 0; i < elseSteps.length; i++) {
|
|
263
|
+
const s = elseSteps[i];
|
|
264
|
+
if (!isPlainObject(s))
|
|
265
|
+
continue;
|
|
266
|
+
if (isPlainObject(s.branch)) {
|
|
267
|
+
const { internalStep } = normalizeBranchStep(s, i);
|
|
268
|
+
elseInternal.push(internalStep);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const { internalStep, nodeConfig } = normalizeRegularStep(s, {}, i);
|
|
272
|
+
if (nodeConfig?.inputs) {
|
|
273
|
+
internalStep.inputs = nodeConfig.inputs;
|
|
274
|
+
}
|
|
275
|
+
elseInternal.push(internalStep);
|
|
276
|
+
}
|
|
277
|
+
const conditions = [{ type: "if", condition: when, steps: thenInternal }];
|
|
278
|
+
if (elseInternal.length > 0) {
|
|
279
|
+
conditions.push({ type: "else", steps: elseInternal });
|
|
280
|
+
}
|
|
281
|
+
const internalStep = {
|
|
282
|
+
name: id,
|
|
283
|
+
node: IF_ELSE_NODE_REF,
|
|
284
|
+
type: "module",
|
|
285
|
+
active: step.active === undefined ? true : Boolean(step.active),
|
|
286
|
+
stop: step.stop === true,
|
|
287
|
+
flow: true,
|
|
288
|
+
};
|
|
289
|
+
const nodeConfig = { conditions };
|
|
290
|
+
return { internalStep, nodeConfig };
|
|
291
|
+
}
|
|
292
|
+
const SUBWORKFLOW_NODE_REF = "@blokjs/subworkflow";
|
|
293
|
+
/**
|
|
294
|
+
* Normalize a v2 sub-workflow step into the canonical InternalStep
|
|
295
|
+
* shape. Resolves to a `SubworkflowNode` at run time
|
|
296
|
+
* (Configuration.nodeTypes.subworkflow).
|
|
297
|
+
*
|
|
298
|
+
* Inputs are placed on `nodeConfig.inputs` so the existing
|
|
299
|
+
* blueprint-mapper resolution path resolves `$.state.<id>` /
|
|
300
|
+
* `$.req.body.<key>` refs into concrete values BEFORE the
|
|
301
|
+
* sub-workflow node runs (mirrors how regular steps work).
|
|
302
|
+
*/
|
|
303
|
+
function normalizeSubworkflowStep(step, index) {
|
|
304
|
+
const id = pickString(step.id);
|
|
305
|
+
if (!id) {
|
|
306
|
+
throw new Error(`[blok] WorkflowNormalizer: sub-workflow step at index ${index} is missing \`id\`.`);
|
|
307
|
+
}
|
|
308
|
+
const subworkflow = pickString(step.subworkflow);
|
|
309
|
+
if (!subworkflow) {
|
|
310
|
+
throw new Error(`[blok] WorkflowNormalizer: sub-workflow step "${id}" is missing \`subworkflow\` (workflow name to invoke).`);
|
|
311
|
+
}
|
|
312
|
+
// `wait: false` (fire-and-forget) is now supported. The async dispatch
|
|
313
|
+
// branch lives in SubworkflowNode.run; the field is threaded through
|
|
314
|
+
// onto InternalStep below for the resolver to copy onto the
|
|
315
|
+
// SubworkflowNode instance.
|
|
316
|
+
// Persistence + retry + idempotency knobs — pass through verbatim
|
|
317
|
+
// (mirrors normalizeRegularStep). `as` and `spread` mutual exclusion
|
|
318
|
+
// is also enforced at the schema level; defensive check here.
|
|
319
|
+
const ephemeral = step.ephemeral === true;
|
|
320
|
+
const as = pickString(step.as);
|
|
321
|
+
const spread = step.spread === true;
|
|
322
|
+
if (as && spread) {
|
|
323
|
+
throw new Error(`[blok] WorkflowNormalizer: sub-workflow step "${id}" sets both \`as\` and \`spread\` — they are mutually exclusive.`);
|
|
324
|
+
}
|
|
325
|
+
const internalStep = {
|
|
326
|
+
name: id,
|
|
327
|
+
node: SUBWORKFLOW_NODE_REF,
|
|
328
|
+
type: "subworkflow",
|
|
329
|
+
active: step.active === undefined ? true : Boolean(step.active),
|
|
330
|
+
stop: step.stop === true,
|
|
331
|
+
as,
|
|
332
|
+
spread,
|
|
333
|
+
ephemeral,
|
|
334
|
+
subworkflow,
|
|
335
|
+
// Default `wait: true` when omitted. `wait: false` triggers the
|
|
336
|
+
// async dispatch branch in SubworkflowNode.run.
|
|
337
|
+
wait: step.wait === undefined ? true : Boolean(step.wait),
|
|
338
|
+
};
|
|
339
|
+
if (typeof step.idempotencyKey === "string" && step.idempotencyKey.length > 0) {
|
|
340
|
+
internalStep.idempotencyKey = step.idempotencyKey;
|
|
341
|
+
}
|
|
342
|
+
if (typeof step.idempotencyKeyTTL === "number" && Number.isFinite(step.idempotencyKeyTTL)) {
|
|
343
|
+
internalStep.idempotencyKeyTTL = step.idempotencyKeyTTL;
|
|
344
|
+
}
|
|
345
|
+
if (isPlainObject(step.retry)) {
|
|
346
|
+
const r = step.retry;
|
|
347
|
+
if (typeof r.maxAttempts === "number" && Number.isInteger(r.maxAttempts)) {
|
|
348
|
+
const retry = { maxAttempts: r.maxAttempts };
|
|
349
|
+
if (typeof r.minTimeoutInMs === "number")
|
|
350
|
+
retry.minTimeoutInMs = r.minTimeoutInMs;
|
|
351
|
+
if (typeof r.maxTimeoutInMs === "number")
|
|
352
|
+
retry.maxTimeoutInMs = r.maxTimeoutInMs;
|
|
353
|
+
if (typeof r.factor === "number")
|
|
354
|
+
retry.factor = r.factor;
|
|
355
|
+
internalStep.retry = retry;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (typeof step.maxDuration === "number" || typeof step.maxDuration === "string") {
|
|
359
|
+
internalStep.maxDuration = step.maxDuration;
|
|
360
|
+
}
|
|
361
|
+
// Inputs land on nodeConfig so the blueprint mapper resolves
|
|
362
|
+
// $.<path> / js/... refs before SubworkflowNode reads them via
|
|
363
|
+
// `ctx.config[step.name]`.
|
|
364
|
+
const inlineInputs = isPlainObject(step.inputs) ? step.inputs : null;
|
|
365
|
+
const nodeConfig = inlineInputs ? { inputs: inlineInputs } : null;
|
|
366
|
+
return { internalStep, nodeConfig };
|
|
367
|
+
}
|
|
368
|
+
const WAIT_NODE_REF = "@blokjs/wait";
|
|
369
|
+
/**
|
|
370
|
+
* PR 4 — normalize a v2 wait step.
|
|
371
|
+
*
|
|
372
|
+
* Wait steps are intercepted by `RunnerSteps` BEFORE `step.process` is
|
|
373
|
+
* invoked (the wait IS the runner-level deferral); the resolved node is
|
|
374
|
+
* a no-op placeholder. The runner reads `waitForMs` / `waitUntil` off
|
|
375
|
+
* the InternalStep to decide how long to wait.
|
|
376
|
+
*
|
|
377
|
+
* `wait.for` (duration string or number) is parsed to milliseconds via
|
|
378
|
+
* `parseDuration`. `wait.until` is left as-is — the runner resolves
|
|
379
|
+
* $-proxy expressions against the live ctx at first-pass invocation.
|
|
380
|
+
*/
|
|
381
|
+
function normalizeWaitStep(step, index) {
|
|
382
|
+
const id = pickString(step.id);
|
|
383
|
+
if (!id) {
|
|
384
|
+
throw new Error(`[blok] WorkflowNormalizer: wait step at index ${index} is missing \`id\`.`);
|
|
385
|
+
}
|
|
386
|
+
const waitObj = step.wait;
|
|
387
|
+
const hasFor = waitObj.for !== undefined;
|
|
388
|
+
const hasUntil = waitObj.until !== undefined;
|
|
389
|
+
if (hasFor === hasUntil) {
|
|
390
|
+
throw new Error(`[blok] WorkflowNormalizer: wait step "${id}" must set exactly one of \`wait.for\` or \`wait.until\`.`);
|
|
391
|
+
}
|
|
392
|
+
let waitForMs;
|
|
393
|
+
let waitUntil;
|
|
394
|
+
if (hasFor) {
|
|
395
|
+
const raw = waitObj.for;
|
|
396
|
+
if (typeof raw === "number") {
|
|
397
|
+
waitForMs = raw;
|
|
398
|
+
}
|
|
399
|
+
else if (typeof raw === "string") {
|
|
400
|
+
// parseDuration may throw on invalid grammar — let it surface.
|
|
401
|
+
waitForMs = parseDuration(raw);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
throw new Error(`[blok] WorkflowNormalizer: wait step "${id}" has invalid \`wait.for\` (must be number ms or duration string).`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (hasUntil) {
|
|
408
|
+
const raw = waitObj.until;
|
|
409
|
+
if (typeof raw !== "number" && typeof raw !== "string") {
|
|
410
|
+
throw new Error(`[blok] WorkflowNormalizer: wait step "${id}" has invalid \`wait.until\` (must be number ms or string).`);
|
|
411
|
+
}
|
|
412
|
+
waitUntil = raw;
|
|
413
|
+
}
|
|
414
|
+
const ephemeral = step.ephemeral === true;
|
|
415
|
+
const as = pickString(step.as);
|
|
416
|
+
return {
|
|
417
|
+
name: id,
|
|
418
|
+
node: WAIT_NODE_REF,
|
|
419
|
+
type: "wait",
|
|
420
|
+
active: step.active === undefined ? true : step.active === true,
|
|
421
|
+
stop: step.stop === true,
|
|
422
|
+
as,
|
|
423
|
+
ephemeral,
|
|
424
|
+
waitForMs,
|
|
425
|
+
waitUntil,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function normalizeTrigger(rawTrigger, sourcePath) {
|
|
429
|
+
if (!isPlainObject(rawTrigger))
|
|
430
|
+
return {};
|
|
431
|
+
const out = {};
|
|
432
|
+
for (const [kind, cfg] of Object.entries(rawTrigger)) {
|
|
433
|
+
if (kind === "http" && isPlainObject(cfg)) {
|
|
434
|
+
const httpCfg = { ...cfg };
|
|
435
|
+
if (httpCfg.method === "*") {
|
|
436
|
+
httpCfg.method = "ANY";
|
|
437
|
+
warnWildcardOnce(sourcePath);
|
|
438
|
+
}
|
|
439
|
+
out[kind] = httpCfg;
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
out[kind] = cfg;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
function inferStepType(nodeRef) {
|
|
448
|
+
// Explicit runtime prefixes — `runtime.python3:my-node` style.
|
|
449
|
+
if (nodeRef.startsWith("runtime.")) {
|
|
450
|
+
const dotIdx = nodeRef.indexOf(":");
|
|
451
|
+
if (dotIdx > 0)
|
|
452
|
+
return nodeRef.slice(0, dotIdx);
|
|
453
|
+
return nodeRef;
|
|
454
|
+
}
|
|
455
|
+
// Default to module — covers `@blokjs/*` and most user-defined nodes.
|
|
456
|
+
return "module";
|
|
457
|
+
}
|
|
458
|
+
function warnWildcardOnce(sourcePath) {
|
|
459
|
+
const key = sourcePath ?? "<unknown>";
|
|
460
|
+
if (_wildcardWarnedFiles.has(key))
|
|
461
|
+
return;
|
|
462
|
+
_wildcardWarnedFiles.add(key);
|
|
463
|
+
console.warn(`[blok] trigger.http.method "*" is deprecated; use "ANY" instead. (workflow: ${key}). Run \`blokctl migrate workflows\` to update.`);
|
|
464
|
+
}
|
|
465
|
+
function isPlainObject(value) {
|
|
466
|
+
if (value === null || value === undefined)
|
|
467
|
+
return false;
|
|
468
|
+
if (typeof value !== "object")
|
|
469
|
+
return false;
|
|
470
|
+
if (Array.isArray(value))
|
|
471
|
+
return false;
|
|
472
|
+
const proto = Object.getPrototypeOf(value);
|
|
473
|
+
return proto === null || proto === Object.prototype;
|
|
474
|
+
}
|
|
475
|
+
function pickString(value) {
|
|
476
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Test-only — reset the per-process wildcard warning cache.
|
|
480
|
+
*
|
|
481
|
+
* @internal
|
|
482
|
+
*/
|
|
483
|
+
export function _resetWildcardWarningCache() {
|
|
484
|
+
_wildcardWarnedFiles = new Set();
|
|
485
|
+
}
|
|
486
|
+
//# sourceMappingURL=WorkflowNormalizer.js.map
|