@lotiai/composer 0.2.0 → 0.2.2
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/build-config/index.d.ts +52 -0
- package/dist/build-config/index.d.ts.map +1 -0
- package/dist/build-config/index.js +66 -0
- package/dist/build-config/index.js.map +1 -0
- package/dist/build-config/loader.d.ts +80 -0
- package/dist/build-config/loader.d.ts.map +1 -0
- package/dist/build-config/loader.js +238 -0
- package/dist/build-config/loader.js.map +1 -0
- package/dist/build-config/schema.d.ts +101 -0
- package/dist/build-config/schema.d.ts.map +1 -0
- package/dist/build-config/schema.js +94 -0
- package/dist/build-config/schema.js.map +1 -0
- package/dist/cli/commands/dev-temporal.d.ts +15 -0
- package/dist/cli/commands/dev-temporal.d.ts.map +1 -0
- package/dist/cli/commands/dev-temporal.js +68 -0
- package/dist/cli/commands/dev-temporal.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +21 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +421 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/profiles.d.ts +15 -0
- package/dist/cli/commands/profiles.d.ts.map +1 -0
- package/dist/cli/commands/profiles.js +69 -0
- package/dist/cli/commands/profiles.js.map +1 -0
- package/dist/cli/index.d.ts +21 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +44 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/async/build/workflow-factory.d.ts +212 -0
- package/dist/internal/async/build/workflow-factory.d.ts.map +1 -0
- package/dist/internal/async/build/workflow-factory.js +533 -0
- package/dist/internal/async/build/workflow-factory.js.map +1 -0
- package/dist/internal/async/build-scripts/utils/common.d.ts +28 -0
- package/dist/internal/async/build-scripts/utils/common.d.ts.map +1 -0
- package/dist/internal/async/build-scripts/utils/common.js +50 -0
- package/dist/internal/async/build-scripts/utils/common.js.map +1 -0
- package/dist/internal/async/build-scripts/utils/import-module.d.ts +19 -0
- package/dist/internal/async/build-scripts/utils/import-module.d.ts.map +1 -0
- package/dist/internal/async/build-scripts/utils/import-module.js +80 -0
- package/dist/internal/async/build-scripts/utils/import-module.js.map +1 -0
- package/dist/internal/async/build-scripts/utils/type-guards.d.ts +123 -0
- package/dist/internal/async/build-scripts/utils/type-guards.d.ts.map +1 -0
- package/dist/internal/async/build-scripts/utils/type-guards.js +145 -0
- package/dist/internal/async/build-scripts/utils/type-guards.js.map +1 -0
- package/dist/internal/async/config/cleanup-policy.d.ts +50 -0
- package/dist/internal/async/config/cleanup-policy.d.ts.map +1 -0
- package/dist/internal/async/config/cleanup-policy.js +20 -0
- package/dist/internal/async/config/cleanup-policy.js.map +1 -0
- package/dist/internal/async/config/worker-profiles.d.ts +172 -0
- package/dist/internal/async/config/worker-profiles.d.ts.map +1 -0
- package/dist/internal/async/config/worker-profiles.js +275 -0
- package/dist/internal/async/config/worker-profiles.js.map +1 -0
- package/dist/internal/async/execute/temporal-client.d.ts +182 -0
- package/dist/internal/async/execute/temporal-client.d.ts.map +1 -0
- package/dist/internal/async/execute/temporal-client.js +190 -0
- package/dist/internal/async/execute/temporal-client.js.map +1 -0
- package/dist/internal/async/execute/workflow-execution.d.ts +98 -0
- package/dist/internal/async/execute/workflow-execution.d.ts.map +1 -0
- package/dist/internal/async/execute/workflow-execution.js +191 -0
- package/dist/internal/async/execute/workflow-execution.js.map +1 -0
- package/dist/internal/async/metrics/task-queue-metrics.d.ts +74 -0
- package/dist/internal/async/metrics/task-queue-metrics.d.ts.map +1 -0
- package/dist/internal/async/metrics/task-queue-metrics.js +155 -0
- package/dist/internal/async/metrics/task-queue-metrics.js.map +1 -0
- package/dist/internal/async/register/activity-worker.d.ts +123 -0
- package/dist/internal/async/register/activity-worker.d.ts.map +1 -0
- package/dist/internal/async/register/activity-worker.js +355 -0
- package/dist/internal/async/register/activity-worker.js.map +1 -0
- package/dist/internal/async/register/generate-workflow-source.d.ts +43 -0
- package/dist/internal/async/register/generate-workflow-source.d.ts.map +1 -0
- package/dist/internal/async/register/generate-workflow-source.js +193 -0
- package/dist/internal/async/register/generate-workflow-source.js.map +1 -0
- package/dist/internal/async/register/workflow-worker.d.ts +125 -0
- package/dist/internal/async/register/workflow-worker.d.ts.map +1 -0
- package/dist/internal/async/register/workflow-worker.js +215 -0
- package/dist/internal/async/register/workflow-worker.js.map +1 -0
- package/dist/internal/async/register-scripts/start-activity-worker.d.ts +70 -0
- package/dist/internal/async/register-scripts/start-activity-worker.d.ts.map +1 -0
- package/dist/internal/async/register-scripts/start-activity-worker.js +75 -0
- package/dist/internal/async/register-scripts/start-activity-worker.js.map +1 -0
- package/dist/internal/async/register-scripts/start-all-workers.d.ts +81 -0
- package/dist/internal/async/register-scripts/start-all-workers.d.ts.map +1 -0
- package/dist/internal/async/register-scripts/start-all-workers.js +84 -0
- package/dist/internal/async/register-scripts/start-all-workers.js.map +1 -0
- package/dist/internal/async/register-scripts/start-workflow-worker.d.ts +69 -0
- package/dist/internal/async/register-scripts/start-workflow-worker.d.ts.map +1 -0
- package/dist/internal/async/register-scripts/start-workflow-worker.js +74 -0
- package/dist/internal/async/register-scripts/start-workflow-worker.js.map +1 -0
- package/dist/internal/async/register-scripts/sync-schedules-cli.d.ts +28 -0
- package/dist/internal/async/register-scripts/sync-schedules-cli.d.ts.map +1 -0
- package/dist/internal/async/register-scripts/sync-schedules-cli.js +124 -0
- package/dist/internal/async/register-scripts/sync-schedules-cli.js.map +1 -0
- package/dist/internal/async/register-scripts/sync-schedules-via-lambda.d.ts +55 -0
- package/dist/internal/async/register-scripts/sync-schedules-via-lambda.d.ts.map +1 -0
- package/dist/internal/async/register-scripts/sync-schedules-via-lambda.js +146 -0
- package/dist/internal/async/register-scripts/sync-schedules-via-lambda.js.map +1 -0
- package/dist/internal/async/register-scripts/sync-schedules.d.ts +68 -0
- package/dist/internal/async/register-scripts/sync-schedules.d.ts.map +1 -0
- package/dist/internal/async/register-scripts/sync-schedules.js +83 -0
- package/dist/internal/async/register-scripts/sync-schedules.js.map +1 -0
- package/dist/internal/async/schedule/constants.d.ts +10 -0
- package/dist/internal/async/schedule/constants.d.ts.map +1 -0
- package/dist/internal/async/schedule/constants.js +13 -0
- package/dist/internal/async/schedule/constants.js.map +1 -0
- package/dist/internal/async/schedule/define-schedule.d.ts +192 -0
- package/dist/internal/async/schedule/define-schedule.d.ts.map +1 -0
- package/dist/internal/async/schedule/define-schedule.js +79 -0
- package/dist/internal/async/schedule/define-schedule.js.map +1 -0
- package/dist/internal/async/schedule/index.d.ts +3 -0
- package/dist/internal/async/schedule/index.d.ts.map +1 -0
- package/dist/internal/async/schedule/index.js +9 -0
- package/dist/internal/async/schedule/index.js.map +1 -0
- package/dist/internal/async/schedule/schedule-definition-schema.d.ts +64 -0
- package/dist/internal/async/schedule/schedule-definition-schema.d.ts.map +1 -0
- package/dist/internal/async/schedule/schedule-definition-schema.js +77 -0
- package/dist/internal/async/schedule/schedule-definition-schema.js.map +1 -0
- package/dist/internal/async/schedule/sync-schedules.d.ts +71 -0
- package/dist/internal/async/schedule/sync-schedules.d.ts.map +1 -0
- package/dist/internal/async/schedule/sync-schedules.js +231 -0
- package/dist/internal/async/schedule/sync-schedules.js.map +1 -0
- package/dist/internal/async/utils/ensure-namespace.d.ts +16 -0
- package/dist/internal/async/utils/ensure-namespace.d.ts.map +1 -0
- package/dist/internal/async/utils/ensure-namespace.js +64 -0
- package/dist/internal/async/utils/ensure-namespace.js.map +1 -0
- package/dist/internal/async/utils/find-package-root.d.ts +9 -0
- package/dist/internal/async/utils/find-package-root.d.ts.map +1 -0
- package/dist/internal/async/utils/find-package-root.js +28 -0
- package/dist/internal/async/utils/find-package-root.js.map +1 -0
- package/dist/internal/async/utils/is-composer-error.d.ts +14 -0
- package/dist/internal/async/utils/is-composer-error.d.ts.map +1 -0
- package/dist/internal/async/utils/is-composer-error.js +17 -0
- package/dist/internal/async/utils/is-composer-error.js.map +1 -0
- package/dist/internal/context-provider.d.ts +316 -0
- package/dist/internal/context-provider.d.ts.map +1 -0
- package/dist/internal/context-provider.js +12 -0
- package/dist/internal/context-provider.js.map +1 -0
- package/dist/internal/create-composer.d.ts +57 -0
- package/dist/internal/create-composer.d.ts.map +1 -0
- package/dist/internal/create-composer.js +125 -0
- package/dist/internal/create-composer.js.map +1 -0
- package/dist/internal/dag-sync-fanout.d.ts +79 -0
- package/dist/internal/dag-sync-fanout.d.ts.map +1 -0
- package/dist/internal/dag-sync-fanout.js +59 -0
- package/dist/internal/dag-sync-fanout.js.map +1 -0
- package/dist/internal/dag-sync-step.d.ts +230 -0
- package/dist/internal/dag-sync-step.d.ts.map +1 -0
- package/dist/internal/dag-sync-step.js +159 -0
- package/dist/internal/dag-sync-step.js.map +1 -0
- package/dist/internal/dag-sync-workflow.d.ts +535 -0
- package/dist/internal/dag-sync-workflow.d.ts.map +1 -0
- package/dist/internal/dag-sync-workflow.js +843 -0
- package/dist/internal/dag-sync-workflow.js.map +1 -0
- package/dist/internal/defaults.d.ts +24 -0
- package/dist/internal/defaults.d.ts.map +1 -0
- package/dist/internal/defaults.js +102 -0
- package/dist/internal/defaults.js.map +1 -0
- package/dist/internal/errors.d.ts +109 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +212 -0
- package/dist/internal/errors.js.map +1 -0
- package/dist/internal/index.d.ts +9 -0
- package/dist/internal/index.d.ts.map +1 -0
- package/dist/internal/index.js +22 -0
- package/dist/internal/index.js.map +1 -0
- package/dist/internal/observability.d.ts +63 -0
- package/dist/internal/observability.d.ts.map +1 -0
- package/dist/internal/observability.js +323 -0
- package/dist/internal/observability.js.map +1 -0
- package/dist/internal/types.d.ts +80 -0
- package/dist/internal/types.d.ts.map +1 -0
- package/dist/internal/types.js +9 -0
- package/dist/internal/types.js.map +1 -0
- package/dist/internal/workflow-planning.d.ts +107 -0
- package/dist/internal/workflow-planning.d.ts.map +1 -0
- package/dist/internal/workflow-planning.js +216 -0
- package/dist/internal/workflow-planning.js.map +1 -0
- package/dist/schedule-sync.d.ts +17 -0
- package/dist/schedule-sync.d.ts.map +1 -0
- package/dist/schedule-sync.js +22 -0
- package/dist/schedule-sync.js.map +1 -0
- package/dist/temporal-naming.d.ts +27 -0
- package/dist/temporal-naming.d.ts.map +1 -0
- package/dist/temporal-naming.js +33 -0
- package/dist/temporal-naming.js.map +1 -0
- package/package.json +29 -17
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DAG-based Workflow Engine with Compile-time Validation
|
|
4
|
+
*
|
|
5
|
+
* This module implements a directed acyclic graph (DAG) workflow system that:
|
|
6
|
+
* - Validates step dependencies at compile-time using TypeScript
|
|
7
|
+
* - Executes steps in parallel when dependencies allow
|
|
8
|
+
* - Uses topological sorting with batching for optimal performance
|
|
9
|
+
* - Supports workflow composition for modularity and reusability
|
|
10
|
+
*
|
|
11
|
+
* ## Core Concepts
|
|
12
|
+
*
|
|
13
|
+
* **Step**: A unit of work with declared inputs (needs) and outputs (provides)
|
|
14
|
+
* **Workflow**: A collection of steps with automatic dependency resolution
|
|
15
|
+
* **Bag**: The data container that flows through the workflow
|
|
16
|
+
*
|
|
17
|
+
* ## Execution Model
|
|
18
|
+
*
|
|
19
|
+
* The engine uses **batch-based parallel execution**:
|
|
20
|
+
* 1. **Dependency Analysis**: Builds a dependency graph using topological sorting
|
|
21
|
+
* 2. **Batch Formation**: Groups steps that can run simultaneously
|
|
22
|
+
* 3. **Parallel Execution**: Runs each batch with Promise.all()
|
|
23
|
+
* 4. **Dependency Tracking**: Uses indegree counters to trigger next batch
|
|
24
|
+
*
|
|
25
|
+
* ## Example Execution Flow
|
|
26
|
+
*
|
|
27
|
+
* Given workflow: [stepA, stepB, stepC, stepD]
|
|
28
|
+
* - stepA: needs [] → provides ["x"]
|
|
29
|
+
* - stepB: needs ["x"] → provides ["y"]
|
|
30
|
+
* - stepC: needs ["x"] → provides ["z"]
|
|
31
|
+
* - stepD: needs ["y", "z"] → provides ["result"]
|
|
32
|
+
*
|
|
33
|
+
* Execution batches:
|
|
34
|
+
* - Batch 1: [stepA] (sequential)
|
|
35
|
+
* - Batch 2: [stepB, stepC] (parallel - both need "x")
|
|
36
|
+
* - Batch 3: [stepD] (sequential - needs both "y" and "z")
|
|
37
|
+
*
|
|
38
|
+
* ## Workflow Composition
|
|
39
|
+
*
|
|
40
|
+
* Workflows can be composed using the `use()` helper to create modular, reusable components:
|
|
41
|
+
*
|
|
42
|
+
* ```typescript
|
|
43
|
+
* // Define a reusable child workflow
|
|
44
|
+
* const authWorkflow = createWorkflow<Bag>("authentication")
|
|
45
|
+
* .requires("userId")
|
|
46
|
+
* .build([validateUser, checkPermissions]);
|
|
47
|
+
*
|
|
48
|
+
* // Compose into parent workflow
|
|
49
|
+
* const mainWorkflow = createWorkflow<Bag>("main")
|
|
50
|
+
* .requires("userId")
|
|
51
|
+
* .build([
|
|
52
|
+
* fetchUserData, // Provides userId
|
|
53
|
+
* use(authWorkflow), // Reuses authentication logic
|
|
54
|
+
* processRequest, // Uses auth results
|
|
55
|
+
* ]);
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* **Key Composition Features:**
|
|
59
|
+
* - **Step Flattening**: Child workflow steps are merged into parent's dependency graph
|
|
60
|
+
* - **Maximum Parallelization**: Steps from different workflows can execute in the same batch
|
|
61
|
+
* - **Automatic Namespacing**: Child steps are prefixed as "childName.stepName" to prevent conflicts
|
|
62
|
+
* - **Dependency Propagation**: Child initial requirements become regular dependencies in parent
|
|
63
|
+
* - **Nested Composition**: Workflows can compose other composed workflows to any depth
|
|
64
|
+
* - **Observability Preservation**: SubWorkflow spans maintain logical boundaries in traces
|
|
65
|
+
*
|
|
66
|
+
* **Composition Benefits:**
|
|
67
|
+
* - Code reusability across different workflows
|
|
68
|
+
* - Easier testing of workflow components in isolation
|
|
69
|
+
* - Better organization of complex business logic
|
|
70
|
+
* - Maintains compile-time type safety across composition boundaries
|
|
71
|
+
*
|
|
72
|
+
* ## Compile-time Safety
|
|
73
|
+
*
|
|
74
|
+
* The type system validates:
|
|
75
|
+
* - All step dependencies are satisfied by previous steps or initial data
|
|
76
|
+
* - Required initial fields are provided at runtime
|
|
77
|
+
* - Clear error messages for missing dependencies
|
|
78
|
+
*
|
|
79
|
+
* ## Data Protection & Overwrite Prevention
|
|
80
|
+
*
|
|
81
|
+
* The engine enforces strict data integrity rules:
|
|
82
|
+
* - **Step-to-Step Protection**: Each field can have only one producer step (duplicate producers are rejected)
|
|
83
|
+
* - **Initial Data Protection**: Steps cannot overwrite fields provided in initial data
|
|
84
|
+
* - **Immutable Outputs**: Once a step produces a field, it cannot be modified by other steps
|
|
85
|
+
* - **Early Detection**: All conflicts are caught at workflow execution time, not during step execution
|
|
86
|
+
*
|
|
87
|
+
* ## Performance Characteristics
|
|
88
|
+
*
|
|
89
|
+
* - **Automatic Parallelization**: Steps run concurrently when safe
|
|
90
|
+
* - **Batch Synchronization**: All steps in a batch complete before next batch starts
|
|
91
|
+
* - **Zero Polling**: Event-driven execution using dependency counters
|
|
92
|
+
* - **Optimal Scheduling**: Steps execute as soon as dependencies are satisfied
|
|
93
|
+
*/
|
|
94
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
95
|
+
exports.DEFAULT_CHECKPOINT_TIMEOUT_MS = void 0;
|
|
96
|
+
exports.use = use;
|
|
97
|
+
exports.createWorkflow = createWorkflow;
|
|
98
|
+
exports.runSyncWorkflow = runSyncWorkflow;
|
|
99
|
+
const api_1 = require("@opentelemetry/api");
|
|
100
|
+
const uuid_1 = require("uuid");
|
|
101
|
+
const dag_sync_fanout_1 = require("./dag-sync-fanout");
|
|
102
|
+
const defaults_1 = require("./defaults");
|
|
103
|
+
const errors_1 = require("./errors");
|
|
104
|
+
const observability_1 = require("./observability");
|
|
105
|
+
const workflow_planning_1 = require("./workflow-planning");
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Checkpoint Types
|
|
108
|
+
// ============================================================================
|
|
109
|
+
/**
|
|
110
|
+
* Default timeout for checkpoints in milliseconds.
|
|
111
|
+
*
|
|
112
|
+
* Checkpoints are designed for fast early returns within HTTP timeout windows.
|
|
113
|
+
* The default of 30 seconds provides a reasonable window for initial step(s)
|
|
114
|
+
* to complete while leaving headroom before typical HTTP timeouts (60s).
|
|
115
|
+
*/
|
|
116
|
+
exports.DEFAULT_CHECKPOINT_TIMEOUT_MS = 30_000; // 30 seconds
|
|
117
|
+
/**
|
|
118
|
+
* IMMUTABILITY PROTECTION FEATURE FLAG
|
|
119
|
+
*
|
|
120
|
+
* When enabled, all step outputs are deeply frozen before being merged into the shared bag.
|
|
121
|
+
* This prevents mutation bugs that can cause non-deterministic workflow behavior.
|
|
122
|
+
*
|
|
123
|
+
* ## What This Protects Against:
|
|
124
|
+
*
|
|
125
|
+
* 1. **Parallel batch mutations**: Two steps in the same batch mutating shared references
|
|
126
|
+
* Example: Both steps call bag.identities.push(...) → race condition
|
|
127
|
+
*
|
|
128
|
+
* 2. **Downstream mutations**: A step mutates data it received from a previous step
|
|
129
|
+
* Example: step.run((ctx, bag) => { bag.rules.sort(); ... }) → changes data for peers
|
|
130
|
+
*
|
|
131
|
+
* 3. **Post-return mutations**: A step keeps a reference and mutates it later
|
|
132
|
+
* Example: setTimeout(() => myData.length = 0, 100) → corrupts bag after batch completes
|
|
133
|
+
*
|
|
134
|
+
* 4. **Array/nested mutations**: TypeScript's Readonly<> is shallow and can't prevent these
|
|
135
|
+
* Example: bag.identities[0].name = "x" → compiles but violates immutability
|
|
136
|
+
*
|
|
137
|
+
* 5. **Third-party library side effects**: SDKs that mutate arguments in place
|
|
138
|
+
* Example: someLibrary.normalize(bag.config) → unexpected mutations
|
|
139
|
+
*
|
|
140
|
+
* ## Why It's Feature-Flagged:
|
|
141
|
+
*
|
|
142
|
+
* Deep freeze has a performance cost (~0.5-10ms per output depending on object size).
|
|
143
|
+
* For workflows where steps do I/O (database, API calls), this overhead is negligible.
|
|
144
|
+
* For CPU-intensive workflows with large objects, it may be noticeable.
|
|
145
|
+
*
|
|
146
|
+
* Recommendation:
|
|
147
|
+
* - Enable in development/test to catch mutation bugs early
|
|
148
|
+
* - Enable in production unless profiling shows it's a bottleneck
|
|
149
|
+
* - If disabled in prod, ensure code reviews catch mutation patterns
|
|
150
|
+
*
|
|
151
|
+
* Deep freeze is now controlled by the `deepFreeze` option in ComposerConfig.
|
|
152
|
+
*
|
|
153
|
+
* TODO(nate): Add observability for cost of deep freeze + any runtime validation.
|
|
154
|
+
*/
|
|
155
|
+
/**
|
|
156
|
+
* Recursively freezes an object and all nested objects/arrays.
|
|
157
|
+
*
|
|
158
|
+
* This provides runtime immutability guarantees that TypeScript's type system cannot enforce.
|
|
159
|
+
* After deep freezing, any attempt to mutate the object or its nested properties will throw
|
|
160
|
+
* an error in strict mode or silently fail in non-strict mode.
|
|
161
|
+
*
|
|
162
|
+
* @param obj - The object to freeze
|
|
163
|
+
* @returns The frozen object (same reference, now immutable)
|
|
164
|
+
*
|
|
165
|
+
* Performance characteristics:
|
|
166
|
+
* - O(n) where n is the total number of nodes in the object graph
|
|
167
|
+
* - Typical cost: 0.5-10ms for objects with 100-1000 nodes
|
|
168
|
+
* - Skips already-frozen objects for efficiency
|
|
169
|
+
* - Handles null/undefined/primitives safely
|
|
170
|
+
*/
|
|
171
|
+
function deepFreeze(obj) {
|
|
172
|
+
// Skip primitives, null, undefined, and already-frozen objects
|
|
173
|
+
if (!obj || typeof obj !== "object" || Object.isFrozen(obj)) {
|
|
174
|
+
return obj;
|
|
175
|
+
}
|
|
176
|
+
// Freeze this level
|
|
177
|
+
Object.freeze(obj);
|
|
178
|
+
// Recursively freeze all nested values
|
|
179
|
+
for (const value of Object.values(obj)) {
|
|
180
|
+
deepFreeze(value);
|
|
181
|
+
}
|
|
182
|
+
return obj;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Runtime validation that ensures all step names are unique.
|
|
186
|
+
*
|
|
187
|
+
* Step names are used as primary keys in dependency maps (stepsByName, children, indegree).
|
|
188
|
+
* Duplicate names would cause silent overwrites in these maps, corrupting the DAG structure
|
|
189
|
+
* and leading to wrong execution order or missing dependencies.
|
|
190
|
+
*
|
|
191
|
+
* This function provides clear error messages that help developers identify which steps
|
|
192
|
+
* have conflicting names and where they appear in the workflow definition.
|
|
193
|
+
*/
|
|
194
|
+
function assertUniqueStepNames(steps) {
|
|
195
|
+
const seen = new Map(); // Track first occurrence index for better errors
|
|
196
|
+
steps.forEach((step, index) => {
|
|
197
|
+
const stepName = step.name;
|
|
198
|
+
if (seen.has(stepName)) {
|
|
199
|
+
const firstIndex = seen.get(stepName);
|
|
200
|
+
throw new Error(`Duplicate step name "${stepName}" found at positions ${firstIndex} and ${index}. ` +
|
|
201
|
+
`Step names must be unique within a workflow.\n\n` +
|
|
202
|
+
`💡 Fix: Rename one of the steps to have a unique name.`);
|
|
203
|
+
}
|
|
204
|
+
seen.set(stepName, index);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Helper function to find the first duplicate key in an array.
|
|
209
|
+
*
|
|
210
|
+
* Returns undefined if no duplicates exist, or the duplicated key if found.
|
|
211
|
+
*/
|
|
212
|
+
function findDuplicateKey(keys) {
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
for (const key of keys) {
|
|
215
|
+
const keyStr = String(key);
|
|
216
|
+
if (seen.has(keyStr)) {
|
|
217
|
+
return keyStr;
|
|
218
|
+
}
|
|
219
|
+
seen.add(keyStr);
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Runtime validation that ensures no duplicates exist in step needs/provides arrays.
|
|
225
|
+
*
|
|
226
|
+
* Duplicate needs cause deadlocks in the topological sort (indegree is incremented twice
|
|
227
|
+
* but only decremented once, so the step never reaches zero and never executes).
|
|
228
|
+
*
|
|
229
|
+
* Duplicate provides violate the semantic contract and make observability confusing,
|
|
230
|
+
* even though they don't break execution (the field only exists once in the return object).
|
|
231
|
+
*/
|
|
232
|
+
function assertNoDuplicatesInStepArrays(steps) {
|
|
233
|
+
for (const step of steps) {
|
|
234
|
+
const duplicateNeed = findDuplicateKey(step.needs);
|
|
235
|
+
if (duplicateNeed) {
|
|
236
|
+
throw new Error(`Step "${step.name}" lists "${duplicateNeed}" twice in "needs". ` +
|
|
237
|
+
`Each dependency should only be declared once.\n\n` +
|
|
238
|
+
`💡 Fix: Remove the duplicate "${duplicateNeed}" from the needs array.`);
|
|
239
|
+
}
|
|
240
|
+
const duplicateProvide = findDuplicateKey(step.provides);
|
|
241
|
+
if (duplicateProvide) {
|
|
242
|
+
throw new Error(`Step "${step.name}" lists "${duplicateProvide}" twice in "provides". ` +
|
|
243
|
+
`Each output field should only be declared once.\n\n` +
|
|
244
|
+
`💡 Fix: Remove the duplicate "${duplicateProvide}" from the provides array.`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Composes a child workflow into a parent workflow.
|
|
250
|
+
*
|
|
251
|
+
* When building a workflow, use `use(childWorkflow)` to include all steps from the child.
|
|
252
|
+
* The steps will be:
|
|
253
|
+
* - Flattened into the parent's dependency graph for maximum parallelization
|
|
254
|
+
* - Automatically namespaced as "childWorkflowName.stepName" to prevent conflicts
|
|
255
|
+
* - Tracked with workflow provenance metadata for proper observability
|
|
256
|
+
*
|
|
257
|
+
* Any initial requirements from the child workflow become regular dependencies that
|
|
258
|
+
* must be satisfied by the parent's steps or initial data.
|
|
259
|
+
*
|
|
260
|
+
* Example:
|
|
261
|
+
* ```typescript
|
|
262
|
+
* const childWf = createWorkflow<Bag>("child").build([stepA, stepB]);
|
|
263
|
+
* const parentWf = createWorkflow<Bag>("parent").build([
|
|
264
|
+
* stepX,
|
|
265
|
+
* use(childWf), // Includes stepA and stepB with proper namespacing
|
|
266
|
+
* stepY,
|
|
267
|
+
* ]);
|
|
268
|
+
*
|
|
269
|
+
* ## Observability
|
|
270
|
+
*
|
|
271
|
+
* Composed workflows create SubWorkflow spans in traces, making it easy to see logical
|
|
272
|
+
* boundaries even though execution is flattened:
|
|
273
|
+
* ```
|
|
274
|
+
* Workflow: main
|
|
275
|
+
* └─ Batch 1
|
|
276
|
+
* └─ SubWorkflow: authentication
|
|
277
|
+
* └─ Step: authentication.validateUser
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
function use(workflow) {
|
|
281
|
+
return {
|
|
282
|
+
__composedWorkflow: true,
|
|
283
|
+
workflow,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function workflow(name, steps, requiredInitial, configuredValues, checkpoints) {
|
|
287
|
+
// Explicitly specify all 5 type parameters to ensure CheckpointNames is `never`
|
|
288
|
+
const wf = {
|
|
289
|
+
name,
|
|
290
|
+
steps,
|
|
291
|
+
...(requiredInitial && requiredInitial.length > 0 ? { requiredInitial } : {}),
|
|
292
|
+
...(configuredValues && Object.keys(configuredValues).length > 0 ? { configuredValues } : {}),
|
|
293
|
+
...(checkpoints && checkpoints.length > 0 ? { checkpoints } : {}),
|
|
294
|
+
};
|
|
295
|
+
// Create a builder function that returns a new WorkflowBuilder with updated checkpoints
|
|
296
|
+
// The CheckpointNames type parameter is tracked at compile-time only; runtime uses currentCheckpoints
|
|
297
|
+
function createBuilder(currentWf, currentCheckpoints) {
|
|
298
|
+
// Implementation uses 'any' for internal assignment since TypeScript can't track
|
|
299
|
+
// the conditional types through Object.assign. The public interface types are correct.
|
|
300
|
+
const builder = {
|
|
301
|
+
...currentWf,
|
|
302
|
+
checkpoint(checkpointName, options) {
|
|
303
|
+
// Validate checkpoint name is unique (runtime validation as safety net)
|
|
304
|
+
if (currentCheckpoints.some((cp) => cp.name === checkpointName)) {
|
|
305
|
+
throw new Error(`Duplicate checkpoint name "${checkpointName}". Checkpoint names must be unique within a workflow.`);
|
|
306
|
+
}
|
|
307
|
+
// Extract step name from step reference
|
|
308
|
+
const stepName = options.afterStep.name;
|
|
309
|
+
// Validate step exists (supports both namespaced and unnamespaced)
|
|
310
|
+
// The TypeScript constraint S extends Steps[number] provides compile-time validation,
|
|
311
|
+
// but we still do runtime validation for composed workflows where step names get namespaced
|
|
312
|
+
const resolvedStepName = resolveStepName(steps, stepName);
|
|
313
|
+
if (!resolvedStepName) {
|
|
314
|
+
const availableSteps = steps.map((s) => s.name).join(", ");
|
|
315
|
+
throw new Error(`Checkpoint "${checkpointName}" references unknown step "${stepName}". ` +
|
|
316
|
+
`Available steps: [${availableSteps}]`);
|
|
317
|
+
}
|
|
318
|
+
const newCheckpoint = {
|
|
319
|
+
name: checkpointName,
|
|
320
|
+
afterStep: resolvedStepName, // Use resolved (namespaced) name
|
|
321
|
+
timeout: options.timeout,
|
|
322
|
+
};
|
|
323
|
+
const newCheckpoints = [...currentCheckpoints, newCheckpoint];
|
|
324
|
+
const newWf = {
|
|
325
|
+
...currentWf,
|
|
326
|
+
checkpoints: newCheckpoints,
|
|
327
|
+
};
|
|
328
|
+
return createBuilder(newWf, newCheckpoints);
|
|
329
|
+
},
|
|
330
|
+
onError(handler) {
|
|
331
|
+
return {
|
|
332
|
+
...currentWf,
|
|
333
|
+
checkpoints: currentCheckpoints.length > 0 ? currentCheckpoints : undefined,
|
|
334
|
+
errorHandler: handler,
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
return builder;
|
|
339
|
+
}
|
|
340
|
+
// Explicitly pass 'never' as the type parameter since no checkpoints are defined yet
|
|
341
|
+
return createBuilder(wf, checkpoints ?? []);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Resolves a step name, handling both namespaced and unnamespaced names.
|
|
345
|
+
*
|
|
346
|
+
* - If `stepName` exactly matches a step, returns it as-is.
|
|
347
|
+
* - If `stepName` doesn't match but matches the suffix of a namespaced step
|
|
348
|
+
* (e.g., "validateUser" matches "auth.validateUser"), returns the full namespaced name.
|
|
349
|
+
* - If multiple steps match the unnamespaced suffix, throws an error (ambiguous).
|
|
350
|
+
* - Returns null if no match is found.
|
|
351
|
+
*/
|
|
352
|
+
function resolveStepName(steps, stepName) {
|
|
353
|
+
// Direct match - return as-is
|
|
354
|
+
const directMatch = steps.find((s) => s.name === stepName);
|
|
355
|
+
if (directMatch) {
|
|
356
|
+
return directMatch.name;
|
|
357
|
+
}
|
|
358
|
+
// Try to find by suffix (unnamespaced name matching namespaced step)
|
|
359
|
+
const suffixMatches = steps.filter((s) => {
|
|
360
|
+
const parts = s.name.split(".");
|
|
361
|
+
const unnamespaced = parts[parts.length - 1];
|
|
362
|
+
return unnamespaced === stepName;
|
|
363
|
+
});
|
|
364
|
+
if (suffixMatches.length === 1) {
|
|
365
|
+
return suffixMatches[0].name;
|
|
366
|
+
}
|
|
367
|
+
if (suffixMatches.length > 1) {
|
|
368
|
+
const matchingNames = suffixMatches.map((s) => s.name).join(", ");
|
|
369
|
+
throw new Error(`Ambiguous step name "${stepName}" matches multiple steps: [${matchingNames}]. ` +
|
|
370
|
+
`Use the full namespaced name to specify which step.`);
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Type guard to check if an item is a ComposedWorkflow marker.
|
|
376
|
+
*/
|
|
377
|
+
function isComposedWorkflow(item) {
|
|
378
|
+
return item && typeof item === "object" && item.__composedWorkflow === true;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Flattens a step array that may contain ComposedWorkflow markers.
|
|
382
|
+
*
|
|
383
|
+
* This function:
|
|
384
|
+
* - Recursively processes the array to handle nested composition
|
|
385
|
+
* - Namespaces steps as "workflowName.stepName" to prevent conflicts
|
|
386
|
+
* - Tracks workflow provenance in the workflowPath property
|
|
387
|
+
* - Returns a flat array of steps ready for execution
|
|
388
|
+
*
|
|
389
|
+
* @param items - Array of steps and/or ComposedWorkflow markers
|
|
390
|
+
* @param parentPath - The workflow path of the parent (for nested composition)
|
|
391
|
+
* @returns Flattened array of steps with namespacing and provenance
|
|
392
|
+
*/
|
|
393
|
+
function flattenSteps(items, parentPath = []) {
|
|
394
|
+
const result = [];
|
|
395
|
+
for (const item of items) {
|
|
396
|
+
if (isComposedWorkflow(item)) {
|
|
397
|
+
const childWorkflow = item.workflow;
|
|
398
|
+
const childPath = [...parentPath, childWorkflow.name];
|
|
399
|
+
// Recursively flatten child workflow's steps
|
|
400
|
+
const childSteps = flattenSteps(childWorkflow.steps, childPath);
|
|
401
|
+
// Namespace each step and set workflow path
|
|
402
|
+
for (const step of childSteps) {
|
|
403
|
+
// Build the namespaced name: if step already has path, preserve it
|
|
404
|
+
const namespacedName = step.workflowPath
|
|
405
|
+
? step.name // Already namespaced from nested composition
|
|
406
|
+
: `${childWorkflow.name}.${step.name}`;
|
|
407
|
+
// For workflow path: if step already has one from nested composition,
|
|
408
|
+
// it starts with the child workflow's name, so we need to prepend the parent path
|
|
409
|
+
let workflowPath;
|
|
410
|
+
if (step.workflowPath && step.workflowPath.length > 0) {
|
|
411
|
+
// Step already has a path from being in a composed child workflow
|
|
412
|
+
// Prepend parent path (excluding the child workflow name since it's already in step.workflowPath)
|
|
413
|
+
workflowPath = [...parentPath, ...step.workflowPath];
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
// Step is directly in this child workflow
|
|
417
|
+
workflowPath = childPath;
|
|
418
|
+
}
|
|
419
|
+
result.push({
|
|
420
|
+
...step,
|
|
421
|
+
name: namespacedName,
|
|
422
|
+
workflowPath,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
// Regular step - add as-is
|
|
428
|
+
result.push(item);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Creates a workflow builder with compile-time dependency validation.
|
|
435
|
+
*
|
|
436
|
+
* This builder provides three modes:
|
|
437
|
+
* 1. build() - for workflows that don't require initial data
|
|
438
|
+
* 2. requires(fields).build() - for workflows that need the caller to provide specific fields
|
|
439
|
+
* 3. configure(configuration).requires(fields).build() - for workflows that need the bag
|
|
440
|
+
* configured with specific values, as well as initial values provided at runtime
|
|
441
|
+
*
|
|
442
|
+
* The compile-time validation works by:
|
|
443
|
+
* - Analyzing the step dependencies using recursive conditional types
|
|
444
|
+
* - If validation fails, injecting an error property that causes a compile error
|
|
445
|
+
* - If validation passes, allowing the workflow to be created normally
|
|
446
|
+
*
|
|
447
|
+
* Usage:
|
|
448
|
+
* // No initial data required
|
|
449
|
+
* const wf1 = createWorkflow<Bag>("my-workflow").build([stepA, stepB]);
|
|
450
|
+
*
|
|
451
|
+
* // Requires 'promptText' to be provided at runtime
|
|
452
|
+
* const wf2 = createWorkflow<Bag>("prompt-workflow").requires("promptText").build([stepA, stepB]);
|
|
453
|
+
*
|
|
454
|
+
* // Sets lotiContentType to "prompt_media" and requires 'promptText' to be
|
|
455
|
+
* provided at runtime.
|
|
456
|
+
* const wf3 = createWorkflow<Bag>("configured-prompt-workflow")
|
|
457
|
+
* .configure({ lotiContentType: "prompt_media" })
|
|
458
|
+
* .requires("promptText")
|
|
459
|
+
* .build([stepA, stepB]);
|
|
460
|
+
*/
|
|
461
|
+
function createWorkflow(name) {
|
|
462
|
+
return {
|
|
463
|
+
// Build workflow without required initial fields
|
|
464
|
+
build(steps) {
|
|
465
|
+
const flattenedSteps = flattenSteps(steps, [name]);
|
|
466
|
+
assertUniqueStepNames(flattenedSteps);
|
|
467
|
+
assertNoDuplicatesInStepArrays(flattenedSteps);
|
|
468
|
+
return workflow(name, flattenedSteps, undefined);
|
|
469
|
+
},
|
|
470
|
+
// Allow specifying initial fields that will be provided at runtime (rest parameters)
|
|
471
|
+
requires(...fields) {
|
|
472
|
+
return {
|
|
473
|
+
// Build workflow with required initial fields
|
|
474
|
+
build(steps) {
|
|
475
|
+
const flattenedSteps = flattenSteps(steps, [name]);
|
|
476
|
+
assertUniqueStepNames(flattenedSteps);
|
|
477
|
+
assertNoDuplicatesInStepArrays(flattenedSteps);
|
|
478
|
+
return workflow(name, flattenedSteps, fields);
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
configure(configuration) {
|
|
483
|
+
return {
|
|
484
|
+
build(steps) {
|
|
485
|
+
const flattenedSteps = flattenSteps(steps, [name]);
|
|
486
|
+
assertUniqueStepNames(flattenedSteps);
|
|
487
|
+
assertNoDuplicatesInStepArrays(flattenedSteps);
|
|
488
|
+
return workflow(name, flattenedSteps, undefined, configuration);
|
|
489
|
+
},
|
|
490
|
+
// Exclude ConfigKeys to prevent requiring fields that are already configured
|
|
491
|
+
requires(...fields) {
|
|
492
|
+
return {
|
|
493
|
+
build(steps) {
|
|
494
|
+
const flattenedSteps = flattenSteps(steps, [name]);
|
|
495
|
+
assertUniqueStepNames(flattenedSteps);
|
|
496
|
+
assertNoDuplicatesInStepArrays(flattenedSteps);
|
|
497
|
+
return workflow(name, flattenedSteps, fields, configuration);
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
// Implementation
|
|
506
|
+
async function runSyncWorkflow(wf, initialData, contextProvider, logger, enableDeepFreeze) {
|
|
507
|
+
// Resolve defaults for optional parameters
|
|
508
|
+
const log = logger ?? defaults_1.defaultLogger;
|
|
509
|
+
const deepFreeze_ = enableDeepFreeze ?? false;
|
|
510
|
+
// Generate workflowId for observability tracking
|
|
511
|
+
const workflowId = (0, uuid_1.v7)();
|
|
512
|
+
// Initialize bag outside try block so it's accessible in catch block
|
|
513
|
+
const configuredValues = (wf.configuredValues ?? {});
|
|
514
|
+
const bag = {
|
|
515
|
+
...initialData,
|
|
516
|
+
...configuredValues,
|
|
517
|
+
};
|
|
518
|
+
// Start workflow observability, catching any initialization errors
|
|
519
|
+
let workflowHandle;
|
|
520
|
+
try {
|
|
521
|
+
workflowHandle = (0, observability_1.startWorkflowObservability)(workflowId, wf, initialData, log);
|
|
522
|
+
}
|
|
523
|
+
catch (observabilityError) {
|
|
524
|
+
// Observability initialization failed - return error without throwing
|
|
525
|
+
const error = observabilityError instanceof Error
|
|
526
|
+
? observabilityError
|
|
527
|
+
: new Error(String(observabilityError));
|
|
528
|
+
return {
|
|
529
|
+
bag: bag,
|
|
530
|
+
error: new errors_1.WorkflowStepError({
|
|
531
|
+
workflowId,
|
|
532
|
+
stepName: "__observability_init__",
|
|
533
|
+
batchNumber: 0,
|
|
534
|
+
originalError: error,
|
|
535
|
+
bagState: bag,
|
|
536
|
+
workflowPath: [],
|
|
537
|
+
}),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
// Track execution context for enhanced error logging
|
|
541
|
+
const executionContext = {
|
|
542
|
+
stepName: undefined,
|
|
543
|
+
stepNumber: undefined,
|
|
544
|
+
totalSteps: wf.steps.length,
|
|
545
|
+
batchNumber: 0,
|
|
546
|
+
stepStartTime: undefined,
|
|
547
|
+
};
|
|
548
|
+
try {
|
|
549
|
+
// === PHASE 1: VALIDATION & PLANNING ===
|
|
550
|
+
// Validate that all step names are unique (defense-in-depth for dynamically constructed workflows)
|
|
551
|
+
assertUniqueStepNames(wf.steps);
|
|
552
|
+
// Validate that no step has duplicate entries in needs/provides arrays
|
|
553
|
+
assertNoDuplicatesInStepArrays(wf.steps);
|
|
554
|
+
// Plan workflow execution using topological sort
|
|
555
|
+
// This validates dependencies, detects cycles, and organizes steps into parallel execution batches
|
|
556
|
+
const initialFields = new Set([
|
|
557
|
+
...Object.keys(configuredValues),
|
|
558
|
+
...Object.keys(initialData ?? {}),
|
|
559
|
+
]);
|
|
560
|
+
const plan = (0, workflow_planning_1.planWorkflowBatches)([...wf.steps], initialFields);
|
|
561
|
+
// === PHASE 2: EXECUTE BATCHES ===
|
|
562
|
+
// Each batch contains steps that can run in parallel
|
|
563
|
+
for (let batchIndex = 0; batchIndex < plan.batches.length; batchIndex++) {
|
|
564
|
+
const batch = plan.batches[batchIndex]; // Safe: within bounds check
|
|
565
|
+
const batchNumber = batchIndex + 1;
|
|
566
|
+
// Update execution context for current batch
|
|
567
|
+
executionContext.batchNumber = batchNumber;
|
|
568
|
+
// Start batch observability
|
|
569
|
+
const batchHandle = (0, observability_1.startBatchObservability)(workflowHandle, batchNumber, batch.map((step) => step.name));
|
|
570
|
+
// Execute all steps in this batch in parallel using allSettled
|
|
571
|
+
// This ensures all steps complete (even if some fail) so we can:
|
|
572
|
+
// 1. Collect outputs from successful steps
|
|
573
|
+
// 2. Aggregate all failures into a single WorkflowBatchError
|
|
574
|
+
const settledResults = await Promise.allSettled(batch.map(async (step) => {
|
|
575
|
+
// Update execution context for current step
|
|
576
|
+
executionContext.stepName = step.name;
|
|
577
|
+
executionContext.stepNumber =
|
|
578
|
+
wf.steps.findIndex((workflowStep) => workflowStep.name === step.name) + 1; // 1-based indexing
|
|
579
|
+
executionContext.stepStartTime = Date.now();
|
|
580
|
+
// Start step observability
|
|
581
|
+
const stepHandle = (0, observability_1.startStepObservability)(workflowHandle, batchHandle, step);
|
|
582
|
+
// Create context via beforeStep hook if provider exists
|
|
583
|
+
let ctx;
|
|
584
|
+
if (contextProvider) {
|
|
585
|
+
ctx = await contextProvider.beforeStep(step.name);
|
|
586
|
+
}
|
|
587
|
+
let stepError;
|
|
588
|
+
try {
|
|
589
|
+
let stepOutput;
|
|
590
|
+
if ((0, dag_sync_fanout_1.isFanOutStep)(step)) {
|
|
591
|
+
stepOutput = await executeFanOut(step.__fanOut, step.name, bag, contextProvider, log, deepFreeze_);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
// Build input object with only the fields this step needs
|
|
595
|
+
const stepInput = step.needs.reduce((inputAcc, neededField) => {
|
|
596
|
+
inputAcc[neededField] = bag[neededField];
|
|
597
|
+
return inputAcc;
|
|
598
|
+
}, {});
|
|
599
|
+
// Freeze the step input if immutability protection is enabled
|
|
600
|
+
// This prevents steps from modifying their input bag
|
|
601
|
+
if (deepFreeze_) {
|
|
602
|
+
Object.freeze(stepInput);
|
|
603
|
+
}
|
|
604
|
+
const syncRuntime = {
|
|
605
|
+
heartbeat: () => { },
|
|
606
|
+
getHeartbeatDetails: () => undefined,
|
|
607
|
+
};
|
|
608
|
+
// Object.assign preserves context identity and prototype chain when ctx is
|
|
609
|
+
// a class instance, so afterStep receives the same object the step mutated.
|
|
610
|
+
// Falls back to spread when ctx is undefined (no context provider).
|
|
611
|
+
const stepCtx = ctx != null && typeof ctx === "object"
|
|
612
|
+
? Object.assign(ctx, syncRuntime)
|
|
613
|
+
: syncRuntime;
|
|
614
|
+
// Execute the step within the OTel step context for proper span hierarchy
|
|
615
|
+
stepOutput = (await api_1.context.with(stepHandle.stepContext, () => step.run(stepCtx, stepInput)));
|
|
616
|
+
}
|
|
617
|
+
// End step observability - success
|
|
618
|
+
(0, observability_1.endStepObservability)(stepHandle, {
|
|
619
|
+
success: true,
|
|
620
|
+
outputFields: Object.keys(stepOutput),
|
|
621
|
+
});
|
|
622
|
+
return { stepName: step.name, output: stepOutput };
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
// Wrap error with workflow context
|
|
626
|
+
stepError = error instanceof Error ? error : new Error(String(error));
|
|
627
|
+
const workflowError = new errors_1.WorkflowStepError({
|
|
628
|
+
workflowId: workflowId,
|
|
629
|
+
stepName: step.name,
|
|
630
|
+
batchNumber,
|
|
631
|
+
originalError: stepError,
|
|
632
|
+
bagState: bag,
|
|
633
|
+
workflowPath: step.workflowPath,
|
|
634
|
+
});
|
|
635
|
+
// End step observability - error
|
|
636
|
+
(0, observability_1.endStepObservability)(stepHandle, {
|
|
637
|
+
success: false,
|
|
638
|
+
error: workflowError,
|
|
639
|
+
});
|
|
640
|
+
throw workflowError;
|
|
641
|
+
}
|
|
642
|
+
finally {
|
|
643
|
+
// Call afterStep hook for cleanup if provider exists.
|
|
644
|
+
// Wrapped in try/catch to prevent cleanup errors (e.g., EM flush failures)
|
|
645
|
+
// from swallowing the original step error. The step's business logic error
|
|
646
|
+
// is more important to surface than infrastructure cleanup errors.
|
|
647
|
+
if (contextProvider && ctx !== undefined) {
|
|
648
|
+
try {
|
|
649
|
+
await contextProvider.afterStep(ctx, stepError);
|
|
650
|
+
}
|
|
651
|
+
catch (cleanupError) {
|
|
652
|
+
log.error("afterStep cleanup failed", {
|
|
653
|
+
stepName: step.name,
|
|
654
|
+
workflowId,
|
|
655
|
+
cleanupError: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
|
|
656
|
+
originalStepError: stepError?.message,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}));
|
|
662
|
+
// Separate fulfilled and rejected results
|
|
663
|
+
const fulfilled = settledResults.filter((r) => r.status === "fulfilled");
|
|
664
|
+
const rejected = settledResults.filter((r) => r.status === "rejected");
|
|
665
|
+
// First, merge successful step outputs into the bag
|
|
666
|
+
// This ensures the bag has all available data even if some steps failed
|
|
667
|
+
for (const result of fulfilled) {
|
|
668
|
+
const { output } = result.value;
|
|
669
|
+
if (deepFreeze_) {
|
|
670
|
+
for (const [key, value] of Object.entries(output)) {
|
|
671
|
+
bag[key] = deepFreeze(value);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
Object.assign(bag, output);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// If any steps failed, aggregate into WorkflowBatchError
|
|
679
|
+
if (rejected.length > 0) {
|
|
680
|
+
const stepErrors = rejected.map((r) => r.reason);
|
|
681
|
+
// End batch observability - error
|
|
682
|
+
(0, observability_1.endBatchObservability)(batchHandle, {
|
|
683
|
+
success: false,
|
|
684
|
+
error: stepErrors[0], // Primary error for observability (safe: length > 0)
|
|
685
|
+
});
|
|
686
|
+
throw new errors_1.WorkflowBatchError({
|
|
687
|
+
errors: stepErrors,
|
|
688
|
+
bagState: bag,
|
|
689
|
+
batchNumber,
|
|
690
|
+
workflowId,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
// End batch observability - success
|
|
694
|
+
(0, observability_1.endBatchObservability)(batchHandle, {
|
|
695
|
+
success: true,
|
|
696
|
+
outputFields: fulfilled.flatMap((r) => Object.keys(r.value.output)),
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
// End workflow observability - success
|
|
700
|
+
(0, observability_1.endWorkflowObservability)(workflowHandle, {
|
|
701
|
+
success: true,
|
|
702
|
+
batchCount: plan.batches.length,
|
|
703
|
+
outputFields: Object.keys(bag),
|
|
704
|
+
}, executionContext, bag);
|
|
705
|
+
return {
|
|
706
|
+
bag: bag,
|
|
707
|
+
error: undefined,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
// End workflow observability - error
|
|
712
|
+
(0, observability_1.endWorkflowObservability)(workflowHandle, {
|
|
713
|
+
success: false,
|
|
714
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
715
|
+
}, executionContext, bag);
|
|
716
|
+
// Cast bag for return type
|
|
717
|
+
const resultBag = bag;
|
|
718
|
+
// Invoke error handler if present
|
|
719
|
+
if (wf.errorHandler &&
|
|
720
|
+
(error instanceof errors_1.WorkflowStepError || error instanceof errors_1.WorkflowBatchError)) {
|
|
721
|
+
// Get context for error handler
|
|
722
|
+
let ctx;
|
|
723
|
+
if (contextProvider) {
|
|
724
|
+
try {
|
|
725
|
+
ctx = await contextProvider.beforeStep("__errorHandler__");
|
|
726
|
+
}
|
|
727
|
+
catch (contextError) {
|
|
728
|
+
// If we can't create context, proceed without it
|
|
729
|
+
log.warn("Failed to create context for error handler", {
|
|
730
|
+
workflowId,
|
|
731
|
+
contextError: contextError instanceof Error ? contextError.message : String(contextError),
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
// Handler returns Error | undefined
|
|
737
|
+
// - undefined = handled, no error to propagate
|
|
738
|
+
// - Error = propagate that error (possibly transformed)
|
|
739
|
+
const handlerResult = await wf.errorHandler(ctx, bag, error);
|
|
740
|
+
// Clean up context if we created one
|
|
741
|
+
if (contextProvider && ctx !== undefined) {
|
|
742
|
+
try {
|
|
743
|
+
await contextProvider.afterStep(ctx, undefined);
|
|
744
|
+
}
|
|
745
|
+
catch (cleanupError) {
|
|
746
|
+
log.error("afterStep cleanup failed for error handler", {
|
|
747
|
+
workflowId,
|
|
748
|
+
cleanupError: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Return based on handler result
|
|
753
|
+
return {
|
|
754
|
+
bag: resultBag,
|
|
755
|
+
error: handlerResult, // undefined = handled, Error = propagate
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
catch (handlerError) {
|
|
759
|
+
// Handler threw unexpectedly - wrap both errors
|
|
760
|
+
// Clean up context if we created one
|
|
761
|
+
if (contextProvider && ctx !== undefined) {
|
|
762
|
+
try {
|
|
763
|
+
await contextProvider.afterStep(ctx, handlerError instanceof Error ? handlerError : undefined);
|
|
764
|
+
}
|
|
765
|
+
catch (cleanupError) {
|
|
766
|
+
log.error("afterStep cleanup failed for error handler", {
|
|
767
|
+
workflowId,
|
|
768
|
+
cleanupError: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// Handler failed due to its own error (e.g., DB failure during cleanup)
|
|
773
|
+
return {
|
|
774
|
+
bag: resultBag,
|
|
775
|
+
error: new errors_1.WorkflowErrorHandlerFailure({
|
|
776
|
+
originalError: error,
|
|
777
|
+
handlerError: handlerError instanceof Error ? handlerError : new Error(String(handlerError)),
|
|
778
|
+
workflowId,
|
|
779
|
+
}),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
// No handler - return original error
|
|
784
|
+
return {
|
|
785
|
+
bag: resultBag,
|
|
786
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Executes a FanOut step using the lane-based concurrency pattern.
|
|
792
|
+
*
|
|
793
|
+
* 1. Calls mapInput to produce child workflow inputs from the current bag
|
|
794
|
+
* 2. Spawns min(concurrency, N) async lanes that pull work from a shared index
|
|
795
|
+
* 3. Each lane runs child workflows via runSyncWorkflow, collecting results
|
|
796
|
+
* 4. After all lanes complete, separates successes from failures
|
|
797
|
+
* 5. If all children succeed, calls aggregateResults to produce the step output
|
|
798
|
+
* 6. If any children fail, throws AggregateError containing child errors
|
|
799
|
+
* (compatible with extractErrorCandidates via .errors[])
|
|
800
|
+
*/
|
|
801
|
+
async function executeFanOut(metadata, fanOutName, currentBag, contextProvider, logger, enableDeepFreeze) {
|
|
802
|
+
const { childWorkflow, mapInput, aggregateResults, concurrency } = metadata;
|
|
803
|
+
const inputs = mapInput(currentBag);
|
|
804
|
+
if (inputs.length === 0) {
|
|
805
|
+
return aggregateResults([]);
|
|
806
|
+
}
|
|
807
|
+
const laneCount = Math.min(concurrency, inputs.length);
|
|
808
|
+
const results = new Array(inputs.length);
|
|
809
|
+
let nextIndex = 0;
|
|
810
|
+
const lane = async () => {
|
|
811
|
+
while (true) {
|
|
812
|
+
const index = nextIndex++;
|
|
813
|
+
if (index >= inputs.length)
|
|
814
|
+
break;
|
|
815
|
+
// Cast: type safety is enforced at the fanOut() factory level, not here
|
|
816
|
+
const childResult = await runSyncWorkflow(childWorkflow, inputs[index], contextProvider, logger, enableDeepFreeze);
|
|
817
|
+
if (childResult.error) {
|
|
818
|
+
results[index] = { error: childResult.error };
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
results[index] = { bag: childResult.bag };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
await Promise.all(Array.from({ length: laneCount }, () => lane()));
|
|
826
|
+
const errors = [];
|
|
827
|
+
const successfulBags = [];
|
|
828
|
+
for (const result of results) {
|
|
829
|
+
if (!result)
|
|
830
|
+
continue;
|
|
831
|
+
if ("error" in result) {
|
|
832
|
+
errors.push(result.error);
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
successfulBags.push(result.bag);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (errors.length > 0) {
|
|
839
|
+
throw new AggregateError(errors, `FanOut "${fanOutName}": ${errors.length} of ${inputs.length} child workflow(s) failed`);
|
|
840
|
+
}
|
|
841
|
+
return aggregateResults(successfulBags);
|
|
842
|
+
}
|
|
843
|
+
//# sourceMappingURL=dag-sync-workflow.js.map
|