@elaraai/e3-core 0.0.2-beta.5 → 0.0.2-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/README.md +25 -22
  2. package/dist/src/dataflow/api-compat.d.ts +90 -0
  3. package/dist/src/dataflow/api-compat.d.ts.map +1 -0
  4. package/dist/src/dataflow/api-compat.js +139 -0
  5. package/dist/src/dataflow/api-compat.js.map +1 -0
  6. package/dist/src/dataflow/index.d.ts +18 -0
  7. package/dist/src/dataflow/index.d.ts.map +1 -0
  8. package/dist/src/dataflow/index.js +23 -0
  9. package/dist/src/dataflow/index.js.map +1 -0
  10. package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts +76 -0
  11. package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts.map +1 -0
  12. package/dist/src/dataflow/orchestrator/LocalOrchestrator.js +729 -0
  13. package/dist/src/dataflow/orchestrator/LocalOrchestrator.js.map +1 -0
  14. package/dist/src/dataflow/orchestrator/index.d.ts +12 -0
  15. package/dist/src/dataflow/orchestrator/index.d.ts.map +1 -0
  16. package/dist/src/dataflow/orchestrator/index.js +12 -0
  17. package/dist/src/dataflow/orchestrator/index.js.map +1 -0
  18. package/dist/src/dataflow/orchestrator/interfaces.d.ts +163 -0
  19. package/dist/src/dataflow/orchestrator/interfaces.d.ts.map +1 -0
  20. package/dist/src/dataflow/orchestrator/interfaces.js +52 -0
  21. package/dist/src/dataflow/orchestrator/interfaces.js.map +1 -0
  22. package/dist/src/dataflow/state-store/FileStateStore.d.ts +67 -0
  23. package/dist/src/dataflow/state-store/FileStateStore.d.ts.map +1 -0
  24. package/dist/src/dataflow/state-store/FileStateStore.js +300 -0
  25. package/dist/src/dataflow/state-store/FileStateStore.js.map +1 -0
  26. package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts +42 -0
  27. package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts.map +1 -0
  28. package/dist/src/dataflow/state-store/InMemoryStateStore.js +229 -0
  29. package/dist/src/dataflow/state-store/InMemoryStateStore.js.map +1 -0
  30. package/dist/src/dataflow/state-store/index.d.ts +13 -0
  31. package/dist/src/dataflow/state-store/index.d.ts.map +1 -0
  32. package/dist/src/dataflow/state-store/index.js +13 -0
  33. package/dist/src/dataflow/state-store/index.js.map +1 -0
  34. package/dist/src/dataflow/state-store/interfaces.d.ts +159 -0
  35. package/dist/src/dataflow/state-store/interfaces.d.ts.map +1 -0
  36. package/dist/src/dataflow/state-store/interfaces.js +6 -0
  37. package/dist/src/dataflow/state-store/interfaces.js.map +1 -0
  38. package/dist/src/dataflow/steps.d.ts +222 -0
  39. package/dist/src/dataflow/steps.d.ts.map +1 -0
  40. package/dist/src/dataflow/steps.js +707 -0
  41. package/dist/src/dataflow/steps.js.map +1 -0
  42. package/dist/src/dataflow/types.d.ts +127 -0
  43. package/dist/src/dataflow/types.d.ts.map +1 -0
  44. package/dist/src/dataflow/types.js +7 -0
  45. package/dist/src/dataflow/types.js.map +1 -0
  46. package/dist/src/dataflow.d.ts +113 -38
  47. package/dist/src/dataflow.d.ts.map +1 -1
  48. package/dist/src/dataflow.js +269 -416
  49. package/dist/src/dataflow.js.map +1 -1
  50. package/dist/src/dataset-refs.d.ts +124 -0
  51. package/dist/src/dataset-refs.d.ts.map +1 -0
  52. package/dist/src/dataset-refs.js +319 -0
  53. package/dist/src/dataset-refs.js.map +1 -0
  54. package/dist/src/errors.d.ts +39 -9
  55. package/dist/src/errors.d.ts.map +1 -1
  56. package/dist/src/errors.js +51 -8
  57. package/dist/src/errors.js.map +1 -1
  58. package/dist/src/execution/LocalTaskRunner.d.ts +73 -0
  59. package/dist/src/execution/LocalTaskRunner.d.ts.map +1 -0
  60. package/dist/src/execution/LocalTaskRunner.js +399 -0
  61. package/dist/src/execution/LocalTaskRunner.js.map +1 -0
  62. package/dist/src/execution/MockTaskRunner.d.ts +49 -0
  63. package/dist/src/execution/MockTaskRunner.d.ts.map +1 -0
  64. package/dist/src/execution/MockTaskRunner.js +54 -0
  65. package/dist/src/execution/MockTaskRunner.js.map +1 -0
  66. package/dist/src/execution/index.d.ts +16 -0
  67. package/dist/src/execution/index.d.ts.map +1 -0
  68. package/dist/src/execution/index.js +8 -0
  69. package/dist/src/execution/index.js.map +1 -0
  70. package/dist/src/execution/interfaces.d.ts +246 -0
  71. package/dist/src/execution/interfaces.d.ts.map +1 -0
  72. package/dist/src/execution/interfaces.js +6 -0
  73. package/dist/src/execution/interfaces.js.map +1 -0
  74. package/dist/src/execution/processHelpers.d.ts +20 -0
  75. package/dist/src/execution/processHelpers.d.ts.map +1 -0
  76. package/dist/src/execution/processHelpers.js +62 -0
  77. package/dist/src/execution/processHelpers.js.map +1 -0
  78. package/dist/src/executions.d.ts +71 -104
  79. package/dist/src/executions.d.ts.map +1 -1
  80. package/dist/src/executions.js +110 -476
  81. package/dist/src/executions.js.map +1 -1
  82. package/dist/src/index.d.ts +20 -10
  83. package/dist/src/index.d.ts.map +1 -1
  84. package/dist/src/index.js +48 -18
  85. package/dist/src/index.js.map +1 -1
  86. package/dist/src/objects.d.ts +7 -53
  87. package/dist/src/objects.d.ts.map +1 -1
  88. package/dist/src/objects.js +13 -232
  89. package/dist/src/objects.js.map +1 -1
  90. package/dist/src/packages.d.ts +41 -14
  91. package/dist/src/packages.d.ts.map +1 -1
  92. package/dist/src/packages.js +145 -88
  93. package/dist/src/packages.js.map +1 -1
  94. package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts +35 -0
  95. package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts.map +1 -0
  96. package/dist/src/storage/in-memory/InMemoryRepoStore.js +107 -0
  97. package/dist/src/storage/in-memory/InMemoryRepoStore.js.map +1 -0
  98. package/dist/src/storage/in-memory/InMemoryStorage.d.ts +139 -0
  99. package/dist/src/storage/in-memory/InMemoryStorage.d.ts.map +1 -0
  100. package/dist/src/storage/in-memory/InMemoryStorage.js +439 -0
  101. package/dist/src/storage/in-memory/InMemoryStorage.js.map +1 -0
  102. package/dist/src/storage/in-memory/index.d.ts +12 -0
  103. package/dist/src/storage/in-memory/index.d.ts.map +1 -0
  104. package/dist/src/storage/in-memory/index.js +12 -0
  105. package/dist/src/storage/in-memory/index.js.map +1 -0
  106. package/dist/src/storage/index.d.ts +18 -0
  107. package/dist/src/storage/index.d.ts.map +1 -0
  108. package/dist/src/storage/index.js +10 -0
  109. package/dist/src/storage/index.js.map +1 -0
  110. package/dist/src/storage/interfaces.d.ts +581 -0
  111. package/dist/src/storage/interfaces.d.ts.map +1 -0
  112. package/dist/src/storage/interfaces.js +6 -0
  113. package/dist/src/storage/interfaces.js.map +1 -0
  114. package/dist/src/storage/local/LocalBackend.d.ts +56 -0
  115. package/dist/src/storage/local/LocalBackend.d.ts.map +1 -0
  116. package/dist/src/storage/local/LocalBackend.js +145 -0
  117. package/dist/src/storage/local/LocalBackend.js.map +1 -0
  118. package/dist/src/storage/local/LocalDatasetRefStore.d.ts +22 -0
  119. package/dist/src/storage/local/LocalDatasetRefStore.d.ts.map +1 -0
  120. package/dist/src/storage/local/LocalDatasetRefStore.js +118 -0
  121. package/dist/src/storage/local/LocalDatasetRefStore.js.map +1 -0
  122. package/dist/src/storage/local/LocalLockService.d.ts +111 -0
  123. package/dist/src/storage/local/LocalLockService.d.ts.map +1 -0
  124. package/dist/src/storage/local/LocalLockService.js +364 -0
  125. package/dist/src/storage/local/LocalLockService.js.map +1 -0
  126. package/dist/src/storage/local/LocalLogStore.d.ts +23 -0
  127. package/dist/src/storage/local/LocalLogStore.d.ts.map +1 -0
  128. package/dist/src/storage/local/LocalLogStore.js +66 -0
  129. package/dist/src/storage/local/LocalLogStore.js.map +1 -0
  130. package/dist/src/storage/local/LocalObjectStore.d.ts +55 -0
  131. package/dist/src/storage/local/LocalObjectStore.d.ts.map +1 -0
  132. package/dist/src/storage/local/LocalObjectStore.js +300 -0
  133. package/dist/src/storage/local/LocalObjectStore.js.map +1 -0
  134. package/dist/src/storage/local/LocalRefStore.d.ts +50 -0
  135. package/dist/src/storage/local/LocalRefStore.d.ts.map +1 -0
  136. package/dist/src/storage/local/LocalRefStore.js +337 -0
  137. package/dist/src/storage/local/LocalRefStore.js.map +1 -0
  138. package/dist/src/storage/local/LocalRepoStore.d.ts +55 -0
  139. package/dist/src/storage/local/LocalRepoStore.d.ts.map +1 -0
  140. package/dist/src/storage/local/LocalRepoStore.js +365 -0
  141. package/dist/src/storage/local/LocalRepoStore.js.map +1 -0
  142. package/dist/src/storage/local/gc.d.ts +92 -0
  143. package/dist/src/storage/local/gc.d.ts.map +1 -0
  144. package/dist/src/storage/local/gc.js +377 -0
  145. package/dist/src/storage/local/gc.js.map +1 -0
  146. package/dist/src/storage/local/index.d.ts +18 -0
  147. package/dist/src/storage/local/index.d.ts.map +1 -0
  148. package/dist/src/storage/local/index.js +18 -0
  149. package/dist/src/storage/local/index.js.map +1 -0
  150. package/dist/src/storage/local/localHelpers.d.ts +25 -0
  151. package/dist/src/storage/local/localHelpers.d.ts.map +1 -0
  152. package/dist/src/storage/local/localHelpers.js +69 -0
  153. package/dist/src/storage/local/localHelpers.js.map +1 -0
  154. package/dist/src/{repository.d.ts → storage/local/repository.d.ts} +8 -4
  155. package/dist/src/storage/local/repository.d.ts.map +1 -0
  156. package/dist/src/{repository.js → storage/local/repository.js} +31 -29
  157. package/dist/src/storage/local/repository.js.map +1 -0
  158. package/dist/src/tasks.d.ts +16 -10
  159. package/dist/src/tasks.d.ts.map +1 -1
  160. package/dist/src/tasks.js +35 -41
  161. package/dist/src/tasks.js.map +1 -1
  162. package/dist/src/test-helpers.d.ts +5 -4
  163. package/dist/src/test-helpers.d.ts.map +1 -1
  164. package/dist/src/test-helpers.js +9 -21
  165. package/dist/src/test-helpers.js.map +1 -1
  166. package/dist/src/transfer/InMemoryTransferBackend.d.ts +75 -0
  167. package/dist/src/transfer/InMemoryTransferBackend.d.ts.map +1 -0
  168. package/dist/src/transfer/InMemoryTransferBackend.js +211 -0
  169. package/dist/src/transfer/InMemoryTransferBackend.js.map +1 -0
  170. package/dist/src/transfer/index.d.ts +9 -0
  171. package/dist/src/transfer/index.d.ts.map +1 -0
  172. package/dist/src/transfer/index.js +11 -0
  173. package/dist/src/transfer/index.js.map +1 -0
  174. package/dist/src/transfer/interfaces.d.ts +103 -0
  175. package/dist/src/transfer/interfaces.d.ts.map +1 -0
  176. package/dist/src/transfer/interfaces.js +6 -0
  177. package/dist/src/transfer/interfaces.js.map +1 -0
  178. package/dist/src/transfer/process.d.ts +55 -0
  179. package/dist/src/transfer/process.d.ts.map +1 -0
  180. package/dist/src/transfer/process.js +144 -0
  181. package/dist/src/transfer/process.js.map +1 -0
  182. package/dist/src/transfer/types.d.ts +106 -0
  183. package/dist/src/transfer/types.d.ts.map +1 -0
  184. package/dist/src/transfer/types.js +61 -0
  185. package/dist/src/transfer/types.js.map +1 -0
  186. package/dist/src/trees.d.ts +147 -59
  187. package/dist/src/trees.d.ts.map +1 -1
  188. package/dist/src/trees.js +372 -419
  189. package/dist/src/trees.js.map +1 -1
  190. package/dist/src/uuid.d.ts +26 -0
  191. package/dist/src/uuid.d.ts.map +1 -0
  192. package/dist/src/uuid.js +80 -0
  193. package/dist/src/uuid.js.map +1 -0
  194. package/dist/src/workspaceStatus.d.ts +6 -4
  195. package/dist/src/workspaceStatus.d.ts.map +1 -1
  196. package/dist/src/workspaceStatus.js +46 -60
  197. package/dist/src/workspaceStatus.js.map +1 -1
  198. package/dist/src/workspaces.d.ts +46 -47
  199. package/dist/src/workspaces.d.ts.map +1 -1
  200. package/dist/src/workspaces.js +281 -221
  201. package/dist/src/workspaces.js.map +1 -1
  202. package/package.json +4 -4
  203. package/dist/src/gc.d.ts +0 -54
  204. package/dist/src/gc.d.ts.map +0 -1
  205. package/dist/src/gc.js +0 -233
  206. package/dist/src/gc.js.map +0 -1
  207. package/dist/src/repository.d.ts.map +0 -1
  208. package/dist/src/repository.js.map +0 -1
  209. package/dist/src/workspaceLock.d.ts +0 -67
  210. package/dist/src/workspaceLock.d.ts.map +0 -1
  211. package/dist/src/workspaceLock.js +0 -217
  212. package/dist/src/workspaceLock.js.map +0 -1
@@ -0,0 +1,729 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ /**
6
+ * Local in-process dataflow orchestrator.
7
+ *
8
+ * Executes dataflow using an async loop with step functions.
9
+ * This is the default orchestrator for CLI and local API server usage.
10
+ *
11
+ * Supports reactive execution: after each task completes, checks for
12
+ * root input changes. If inputs changed, affected tasks are invalidated
13
+ * and re-executed. Version vector consistency checks defer tasks whose
14
+ * inputs have conflicting provenance (diamond dependency protection).
15
+ */
16
+ import { decodeBeast2For, encodeBeast2For, variant } from '@elaraai/east';
17
+ import { WorkspaceStateType } from '@elaraai/e3-types';
18
+ import { taskExecute } from '../../execution/LocalTaskRunner.js';
19
+ import { WorkspaceLockError, DataflowAbortedError, DataflowError } from '../../errors.js';
20
+ import { uuidv7 } from '../../uuid.js';
21
+ import { stateToStatus } from './interfaces.js';
22
+ import { stepInitialize, stepGetReady, stepPrepareTask, stepTaskStarted, stepTaskCompleted, stepTaskFailed, stepTasksSkipped, stepIsComplete, stepFinalize, stepCancel, stepApplyTreeUpdate, stepDetectInputChanges, stepInvalidateTasks, stepCheckVersionConsistency, } from '../steps.js';
23
+ // =============================================================================
24
+ // Async Mutex for State Mutations
25
+ // =============================================================================
26
+ /**
27
+ * Simple async mutex to serialize state mutations.
28
+ *
29
+ * When multiple tasks complete concurrently, their `.then()` callbacks
30
+ * mutate shared DataflowExecutionState. Between `await` points
31
+ * (stepApplyTreeUpdate, handleInputChanges), another callback can run
32
+ * and corrupt counters/version vectors. This mutex ensures only one
33
+ * state mutation runs at a time while task execution itself runs in parallel.
34
+ */
35
+ class AsyncMutex {
36
+ queue = [];
37
+ locked = false;
38
+ /**
39
+ * Acquire the mutex, execute the callback, then release.
40
+ * If the mutex is already held, waits until it's available.
41
+ */
42
+ async runExclusive(fn) {
43
+ await this.acquire();
44
+ try {
45
+ return await fn();
46
+ }
47
+ finally {
48
+ this.release();
49
+ }
50
+ }
51
+ acquire() {
52
+ return new Promise((resolve) => {
53
+ if (!this.locked) {
54
+ this.locked = true;
55
+ resolve();
56
+ }
57
+ else {
58
+ this.queue.push(resolve);
59
+ }
60
+ });
61
+ }
62
+ release() {
63
+ const next = this.queue.shift();
64
+ if (next) {
65
+ next();
66
+ }
67
+ else {
68
+ this.locked = false;
69
+ }
70
+ }
71
+ }
72
+ /**
73
+ * Local orchestrator for in-process dataflow execution.
74
+ *
75
+ * @remarks
76
+ * - Uses step functions for each operation
77
+ * - Per-dataset ref writes are atomic and independent (no mutex needed)
78
+ * - Supports AbortSignal for cancellation
79
+ * - Persists state through the provided state store
80
+ * - Reactive: detects input changes after each task, invalidates and
81
+ * re-executes affected tasks until fixpoint
82
+ */
83
+ export class LocalOrchestrator {
84
+ stateStore;
85
+ executions = new Map();
86
+ /**
87
+ * Create a new LocalOrchestrator.
88
+ *
89
+ * @param stateStore - Optional state store for persistence.
90
+ * If not provided, state is only kept in memory.
91
+ */
92
+ constructor(stateStore) {
93
+ this.stateStore = stateStore;
94
+ }
95
+ async start(storage, repo, workspace, options = {}) {
96
+ // Acquire locks if not provided externally.
97
+ // Dual-lock model:
98
+ // - Shared lock on workspace (allows concurrent e3 set)
99
+ // - Exclusive lock on workspace#dataflow (prevents concurrent starts)
100
+ const externalLock = !!options.lock;
101
+ let sharedLock = null;
102
+ let dataflowLock = null;
103
+ if (externalLock) {
104
+ // Caller's lock serves as shared workspace lock
105
+ sharedLock = options.lock;
106
+ // Still acquire exclusive dataflow lock (prevents concurrent starts)
107
+ dataflowLock = await storage.locks.acquire(repo, `${workspace}#dataflow`, variant('dataflow', null));
108
+ if (!dataflowLock) {
109
+ throw new WorkspaceLockError(workspace);
110
+ }
111
+ }
112
+ else {
113
+ // Acquire shared workspace lock first (coexists with e3 set)
114
+ sharedLock = await storage.locks.acquire(repo, workspace, variant('dataflow', null), { mode: 'shared' });
115
+ if (!sharedLock) {
116
+ throw new WorkspaceLockError(workspace);
117
+ }
118
+ // Acquire exclusive dataflow lock (prevents concurrent starts)
119
+ dataflowLock = await storage.locks.acquire(repo, `${workspace}#dataflow`, variant('dataflow', null));
120
+ if (!dataflowLock) {
121
+ await sharedLock.release();
122
+ throw new WorkspaceLockError(workspace);
123
+ }
124
+ }
125
+ try {
126
+ // Get next execution ID from state store if available
127
+ const executionId = this.stateStore
128
+ ? await this.stateStore.nextExecutionId(repo, workspace)
129
+ : String(Date.now()); // Fallback to timestamp if no state store
130
+ // Initialize execution state
131
+ const { state, readyTasks: _ } = await stepInitialize(storage, repo, workspace, executionId, {
132
+ concurrency: options.concurrency,
133
+ force: options.force,
134
+ filter: options.filter,
135
+ });
136
+ // Persist initial state
137
+ if (this.stateStore) {
138
+ await this.stateStore.create(state);
139
+ }
140
+ // Create completion promise
141
+ let resolveCompletion;
142
+ let rejectCompletion;
143
+ const completionPromise = new Promise((resolve, reject) => {
144
+ resolveCompletion = resolve;
145
+ rejectCompletion = reject;
146
+ });
147
+ // Create running execution state
148
+ const execution = {
149
+ state,
150
+ lock: dataflowLock,
151
+ sharedLock,
152
+ externalLock,
153
+ options,
154
+ aborted: false,
155
+ runningTasks: new Map(),
156
+ mutex: new AsyncMutex(),
157
+ runId: uuidv7(),
158
+ taskExecutions: new Map(),
159
+ completionPromise,
160
+ resolveCompletion,
161
+ rejectCompletion,
162
+ };
163
+ const key = this.executionKey(repo, workspace, executionId);
164
+ this.executions.set(key, execution);
165
+ // Listen for abort signal to persist cancellation immediately.
166
+ if (options.signal) {
167
+ const onAbort = () => {
168
+ execution.aborted = true;
169
+ if (this.stateStore) {
170
+ void this.stateStore.updateStatus(repo, workspace, executionId, 'cancelled', { error: 'Execution was cancelled' }).catch(() => { });
171
+ }
172
+ };
173
+ options.signal.addEventListener('abort', onAbort, { once: true });
174
+ execution.abortCleanup = () => options.signal.removeEventListener('abort', onAbort);
175
+ }
176
+ // Start the execution loop (non-blocking)
177
+ this.runExecutionLoop(storage, repo, execution).catch(err => {
178
+ rejectCompletion(err);
179
+ });
180
+ return { id: executionId, repo, workspace };
181
+ }
182
+ catch (err) {
183
+ // Always release the dataflow lock on initialization failure
184
+ await dataflowLock.release();
185
+ // Release shared workspace lock only if we acquired it (not external)
186
+ if (!externalLock && sharedLock) {
187
+ await sharedLock.release();
188
+ }
189
+ throw err;
190
+ }
191
+ }
192
+ async wait(handle) {
193
+ const key = this.executionKey(handle.repo, handle.workspace, handle.id);
194
+ const execution = this.executions.get(key);
195
+ if (!execution) {
196
+ throw new Error(`Execution ${handle.id} not found for workspace '${handle.workspace}'`);
197
+ }
198
+ return execution.completionPromise;
199
+ }
200
+ async getStatus(handle) {
201
+ const key = this.executionKey(handle.repo, handle.workspace, handle.id);
202
+ const execution = this.executions.get(key);
203
+ if (!execution) {
204
+ // Try to read from state store
205
+ if (this.stateStore) {
206
+ const state = await this.stateStore.read(handle.repo, handle.workspace, handle.id);
207
+ if (state) {
208
+ return stateToStatus(state);
209
+ }
210
+ }
211
+ throw new Error(`Execution ${handle.id} not found for workspace '${handle.workspace}'`);
212
+ }
213
+ return stateToStatus(execution.state);
214
+ }
215
+ async cancel(handle) {
216
+ const key = this.executionKey(handle.repo, handle.workspace, handle.id);
217
+ const execution = this.executions.get(key);
218
+ if (!execution) {
219
+ throw new Error(`Execution ${handle.id} not found for workspace '${handle.workspace}'`);
220
+ }
221
+ execution.aborted = true;
222
+ if (this.stateStore) {
223
+ await this.stateStore.updateStatus(handle.repo, handle.workspace, handle.id, 'cancelled', { error: 'Execution was cancelled' });
224
+ }
225
+ }
226
+ async getEvents(handle, sinceSeq) {
227
+ if (!this.stateStore) {
228
+ return [];
229
+ }
230
+ return this.stateStore.getEventsSince(handle.repo, handle.workspace, handle.id, sinceSeq);
231
+ }
232
+ /**
233
+ * Main execution loop with reactive fixpoint.
234
+ *
235
+ * After each task completes, checks for input changes and invalidates
236
+ * affected tasks. Uses version vector consistency checks to defer tasks
237
+ * whose inputs have conflicting provenance. Execution continues until
238
+ * fixpoint (no more ready, running, or deferred tasks).
239
+ */
240
+ async runExecutionLoop(storage, repo, execution) {
241
+ const { state, options } = execution;
242
+ try {
243
+ let hasFailure = false;
244
+ // Read workspace state for DataflowRun recording
245
+ const wsData = await storage.refs.workspaceRead(repo, state.workspace);
246
+ const wsDecoder = decodeBeast2For(WorkspaceStateType);
247
+ const wsState = wsData && wsData.length > 0 ? wsDecoder(wsData) : null;
248
+ // Cache structure for the entire execution (immutable during execution)
249
+ const structure = wsState ? await this.readStructure(storage, repo, wsState.packageHash) : null;
250
+ // Write initial DataflowRun record
251
+ if (wsState) {
252
+ const initialRun = {
253
+ runId: execution.runId,
254
+ workspaceName: state.workspace,
255
+ packageRef: `${wsState.packageName}@${wsState.packageVersion}`,
256
+ startedAt: state.startedAt,
257
+ completedAt: variant('none', null),
258
+ status: variant('running', {}),
259
+ inputVersions: new Map(state.inputSnapshot),
260
+ outputVersions: variant('none', null),
261
+ taskExecutions: new Map(),
262
+ summary: {
263
+ total: BigInt(state.tasks.size),
264
+ completed: 0n,
265
+ cached: 0n,
266
+ failed: 0n,
267
+ skipped: 0n,
268
+ reexecuted: 0n,
269
+ },
270
+ };
271
+ await storage.refs.dataflowRunWrite(repo, state.workspace, initialRun);
272
+ }
273
+ // Check for abort signal from options
274
+ const checkAborted = () => {
275
+ if (options.signal?.aborted && !execution.aborted) {
276
+ execution.aborted = true;
277
+ }
278
+ return execution.aborted;
279
+ };
280
+ while (true) {
281
+ // Check if we're done
282
+ if (execution.runningTasks.size === 0 && stepIsComplete(state)) {
283
+ break;
284
+ }
285
+ // Get ready tasks
286
+ const readyTasks = stepGetReady(state);
287
+ // Track whether any task was completed synchronously (via cache hit)
288
+ // in this iteration. If so, new downstream tasks may have become ready
289
+ // that aren't in the stale readyTasks array.
290
+ let hadSyncCompletion = false;
291
+ // Launch tasks up to concurrency limit if no failure and not aborted
292
+ const concurrencyLimit = Number(state.concurrency);
293
+ while (!hasFailure &&
294
+ !checkAborted() &&
295
+ readyTasks.length > 0 &&
296
+ execution.runningTasks.size < concurrencyLimit) {
297
+ const taskName = readyTasks.shift();
298
+ const taskState = state.tasks.get(taskName);
299
+ if (!taskState || taskState.status === 'in_progress' || taskState.status === 'completed') {
300
+ continue;
301
+ }
302
+ // Version vector consistency check before launching
303
+ const vvCheck = stepCheckVersionConsistency(state, taskName);
304
+ if (!vvCheck.consistent) {
305
+ // Defer: inputs have inconsistent versions of the same root input
306
+ const ts = state.tasks.get(taskName);
307
+ if (ts)
308
+ ts.status = 'deferred';
309
+ // Emit task_deferred event
310
+ const mutableState = state;
311
+ mutableState.eventSeq = state.eventSeq + 1n;
312
+ const deferEvent = variant('task_deferred', {
313
+ seq: mutableState.eventSeq,
314
+ timestamp: new Date(),
315
+ task: taskName,
316
+ conflictPath: vvCheck.conflictPath,
317
+ });
318
+ mutableState.events.push(deferEvent);
319
+ options.onTaskDeferred?.(taskName, vvCheck.conflictPath);
320
+ continue;
321
+ }
322
+ // Prepare task (resolve inputs, check cache)
323
+ const prepared = await stepPrepareTask(storage, state, taskName);
324
+ // Check cache
325
+ if (prepared.cachedOutputHash !== null) {
326
+ hadSyncCompletion = true;
327
+ // Cache hit — wrap in mutex to serialize with concurrent .then() callbacks
328
+ await execution.mutex.runExclusive(async () => {
329
+ // Write ref with merged VV and update state
330
+ await stepApplyTreeUpdate(storage, repo, state.workspace, prepared.outputPath, prepared.cachedOutputHash, vvCheck.mergedVV);
331
+ stepTaskCompleted(state, taskName, prepared.cachedOutputHash, true, 0);
332
+ // Track task execution for DataflowRun
333
+ const existingCached = execution.taskExecutions.get(taskName);
334
+ execution.taskExecutions.set(taskName, {
335
+ executionId: state.id,
336
+ cached: true,
337
+ outputVersions: new Map(vvCheck.mergedVV),
338
+ executionCount: (existingCached?.executionCount ?? 0n) + 1n,
339
+ });
340
+ // Notify callback
341
+ options.onTaskComplete?.({
342
+ name: taskName,
343
+ cached: true,
344
+ state: 'success',
345
+ duration: 0,
346
+ });
347
+ // Detect input changes after cached result
348
+ await this.handleInputChanges(storage, state, options, structure);
349
+ // Update state store
350
+ await this.persistState(execution, state);
351
+ });
352
+ continue;
353
+ }
354
+ // Mark as started (event added by step function)
355
+ stepTaskStarted(state, taskName);
356
+ await this.persistState(execution, state);
357
+ options.onTaskStart?.(taskName);
358
+ // Launch task execution
359
+ const taskPromise = this.executeTask(storage, repo, execution, taskName, prepared).then(result => execution.mutex.runExclusive(async () => {
360
+ // Handle task completion
361
+ if (result.state === 'success') {
362
+ // Check if task's inputs changed during execution by comparing
363
+ // the launch-time merged VV (captured in closure) against current.
364
+ // handleInputChanges may have updated root input VVs while this
365
+ // task was in_progress, making its result stale.
366
+ const launchMergedVV = vvCheck.mergedVV;
367
+ const currentVVCheck = stepCheckVersionConsistency(state, taskName);
368
+ const inputsStale = !currentVVCheck.consistent || (() => {
369
+ const current = currentVVCheck.mergedVV;
370
+ if (launchMergedVV.size !== current.size)
371
+ return true;
372
+ for (const [key, value] of launchMergedVV) {
373
+ if (current.get(key) !== value)
374
+ return true;
375
+ }
376
+ return false;
377
+ })();
378
+ if (inputsStale) {
379
+ // Task computed with stale inputs — discard result, reset to pending.
380
+ // The reactive loop will re-execute it with the updated inputs.
381
+ const ts = state.tasks.get(taskName);
382
+ if (ts)
383
+ ts.status = 'pending';
384
+ const mutableState = state;
385
+ mutableState.eventSeq = state.eventSeq + 1n;
386
+ const invalidEvent = variant('task_invalidated', {
387
+ seq: mutableState.eventSeq,
388
+ timestamp: new Date(),
389
+ task: taskName,
390
+ reason: 'inputs changed during execution',
391
+ });
392
+ mutableState.events.push(invalidEvent);
393
+ mutableState.reexecuted = state.reexecuted + 1n;
394
+ options.onTaskInvalidated?.(taskName, 'inputs changed during execution');
395
+ }
396
+ else {
397
+ // Use launch-time VV for the output — it reflects what the task
398
+ // actually consumed, not what state.versionVectors says now.
399
+ const mergedVV = launchMergedVV;
400
+ if (result.outputHash) {
401
+ // Write output ref with merged VV
402
+ await stepApplyTreeUpdate(storage, repo, state.workspace, prepared.outputPath, result.outputHash, mergedVV);
403
+ }
404
+ stepTaskCompleted(state, taskName, result.outputHash ?? '', result.cached, result.duration);
405
+ // Track task execution for DataflowRun
406
+ const existing = execution.taskExecutions.get(taskName);
407
+ execution.taskExecutions.set(taskName, {
408
+ executionId: result.executionId ?? state.id,
409
+ cached: result.cached,
410
+ outputVersions: new Map(mergedVV),
411
+ executionCount: (existing?.executionCount ?? 0n) + 1n,
412
+ });
413
+ options.onTaskComplete?.({
414
+ name: taskName,
415
+ cached: result.cached,
416
+ state: 'success',
417
+ duration: result.duration,
418
+ });
419
+ }
420
+ // Detect input changes after task completion
421
+ await this.handleInputChanges(storage, state, options, structure);
422
+ }
423
+ else {
424
+ hasFailure = true;
425
+ const { result: failedResult } = stepTaskFailed(state, taskName, result.error, result.exitCode, result.duration);
426
+ options.onTaskComplete?.({
427
+ name: taskName,
428
+ cached: false,
429
+ state: result.state === 'failed' ? 'failed' : 'error',
430
+ error: result.error,
431
+ exitCode: result.exitCode,
432
+ duration: result.duration,
433
+ });
434
+ // Skip dependents (events added by step function)
435
+ const skipEvents = stepTasksSkipped(state, failedResult.toSkip, taskName);
436
+ for (const skipEvent of skipEvents) {
437
+ if (skipEvent.type === 'task_skipped') {
438
+ options.onTaskComplete?.({
439
+ name: skipEvent.value.task,
440
+ cached: false,
441
+ state: 'skipped',
442
+ duration: 0,
443
+ });
444
+ }
445
+ }
446
+ }
447
+ // Update state store
448
+ await this.persistState(execution, state);
449
+ })).finally(() => {
450
+ execution.runningTasks.delete(taskName);
451
+ });
452
+ execution.runningTasks.set(taskName, taskPromise);
453
+ }
454
+ // Wait for at least one task to complete if we can't launch more
455
+ if (execution.runningTasks.size > 0) {
456
+ await Promise.race(execution.runningTasks.values());
457
+ }
458
+ else if (hadSyncCompletion) {
459
+ // A cached task completed synchronously, which may have made new
460
+ // downstream tasks ready. Continue to re-check at the top of the loop.
461
+ continue;
462
+ }
463
+ else if (readyTasks.length === 0 || checkAborted() || hasFailure) {
464
+ break;
465
+ }
466
+ }
467
+ // Wait for any remaining tasks
468
+ if (execution.runningTasks.size > 0) {
469
+ await Promise.all(execution.runningTasks.values());
470
+ }
471
+ // Check for stuck state: non-terminal tasks remain but none are ready or running.
472
+ // When a filter is active, only the filtered task is relevant — non-filtered
473
+ // tasks are expected to remain pending.
474
+ const filterValue = state.filter.type === 'some' ? state.filter.value : null;
475
+ const stuckTasks = [...state.tasks.entries()]
476
+ .filter(([name, ts]) => {
477
+ if (ts.status !== 'pending' && ts.status !== 'ready' && ts.status !== 'deferred') {
478
+ return false;
479
+ }
480
+ // When a filter is active, non-filtered tasks staying pending is expected
481
+ if (filterValue !== null && name !== filterValue) {
482
+ return false;
483
+ }
484
+ return true;
485
+ })
486
+ .map(([name, ts]) => `${name} (${ts.status})`)
487
+ .join(', ');
488
+ if (stuckTasks.length > 0 && !checkAborted() && !hasFailure) {
489
+ throw new DataflowError(`Dataflow stuck: ${stuckTasks}`);
490
+ }
491
+ // Check for abort one final time
492
+ if (checkAborted()) {
493
+ stepCancel(state, 'Execution was aborted');
494
+ if (this.stateStore) {
495
+ await this.stateStore.update(state);
496
+ }
497
+ // Write cancelled DataflowRun record
498
+ if (wsState) {
499
+ const cancelledRun = {
500
+ runId: execution.runId,
501
+ workspaceName: state.workspace,
502
+ packageRef: `${wsState.packageName}@${wsState.packageVersion}`,
503
+ startedAt: state.startedAt,
504
+ completedAt: variant('some', new Date()),
505
+ status: variant('cancelled', {}),
506
+ inputVersions: new Map(state.inputSnapshot),
507
+ outputVersions: variant('some', this.buildOutputVersions(state)),
508
+ taskExecutions: new Map(execution.taskExecutions),
509
+ summary: {
510
+ total: BigInt(state.tasks.size),
511
+ completed: state.executed + state.cached,
512
+ cached: state.cached,
513
+ failed: state.failed,
514
+ skipped: state.skipped,
515
+ reexecuted: state.reexecuted,
516
+ },
517
+ };
518
+ await storage.refs.dataflowRunWrite(repo, state.workspace, cancelledRun);
519
+ }
520
+ // Build partial results for abort error
521
+ const partialResults = this.buildPartialResults(state);
522
+ throw new DataflowAbortedError(partialResults);
523
+ }
524
+ // Finalize (event added by step function)
525
+ const { result } = stepFinalize(state, execution.runId);
526
+ if (this.stateStore) {
527
+ await this.stateStore.update(state);
528
+ }
529
+ // Write final DataflowRun record
530
+ if (wsState) {
531
+ let finalStatus;
532
+ if (!result.success) {
533
+ // Find the failed task for the error record
534
+ const failedTaskEntry = [...state.tasks.entries()]
535
+ .find(([, ts]) => ts.status === 'failed');
536
+ const failedTaskName = failedTaskEntry?.[0] ?? 'unknown';
537
+ const failedError = failedTaskEntry?.[1].error.type === 'some'
538
+ ? failedTaskEntry[1].error.value
539
+ : 'Task failed';
540
+ finalStatus = variant('failed', {
541
+ failedTask: failedTaskName,
542
+ error: failedError,
543
+ });
544
+ }
545
+ else {
546
+ finalStatus = variant('completed', {});
547
+ }
548
+ const finalRun = {
549
+ runId: execution.runId,
550
+ workspaceName: state.workspace,
551
+ packageRef: `${wsState.packageName}@${wsState.packageVersion}`,
552
+ startedAt: state.startedAt,
553
+ completedAt: variant('some', new Date()),
554
+ status: finalStatus,
555
+ inputVersions: new Map(state.inputSnapshot),
556
+ outputVersions: variant('some', this.buildOutputVersions(state)),
557
+ taskExecutions: new Map(execution.taskExecutions),
558
+ summary: {
559
+ total: BigInt(state.tasks.size),
560
+ completed: state.executed + state.cached,
561
+ cached: state.cached,
562
+ failed: state.failed,
563
+ skipped: state.skipped,
564
+ reexecuted: state.reexecuted,
565
+ },
566
+ };
567
+ await storage.refs.dataflowRunWrite(repo, state.workspace, finalRun);
568
+ // Update workspace state with currentRunId on success
569
+ if (result.success) {
570
+ const currentWsData = await storage.refs.workspaceRead(repo, state.workspace);
571
+ if (currentWsData && currentWsData.length > 0) {
572
+ const currentWsState = wsDecoder(currentWsData);
573
+ const updatedWsState = {
574
+ ...currentWsState,
575
+ currentRunId: variant('some', execution.runId),
576
+ };
577
+ const encoder = encodeBeast2For(WorkspaceStateType);
578
+ await storage.refs.workspaceWrite(repo, state.workspace, encoder(updatedWsState));
579
+ }
580
+ }
581
+ }
582
+ execution.resolveCompletion(result);
583
+ }
584
+ finally {
585
+ // Remove abort listener to avoid leaking execution object
586
+ execution.abortCleanup?.();
587
+ // Always release the dataflow lock (we always acquire it)
588
+ await execution.lock.release();
589
+ // Release shared workspace lock only if we acquired it (not external)
590
+ if (!execution.externalLock && execution.sharedLock) {
591
+ await execution.sharedLock.release();
592
+ }
593
+ // Clean up execution state
594
+ const key = this.executionKey(repo, state.workspace, state.id);
595
+ this.executions.delete(key);
596
+ }
597
+ }
598
+ /**
599
+ * Detect input changes and invalidate affected tasks.
600
+ *
601
+ * Called after each task completion to implement the reactive loop.
602
+ */
603
+ async handleInputChanges(storage, state, options, structure) {
604
+ const { changes, events: changeEvents } = await stepDetectInputChanges(storage, state, structure);
605
+ // Notify via callbacks
606
+ for (const evt of changeEvents) {
607
+ if (evt.type === 'input_changed') {
608
+ options.onInputChanged?.(evt.value.path, evt.value.previousHash, evt.value.newHash);
609
+ }
610
+ }
611
+ if (changes.length > 0) {
612
+ const mutableState = state;
613
+ const { invalidated, events: invEvents } = stepInvalidateTasks(state, changes);
614
+ // Track re-executions (tasks that were completed and are now invalidated)
615
+ mutableState.reexecuted = state.reexecuted + BigInt(invalidated.length);
616
+ for (const evt of invEvents) {
617
+ if (evt.type === 'task_invalidated') {
618
+ options.onTaskInvalidated?.(evt.value.task, evt.value.reason);
619
+ }
620
+ }
621
+ }
622
+ }
623
+ /**
624
+ * Execute a single task.
625
+ */
626
+ async executeTask(storage, repo, execution, taskName, prepared) {
627
+ const { options } = execution;
628
+ const startTime = Date.now();
629
+ const execOptions = {
630
+ force: execution.state.force,
631
+ signal: options.signal,
632
+ onStdout: options.onStdout ? (data) => options.onStdout(taskName, data) : undefined,
633
+ onStderr: options.onStderr ? (data) => options.onStderr(taskName, data) : undefined,
634
+ };
635
+ // Use provided runner if available, otherwise call taskExecute directly
636
+ if (options.runner) {
637
+ const result = await options.runner.execute(storage, prepared.taskHash, prepared.inputHashes, execOptions);
638
+ return {
639
+ state: result.state,
640
+ cached: result.cached,
641
+ outputHash: result.outputHash,
642
+ executionId: result.executionId,
643
+ exitCode: result.exitCode,
644
+ error: result.error,
645
+ duration: Date.now() - startTime,
646
+ };
647
+ }
648
+ else {
649
+ const result = await taskExecute(storage, repo, prepared.taskHash, prepared.inputHashes, execOptions);
650
+ return {
651
+ state: result.state,
652
+ cached: result.cached,
653
+ outputHash: result.outputHash ?? undefined,
654
+ executionId: result.executionId,
655
+ exitCode: result.exitCode ?? undefined,
656
+ error: result.error ?? undefined,
657
+ duration: Date.now() - startTime,
658
+ };
659
+ }
660
+ }
661
+ /**
662
+ * Build partial results for abort error.
663
+ */
664
+ buildPartialResults(state) {
665
+ const results = [];
666
+ for (const [name, taskState] of state.tasks) {
667
+ if (taskState.status === 'completed' || taskState.status === 'failed' || taskState.status === 'skipped') {
668
+ // Extract values from Option types
669
+ const cached = taskState.cached.type === 'some' ? taskState.cached.value : false;
670
+ const error = taskState.error.type === 'some' ? taskState.error.value : undefined;
671
+ const exitCode = taskState.exitCode.type === 'some' ? Number(taskState.exitCode.value) : undefined;
672
+ const duration = taskState.duration.type === 'some' ? Number(taskState.duration.value) : 0;
673
+ results.push({
674
+ name,
675
+ cached,
676
+ state: taskState.status === 'completed' ? 'success' : taskState.status,
677
+ error,
678
+ exitCode,
679
+ duration,
680
+ });
681
+ }
682
+ }
683
+ return results;
684
+ }
685
+ /**
686
+ * Build output versions map from completed task states.
687
+ */
688
+ buildOutputVersions(state) {
689
+ const outputVersions = new Map();
690
+ const graph = state.graph.type === 'some' ? state.graph.value : null;
691
+ if (graph) {
692
+ for (const task of graph.tasks) {
693
+ const ts = state.tasks.get(task.name);
694
+ if (ts && ts.outputHash.type === 'some') {
695
+ outputVersions.set(task.output, ts.outputHash.value);
696
+ }
697
+ }
698
+ }
699
+ return outputVersions;
700
+ }
701
+ /**
702
+ * Read workspace structure from storage.
703
+ */
704
+ async readStructure(storage, repo, packageHash) {
705
+ const { PackageObjectType } = await import('@elaraai/e3-types');
706
+ const pkgData = await storage.objects.read(repo, packageHash);
707
+ const pkgDecoder = decodeBeast2For(PackageObjectType);
708
+ const pkgObject = pkgDecoder(Buffer.from(pkgData));
709
+ return pkgObject.data.structure;
710
+ }
711
+ /**
712
+ * Persist state, skipping the write when execution has been aborted
713
+ * and the state doesn't yet reflect cancellation (defense-in-depth).
714
+ */
715
+ async persistState(execution, state) {
716
+ if (!this.stateStore)
717
+ return;
718
+ if (execution.aborted && state.status !== 'cancelled')
719
+ return;
720
+ await this.stateStore.update(state);
721
+ }
722
+ /**
723
+ * Generate unique key for an execution.
724
+ */
725
+ executionKey(repo, workspace, id) {
726
+ return `${repo}::${workspace}:${id}`;
727
+ }
728
+ }
729
+ //# sourceMappingURL=LocalOrchestrator.js.map