@elaraai/e3-core 0.0.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +50 -0
- package/README.md +76 -0
- package/dist/src/dataflow.d.ts +96 -0
- package/dist/src/dataflow.d.ts.map +1 -0
- package/dist/src/dataflow.js +433 -0
- package/dist/src/dataflow.js.map +1 -0
- package/dist/src/errors.d.ts +87 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +178 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/executions.d.ts +163 -0
- package/dist/src/executions.d.ts.map +1 -0
- package/dist/src/executions.js +535 -0
- package/dist/src/executions.js.map +1 -0
- package/dist/src/formats.d.ts +38 -0
- package/dist/src/formats.d.ts.map +1 -0
- package/dist/src/formats.js +115 -0
- package/dist/src/formats.js.map +1 -0
- package/dist/src/gc.d.ts +54 -0
- package/dist/src/gc.d.ts.map +1 -0
- package/dist/src/gc.js +232 -0
- package/dist/src/gc.js.map +1 -0
- package/dist/src/index.d.ts +23 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +68 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/objects.d.ts +62 -0
- package/dist/src/objects.d.ts.map +1 -0
- package/dist/src/objects.js +245 -0
- package/dist/src/objects.js.map +1 -0
- package/dist/src/packages.d.ts +85 -0
- package/dist/src/packages.d.ts.map +1 -0
- package/dist/src/packages.js +355 -0
- package/dist/src/packages.js.map +1 -0
- package/dist/src/repository.d.ts +38 -0
- package/dist/src/repository.d.ts.map +1 -0
- package/dist/src/repository.js +103 -0
- package/dist/src/repository.js.map +1 -0
- package/dist/src/tasks.d.ts +63 -0
- package/dist/src/tasks.d.ts.map +1 -0
- package/dist/src/tasks.js +145 -0
- package/dist/src/tasks.js.map +1 -0
- package/dist/src/test-helpers.d.ts +44 -0
- package/dist/src/test-helpers.d.ts.map +1 -0
- package/dist/src/test-helpers.js +141 -0
- package/dist/src/test-helpers.js.map +1 -0
- package/dist/src/trees.d.ts +156 -0
- package/dist/src/trees.d.ts.map +1 -0
- package/dist/src/trees.js +607 -0
- package/dist/src/trees.js.map +1 -0
- package/dist/src/workspaces.d.ts +116 -0
- package/dist/src/workspaces.d.ts.map +1 -0
- package/dist/src/workspaces.js +356 -0
- package/dist/src/workspaces.js.map +1 -0
- package/package.json +50 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Elara AI Pty Ltd
|
|
4
|
+
|
|
5
|
+
## License
|
|
6
|
+
|
|
7
|
+
**Licensor:** Elara AI Pty Ltd
|
|
8
|
+
|
|
9
|
+
**Licensed Work:** @elaraai/e3-core
|
|
10
|
+
|
|
11
|
+
**Change Date:** Four years from the date of each release
|
|
12
|
+
|
|
13
|
+
**Change License:** AGPL-3.0
|
|
14
|
+
|
|
15
|
+
## Terms
|
|
16
|
+
|
|
17
|
+
The Licensed Work is provided under the terms of the Business Source License 1.1 as detailed below.
|
|
18
|
+
|
|
19
|
+
### Grant of Rights
|
|
20
|
+
|
|
21
|
+
The Licensor grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work.
|
|
22
|
+
|
|
23
|
+
### Production Use Limitation
|
|
24
|
+
|
|
25
|
+
**"Production Use"** means any use by or on behalf of a for-profit entity, other than for evaluation, testing, or development purposes.
|
|
26
|
+
|
|
27
|
+
Production Use requires a separate commercial license from the Licensor.
|
|
28
|
+
|
|
29
|
+
### Change Date
|
|
30
|
+
|
|
31
|
+
On the Change Date (four years after each release), the Licensed Work will be made available under the Change License (AGPL-3.0).
|
|
32
|
+
|
|
33
|
+
### No Warranty
|
|
34
|
+
|
|
35
|
+
THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. THE LICENSOR DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
|
36
|
+
|
|
37
|
+
## Commercial Licensing
|
|
38
|
+
|
|
39
|
+
To obtain a commercial license for Production Use, contact:
|
|
40
|
+
|
|
41
|
+
**Email:** support@elara.ai
|
|
42
|
+
**Website:** https://elaraai.com
|
|
43
|
+
|
|
44
|
+
## Governing Law
|
|
45
|
+
|
|
46
|
+
This license is governed by the laws of New South Wales, Australia.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
*Elara AI Pty Ltd*
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @elaraai/e3-core
|
|
2
|
+
|
|
3
|
+
Core library for e3 repository operations, similar to libgit2 for git.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @elaraai/e3-core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Pure business logic with no UI dependencies. Use this to build custom tools, integrations, or alternative interfaces on top of e3.
|
|
14
|
+
|
|
15
|
+
## API
|
|
16
|
+
|
|
17
|
+
### Repository
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { initRepository, findRepository, getRepository } from '@elaraai/e3-core';
|
|
21
|
+
|
|
22
|
+
initRepository('/path/to/project');
|
|
23
|
+
const repoPath = findRepository(); // Searches cwd and parents
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Objects
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { storeObject, loadObject, computeTaskId } from '@elaraai/e3-core';
|
|
30
|
+
|
|
31
|
+
const hash = await storeObject(repoPath, data, '.beast2');
|
|
32
|
+
const data = await loadObject(repoPath, hash, '.beast2');
|
|
33
|
+
const taskId = computeTaskId(irHash, argsHashes);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Commits
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { createNewTaskCommit, createTaskDoneCommit, loadCommit } from '@elaraai/e3-core';
|
|
40
|
+
|
|
41
|
+
const commitHash = await createNewTaskCommit(repoPath, taskId, irHash, argsHashes, 'node', null);
|
|
42
|
+
const commit = await loadCommit(repoPath, commitHash);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Tasks
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { updateTaskState, getTaskState, listTasks } from '@elaraai/e3-core';
|
|
49
|
+
|
|
50
|
+
await updateTaskState(repoPath, taskId, commitHash);
|
|
51
|
+
const commit = await getTaskState(repoPath, taskId);
|
|
52
|
+
const tasks = await listTasks(repoPath);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Refs
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { setTaskRef, deleteTaskRef, listTaskRefs, resolveToTaskId } from '@elaraai/e3-core';
|
|
59
|
+
|
|
60
|
+
await setTaskRef(repoPath, 'my-task', taskId);
|
|
61
|
+
const taskId = await resolveToTaskId(repoPath, 'my-task');
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Related Repos
|
|
65
|
+
|
|
66
|
+
- **[east](https://github.com/elaraai/east)** - East language core
|
|
67
|
+
- **[east-node](https://github.com/elaraai/east-node)** - Node.js runtime and platform functions
|
|
68
|
+
- **[east-py](https://github.com/elaraai/east-py)** - Python runtime and data science
|
|
69
|
+
|
|
70
|
+
## About Elara
|
|
71
|
+
|
|
72
|
+
e3 is developed by [Elara AI](https://elaraai.com/), an AI-powered platform that creates economic digital twins of businesses. e3 powers the execution layer of Elara solutions, enabling durable and efficient execution of East programs across multiple runtimes.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
BSL 1.1. See [LICENSE.md](./LICENSE.md).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Result of executing a single task in the dataflow.
|
|
7
|
+
*/
|
|
8
|
+
export interface TaskExecutionResult {
|
|
9
|
+
/** Task name */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Whether the task was cached */
|
|
12
|
+
cached: boolean;
|
|
13
|
+
/** Final state */
|
|
14
|
+
state: 'success' | 'failed' | 'error' | 'skipped';
|
|
15
|
+
/** Error message if state is 'error' */
|
|
16
|
+
error?: string;
|
|
17
|
+
/** Exit code if state is 'failed' */
|
|
18
|
+
exitCode?: number;
|
|
19
|
+
/** Duration in milliseconds */
|
|
20
|
+
duration: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Result of a dataflow execution.
|
|
24
|
+
*/
|
|
25
|
+
export interface DataflowResult {
|
|
26
|
+
/** Overall success - true if all tasks completed successfully */
|
|
27
|
+
success: boolean;
|
|
28
|
+
/** Number of tasks executed (not from cache) */
|
|
29
|
+
executed: number;
|
|
30
|
+
/** Number of tasks served from cache */
|
|
31
|
+
cached: number;
|
|
32
|
+
/** Number of tasks that failed */
|
|
33
|
+
failed: number;
|
|
34
|
+
/** Number of tasks skipped due to upstream failure */
|
|
35
|
+
skipped: number;
|
|
36
|
+
/** Per-task results */
|
|
37
|
+
tasks: TaskExecutionResult[];
|
|
38
|
+
/** Total duration in milliseconds */
|
|
39
|
+
duration: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Options for dataflow execution.
|
|
43
|
+
*/
|
|
44
|
+
export interface DataflowOptions {
|
|
45
|
+
/** Maximum concurrent task executions (default: 4) */
|
|
46
|
+
concurrency?: number;
|
|
47
|
+
/** Force re-execution even if cached (default: false) */
|
|
48
|
+
force?: boolean;
|
|
49
|
+
/** Filter to run only specific task(s) by exact name */
|
|
50
|
+
filter?: string;
|
|
51
|
+
/** Callback when a task starts */
|
|
52
|
+
onTaskStart?: (name: string) => void;
|
|
53
|
+
/** Callback when a task completes */
|
|
54
|
+
onTaskComplete?: (result: TaskExecutionResult) => void;
|
|
55
|
+
/** Callback for task stdout */
|
|
56
|
+
onStdout?: (taskName: string, data: string) => void;
|
|
57
|
+
/** Callback for task stderr */
|
|
58
|
+
onStderr?: (taskName: string, data: string) => void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Execute all tasks in a workspace according to the dependency graph.
|
|
62
|
+
*
|
|
63
|
+
* Tasks are executed in parallel where dependencies allow, respecting
|
|
64
|
+
* the concurrency limit. On failure, no new tasks are launched but
|
|
65
|
+
* running tasks are allowed to complete.
|
|
66
|
+
*
|
|
67
|
+
* @param repoPath - Path to .e3 repository
|
|
68
|
+
* @param ws - Workspace name
|
|
69
|
+
* @param options - Execution options
|
|
70
|
+
* @returns Result of the dataflow execution
|
|
71
|
+
* @throws {WorkspaceNotFoundError} If workspace doesn't exist
|
|
72
|
+
* @throws {WorkspaceNotDeployedError} If workspace has no package deployed
|
|
73
|
+
* @throws {TaskNotFoundError} If filter specifies a task that doesn't exist
|
|
74
|
+
* @throws {DataflowError} If execution fails for other reasons
|
|
75
|
+
*/
|
|
76
|
+
export declare function dataflowExecute(repoPath: string, ws: string, options?: DataflowOptions): Promise<DataflowResult>;
|
|
77
|
+
/**
|
|
78
|
+
* Get the dependency graph for a workspace (for visualization/debugging).
|
|
79
|
+
*
|
|
80
|
+
* @param repoPath - Path to .e3 repository
|
|
81
|
+
* @param ws - Workspace name
|
|
82
|
+
* @returns Graph information
|
|
83
|
+
* @throws {WorkspaceNotFoundError} If workspace doesn't exist
|
|
84
|
+
* @throws {WorkspaceNotDeployedError} If workspace has no package deployed
|
|
85
|
+
* @throws {DataflowError} If graph building fails for other reasons
|
|
86
|
+
*/
|
|
87
|
+
export declare function dataflowGetGraph(repoPath: string, ws: string): Promise<{
|
|
88
|
+
tasks: Array<{
|
|
89
|
+
name: string;
|
|
90
|
+
hash: string;
|
|
91
|
+
inputs: string[];
|
|
92
|
+
output: string;
|
|
93
|
+
dependsOn: string[];
|
|
94
|
+
}>;
|
|
95
|
+
}>;
|
|
96
|
+
//# sourceMappingURL=dataflow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataflow.d.ts","sourceRoot":"","sources":["../../src/dataflow.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAsEH;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,MAAM,EAAE,OAAO,CAAC;IAChB,kBAAkB;IAClB,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;IAClD,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAC;IACjB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,qCAAqC;IACrC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACvD,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAqHD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CA2RzB;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC;IACT,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC,CAAC;CACJ,CAAC,CA0CD"}
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Dataflow execution for e3 workspaces.
|
|
7
|
+
*
|
|
8
|
+
* Executes tasks in a workspace based on their dependency graph. Tasks are
|
|
9
|
+
* executed in parallel where possible, respecting a concurrency limit.
|
|
10
|
+
*
|
|
11
|
+
* The execution model is event-driven with a work queue:
|
|
12
|
+
* 1. Build dependency graph from tasks (input paths -> task -> output path)
|
|
13
|
+
* 2. Compute reverse dependencies (which tasks depend on each output)
|
|
14
|
+
* 3. Initialize ready queue with tasks whose inputs are all assigned
|
|
15
|
+
* 4. Execute tasks from ready queue, respecting concurrency limit
|
|
16
|
+
* 5. On task completion, update workspace and check dependents for readiness
|
|
17
|
+
* 6. On failure, stop launching new tasks but wait for running ones
|
|
18
|
+
*/
|
|
19
|
+
import { decodeBeast2For } from '@elaraai/east';
|
|
20
|
+
import { PackageObjectType, TaskObjectType, WorkspaceStateType, pathToString, } from '@elaraai/e3-types';
|
|
21
|
+
import { objectRead } from './objects.js';
|
|
22
|
+
import { taskExecute, executionGetOutput, inputsHash, } from './executions.js';
|
|
23
|
+
import { workspaceGetDatasetHash, workspaceSetDatasetByHash, } from './trees.js';
|
|
24
|
+
import { E3Error, WorkspaceNotFoundError, WorkspaceNotDeployedError, TaskNotFoundError, DataflowError, isNotFoundError, } from './errors.js';
|
|
25
|
+
import * as fs from 'fs/promises';
|
|
26
|
+
import * as path from 'path';
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Workspace State Reader
|
|
29
|
+
// =============================================================================
|
|
30
|
+
/**
|
|
31
|
+
* Read workspace state from file.
|
|
32
|
+
* @throws {WorkspaceNotFoundError} If workspace doesn't exist
|
|
33
|
+
* @throws {WorkspaceNotDeployedError} If workspace has no package deployed
|
|
34
|
+
*/
|
|
35
|
+
async function readWorkspaceState(repoPath, ws) {
|
|
36
|
+
const stateFile = path.join(repoPath, 'workspaces', `${ws}.beast2`);
|
|
37
|
+
let data;
|
|
38
|
+
try {
|
|
39
|
+
data = await fs.readFile(stateFile);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (isNotFoundError(err)) {
|
|
43
|
+
throw new WorkspaceNotFoundError(ws);
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
if (data.length === 0) {
|
|
48
|
+
throw new WorkspaceNotDeployedError(ws);
|
|
49
|
+
}
|
|
50
|
+
const decoder = decodeBeast2For(WorkspaceStateType);
|
|
51
|
+
return decoder(data);
|
|
52
|
+
}
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// Dependency Graph Building
|
|
55
|
+
// =============================================================================
|
|
56
|
+
/**
|
|
57
|
+
* Build the dependency graph for a workspace.
|
|
58
|
+
*
|
|
59
|
+
* Returns:
|
|
60
|
+
* - taskNodes: Map of task name -> TaskNode
|
|
61
|
+
* - outputToTask: Map of output path string -> task name
|
|
62
|
+
* - taskDependents: Map of task name -> set of dependent task names
|
|
63
|
+
*/
|
|
64
|
+
async function buildDependencyGraph(repoPath, ws) {
|
|
65
|
+
// Read workspace state to get package hash
|
|
66
|
+
const state = await readWorkspaceState(repoPath, ws);
|
|
67
|
+
// Read package object to get tasks map
|
|
68
|
+
const pkgData = await objectRead(repoPath, state.packageHash);
|
|
69
|
+
const pkgDecoder = decodeBeast2For(PackageObjectType);
|
|
70
|
+
const pkgObject = pkgDecoder(Buffer.from(pkgData));
|
|
71
|
+
const taskNodes = new Map();
|
|
72
|
+
const outputToTask = new Map(); // output path -> task name
|
|
73
|
+
// First pass: load all tasks and build output->task map
|
|
74
|
+
const taskDecoder = decodeBeast2For(TaskObjectType);
|
|
75
|
+
for (const [taskName, taskHash] of pkgObject.tasks) {
|
|
76
|
+
const taskData = await objectRead(repoPath, taskHash);
|
|
77
|
+
const task = taskDecoder(Buffer.from(taskData));
|
|
78
|
+
const outputPathStr = pathToString(task.output);
|
|
79
|
+
outputToTask.set(outputPathStr, taskName);
|
|
80
|
+
taskNodes.set(taskName, {
|
|
81
|
+
name: taskName,
|
|
82
|
+
hash: taskHash,
|
|
83
|
+
task,
|
|
84
|
+
inputPaths: task.inputs,
|
|
85
|
+
outputPath: task.output,
|
|
86
|
+
unresolvedCount: 0, // Will be computed below
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// Build reverse dependency map: task -> tasks that depend on it
|
|
90
|
+
const taskDependents = new Map();
|
|
91
|
+
for (const taskName of taskNodes.keys()) {
|
|
92
|
+
taskDependents.set(taskName, new Set());
|
|
93
|
+
}
|
|
94
|
+
// Second pass: compute dependencies and unresolved counts
|
|
95
|
+
for (const [taskName, node] of taskNodes) {
|
|
96
|
+
for (const inputPath of node.inputPaths) {
|
|
97
|
+
const inputPathStr = pathToString(inputPath);
|
|
98
|
+
const producerTask = outputToTask.get(inputPathStr);
|
|
99
|
+
if (producerTask) {
|
|
100
|
+
// This input comes from another task's output.
|
|
101
|
+
// The task cannot run until the producer task completes,
|
|
102
|
+
// regardless of whether the output is currently assigned
|
|
103
|
+
// (it might be stale from a previous run).
|
|
104
|
+
taskDependents.get(producerTask).add(taskName);
|
|
105
|
+
node.unresolvedCount++;
|
|
106
|
+
}
|
|
107
|
+
// If not produced by a task, it's an external input - check if assigned
|
|
108
|
+
else {
|
|
109
|
+
const { refType } = await workspaceGetDatasetHash(repoPath, ws, inputPath);
|
|
110
|
+
if (refType === 'unassigned') {
|
|
111
|
+
// External input that is unassigned - this task can never run
|
|
112
|
+
node.unresolvedCount++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { taskNodes, outputToTask, taskDependents };
|
|
118
|
+
}
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// Dataflow Execution
|
|
121
|
+
// =============================================================================
|
|
122
|
+
/**
|
|
123
|
+
* Execute all tasks in a workspace according to the dependency graph.
|
|
124
|
+
*
|
|
125
|
+
* Tasks are executed in parallel where dependencies allow, respecting
|
|
126
|
+
* the concurrency limit. On failure, no new tasks are launched but
|
|
127
|
+
* running tasks are allowed to complete.
|
|
128
|
+
*
|
|
129
|
+
* @param repoPath - Path to .e3 repository
|
|
130
|
+
* @param ws - Workspace name
|
|
131
|
+
* @param options - Execution options
|
|
132
|
+
* @returns Result of the dataflow execution
|
|
133
|
+
* @throws {WorkspaceNotFoundError} If workspace doesn't exist
|
|
134
|
+
* @throws {WorkspaceNotDeployedError} If workspace has no package deployed
|
|
135
|
+
* @throws {TaskNotFoundError} If filter specifies a task that doesn't exist
|
|
136
|
+
* @throws {DataflowError} If execution fails for other reasons
|
|
137
|
+
*/
|
|
138
|
+
export async function dataflowExecute(repoPath, ws, options = {}) {
|
|
139
|
+
const startTime = Date.now();
|
|
140
|
+
const concurrency = options.concurrency ?? 4;
|
|
141
|
+
let taskNodes;
|
|
142
|
+
let taskDependents;
|
|
143
|
+
try {
|
|
144
|
+
// Build dependency graph
|
|
145
|
+
const graph = await buildDependencyGraph(repoPath, ws);
|
|
146
|
+
taskNodes = graph.taskNodes;
|
|
147
|
+
taskDependents = graph.taskDependents;
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
// Re-throw E3Errors as-is
|
|
151
|
+
if (err instanceof E3Error)
|
|
152
|
+
throw err;
|
|
153
|
+
// Wrap unexpected errors
|
|
154
|
+
throw new DataflowError(`Failed to build dependency graph: ${err instanceof Error ? err.message : err}`);
|
|
155
|
+
}
|
|
156
|
+
// Apply filter if specified
|
|
157
|
+
const filteredTaskNames = options.filter
|
|
158
|
+
? new Set([options.filter])
|
|
159
|
+
: null;
|
|
160
|
+
// Validate filter
|
|
161
|
+
if (filteredTaskNames && options.filter && !taskNodes.has(options.filter)) {
|
|
162
|
+
throw new TaskNotFoundError(options.filter);
|
|
163
|
+
}
|
|
164
|
+
// Track execution state
|
|
165
|
+
const results = [];
|
|
166
|
+
let executed = 0;
|
|
167
|
+
let cached = 0;
|
|
168
|
+
let failed = 0;
|
|
169
|
+
let skipped = 0;
|
|
170
|
+
let hasFailure = false;
|
|
171
|
+
// Ready queue: tasks with all dependencies resolved
|
|
172
|
+
const readyQueue = [];
|
|
173
|
+
const completed = new Set();
|
|
174
|
+
const inProgress = new Set();
|
|
175
|
+
// Initialize ready queue with tasks that have no unresolved dependencies
|
|
176
|
+
// and pass the filter (if any)
|
|
177
|
+
for (const [taskName, node] of taskNodes) {
|
|
178
|
+
if (node.unresolvedCount === 0) {
|
|
179
|
+
if (!filteredTaskNames || filteredTaskNames.has(taskName)) {
|
|
180
|
+
readyQueue.push(taskName);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Check if the task has a valid cached execution for current inputs
|
|
185
|
+
// Returns the output hash if cached, null if re-execution is needed
|
|
186
|
+
async function getCachedOutput(taskName) {
|
|
187
|
+
const node = taskNodes.get(taskName);
|
|
188
|
+
// Gather current input hashes
|
|
189
|
+
const currentInputHashes = [];
|
|
190
|
+
for (const inputPath of node.inputPaths) {
|
|
191
|
+
const { refType, hash } = await workspaceGetDatasetHash(repoPath, ws, inputPath);
|
|
192
|
+
if (refType !== 'value' || hash === null) {
|
|
193
|
+
// Input not assigned, can't be cached
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
currentInputHashes.push(hash);
|
|
197
|
+
}
|
|
198
|
+
// Check if there's a cached execution for these inputs
|
|
199
|
+
const inHash = inputsHash(currentInputHashes);
|
|
200
|
+
const cachedOutputHash = await executionGetOutput(repoPath, node.hash, inHash);
|
|
201
|
+
if (cachedOutputHash === null) {
|
|
202
|
+
// No cached execution for current inputs
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
// Also verify the workspace output matches the cached output
|
|
206
|
+
// (in case the workspace was modified outside of execution)
|
|
207
|
+
const { refType, hash: wsOutputHash } = await workspaceGetDatasetHash(repoPath, ws, node.outputPath);
|
|
208
|
+
if (refType !== 'value' || wsOutputHash !== cachedOutputHash) {
|
|
209
|
+
// Workspace output doesn't match cached output, need to re-execute
|
|
210
|
+
// (or update workspace with cached value)
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return cachedOutputHash;
|
|
214
|
+
}
|
|
215
|
+
// Execute a single task
|
|
216
|
+
async function executeTask(taskName) {
|
|
217
|
+
const node = taskNodes.get(taskName);
|
|
218
|
+
const taskStartTime = Date.now();
|
|
219
|
+
options.onTaskStart?.(taskName);
|
|
220
|
+
// Gather input hashes
|
|
221
|
+
const inputHashes = [];
|
|
222
|
+
for (const inputPath of node.inputPaths) {
|
|
223
|
+
const { refType, hash } = await workspaceGetDatasetHash(repoPath, ws, inputPath);
|
|
224
|
+
if (refType !== 'value' || hash === null) {
|
|
225
|
+
// Input not available - should not happen if dependency tracking is correct
|
|
226
|
+
return {
|
|
227
|
+
name: taskName,
|
|
228
|
+
cached: false,
|
|
229
|
+
state: 'error',
|
|
230
|
+
error: `Input at ${pathToString(inputPath)} is not assigned (refType: ${refType})`,
|
|
231
|
+
duration: Date.now() - taskStartTime,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
inputHashes.push(hash);
|
|
235
|
+
}
|
|
236
|
+
// Execute the task
|
|
237
|
+
const execOptions = {
|
|
238
|
+
force: options.force,
|
|
239
|
+
onStdout: options.onStdout ? (data) => options.onStdout(taskName, data) : undefined,
|
|
240
|
+
onStderr: options.onStderr ? (data) => options.onStderr(taskName, data) : undefined,
|
|
241
|
+
};
|
|
242
|
+
const result = await taskExecute(repoPath, node.hash, inputHashes, execOptions);
|
|
243
|
+
// Build task result
|
|
244
|
+
const taskResult = {
|
|
245
|
+
name: taskName,
|
|
246
|
+
cached: result.cached,
|
|
247
|
+
state: result.state,
|
|
248
|
+
duration: Date.now() - taskStartTime,
|
|
249
|
+
};
|
|
250
|
+
if (result.state === 'error') {
|
|
251
|
+
taskResult.error = result.error ?? undefined;
|
|
252
|
+
}
|
|
253
|
+
else if (result.state === 'failed') {
|
|
254
|
+
taskResult.exitCode = result.exitCode ?? undefined;
|
|
255
|
+
taskResult.error = result.error ?? undefined;
|
|
256
|
+
}
|
|
257
|
+
// On success, update the workspace with the output
|
|
258
|
+
if (result.state === 'success' && result.outputHash) {
|
|
259
|
+
await workspaceSetDatasetByHash(repoPath, ws, node.outputPath, result.outputHash);
|
|
260
|
+
}
|
|
261
|
+
return taskResult;
|
|
262
|
+
}
|
|
263
|
+
// Process dependents when a task completes
|
|
264
|
+
function notifyDependents(taskName) {
|
|
265
|
+
const dependents = taskDependents.get(taskName) ?? new Set();
|
|
266
|
+
for (const depName of dependents) {
|
|
267
|
+
if (completed.has(depName) || inProgress.has(depName))
|
|
268
|
+
continue;
|
|
269
|
+
// Skip dependents not in the filter
|
|
270
|
+
if (filteredTaskNames && !filteredTaskNames.has(depName))
|
|
271
|
+
continue;
|
|
272
|
+
const depNode = taskNodes.get(depName);
|
|
273
|
+
depNode.unresolvedCount--;
|
|
274
|
+
if (depNode.unresolvedCount === 0 && !readyQueue.includes(depName)) {
|
|
275
|
+
readyQueue.push(depName);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Mark dependents as skipped when a task fails
|
|
280
|
+
function skipDependents(taskName) {
|
|
281
|
+
const dependents = taskDependents.get(taskName) ?? new Set();
|
|
282
|
+
for (const depName of dependents) {
|
|
283
|
+
if (completed.has(depName) || inProgress.has(depName))
|
|
284
|
+
continue;
|
|
285
|
+
// Skip dependents not in the filter
|
|
286
|
+
if (filteredTaskNames && !filteredTaskNames.has(depName))
|
|
287
|
+
continue;
|
|
288
|
+
// Recursively skip
|
|
289
|
+
completed.add(depName);
|
|
290
|
+
skipped++;
|
|
291
|
+
results.push({
|
|
292
|
+
name: depName,
|
|
293
|
+
cached: false,
|
|
294
|
+
state: 'skipped',
|
|
295
|
+
duration: 0,
|
|
296
|
+
});
|
|
297
|
+
options.onTaskComplete?.({
|
|
298
|
+
name: depName,
|
|
299
|
+
cached: false,
|
|
300
|
+
state: 'skipped',
|
|
301
|
+
duration: 0,
|
|
302
|
+
});
|
|
303
|
+
skipDependents(depName);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Main execution loop using a work-stealing approach
|
|
307
|
+
const runningPromises = new Map();
|
|
308
|
+
async function processQueue() {
|
|
309
|
+
while (true) {
|
|
310
|
+
// Check if we're done
|
|
311
|
+
if (readyQueue.length === 0 && runningPromises.size === 0) {
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
// Launch tasks up to concurrency limit if no failure
|
|
315
|
+
while (!hasFailure && readyQueue.length > 0 && runningPromises.size < concurrency) {
|
|
316
|
+
const taskName = readyQueue.shift();
|
|
317
|
+
if (completed.has(taskName) || inProgress.has(taskName))
|
|
318
|
+
continue;
|
|
319
|
+
// Check if there's a valid cached execution for current inputs
|
|
320
|
+
const cachedOutputHash = await getCachedOutput(taskName);
|
|
321
|
+
if (cachedOutputHash !== null && !options.force) {
|
|
322
|
+
// Valid cached execution exists for current inputs
|
|
323
|
+
completed.add(taskName);
|
|
324
|
+
cached++;
|
|
325
|
+
const result = {
|
|
326
|
+
name: taskName,
|
|
327
|
+
cached: true,
|
|
328
|
+
state: 'success',
|
|
329
|
+
duration: 0,
|
|
330
|
+
};
|
|
331
|
+
results.push(result);
|
|
332
|
+
options.onTaskComplete?.(result);
|
|
333
|
+
notifyDependents(taskName);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
inProgress.add(taskName);
|
|
337
|
+
const promise = (async () => {
|
|
338
|
+
try {
|
|
339
|
+
const result = await executeTask(taskName);
|
|
340
|
+
inProgress.delete(taskName);
|
|
341
|
+
completed.add(taskName);
|
|
342
|
+
results.push(result);
|
|
343
|
+
options.onTaskComplete?.(result);
|
|
344
|
+
if (result.state === 'success') {
|
|
345
|
+
if (result.cached) {
|
|
346
|
+
cached++;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
executed++;
|
|
350
|
+
}
|
|
351
|
+
notifyDependents(taskName);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
failed++;
|
|
355
|
+
hasFailure = true;
|
|
356
|
+
skipDependents(taskName);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
runningPromises.delete(taskName);
|
|
361
|
+
}
|
|
362
|
+
})();
|
|
363
|
+
runningPromises.set(taskName, promise);
|
|
364
|
+
}
|
|
365
|
+
// Wait for at least one task to complete if we can't launch more
|
|
366
|
+
if (runningPromises.size > 0) {
|
|
367
|
+
await Promise.race(runningPromises.values());
|
|
368
|
+
}
|
|
369
|
+
else if (readyQueue.length === 0) {
|
|
370
|
+
// No running tasks and no ready tasks - we might have unresolvable dependencies
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
await processQueue();
|
|
376
|
+
// Wait for any remaining tasks
|
|
377
|
+
if (runningPromises.size > 0) {
|
|
378
|
+
await Promise.all(runningPromises.values());
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
success: !hasFailure,
|
|
382
|
+
executed,
|
|
383
|
+
cached,
|
|
384
|
+
failed,
|
|
385
|
+
skipped,
|
|
386
|
+
tasks: results,
|
|
387
|
+
duration: Date.now() - startTime,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get the dependency graph for a workspace (for visualization/debugging).
|
|
392
|
+
*
|
|
393
|
+
* @param repoPath - Path to .e3 repository
|
|
394
|
+
* @param ws - Workspace name
|
|
395
|
+
* @returns Graph information
|
|
396
|
+
* @throws {WorkspaceNotFoundError} If workspace doesn't exist
|
|
397
|
+
* @throws {WorkspaceNotDeployedError} If workspace has no package deployed
|
|
398
|
+
* @throws {DataflowError} If graph building fails for other reasons
|
|
399
|
+
*/
|
|
400
|
+
export async function dataflowGetGraph(repoPath, ws) {
|
|
401
|
+
let taskNodes;
|
|
402
|
+
let outputToTask;
|
|
403
|
+
try {
|
|
404
|
+
const graph = await buildDependencyGraph(repoPath, ws);
|
|
405
|
+
taskNodes = graph.taskNodes;
|
|
406
|
+
outputToTask = graph.outputToTask;
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
if (err instanceof E3Error)
|
|
410
|
+
throw err;
|
|
411
|
+
throw new DataflowError(`Failed to build dependency graph: ${err instanceof Error ? err.message : err}`);
|
|
412
|
+
}
|
|
413
|
+
const tasks = [];
|
|
414
|
+
for (const [taskName, node] of taskNodes) {
|
|
415
|
+
const dependsOn = [];
|
|
416
|
+
for (const inputPath of node.inputPaths) {
|
|
417
|
+
const inputPathStr = pathToString(inputPath);
|
|
418
|
+
const producerTask = outputToTask.get(inputPathStr);
|
|
419
|
+
if (producerTask) {
|
|
420
|
+
dependsOn.push(producerTask);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
tasks.push({
|
|
424
|
+
name: taskName,
|
|
425
|
+
hash: node.hash,
|
|
426
|
+
inputs: node.inputPaths.map(pathToString),
|
|
427
|
+
output: pathToString(node.outputPath),
|
|
428
|
+
dependsOn,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return { tasks };
|
|
432
|
+
}
|
|
433
|
+
//# sourceMappingURL=dataflow.js.map
|