@elaraai/e3-core 0.0.2-beta.4 → 0.0.2-beta.41
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/README.md +25 -22
- package/dist/src/dataflow/api-compat.d.ts +90 -0
- package/dist/src/dataflow/api-compat.d.ts.map +1 -0
- package/dist/src/dataflow/api-compat.js +139 -0
- package/dist/src/dataflow/api-compat.js.map +1 -0
- package/dist/src/dataflow/index.d.ts +18 -0
- package/dist/src/dataflow/index.d.ts.map +1 -0
- package/dist/src/dataflow/index.js +23 -0
- package/dist/src/dataflow/index.js.map +1 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts +76 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts.map +1 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.js +695 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.js.map +1 -0
- package/dist/src/dataflow/orchestrator/index.d.ts +12 -0
- package/dist/src/dataflow/orchestrator/index.d.ts.map +1 -0
- package/dist/src/dataflow/orchestrator/index.js +12 -0
- package/dist/src/dataflow/orchestrator/index.js.map +1 -0
- package/dist/src/dataflow/orchestrator/interfaces.d.ts +163 -0
- package/dist/src/dataflow/orchestrator/interfaces.d.ts.map +1 -0
- package/dist/src/dataflow/orchestrator/interfaces.js +52 -0
- package/dist/src/dataflow/orchestrator/interfaces.js.map +1 -0
- package/dist/src/dataflow/state-store/FileStateStore.d.ts +67 -0
- package/dist/src/dataflow/state-store/FileStateStore.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/FileStateStore.js +300 -0
- package/dist/src/dataflow/state-store/FileStateStore.js.map +1 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts +42 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.js +229 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.js.map +1 -0
- package/dist/src/dataflow/state-store/index.d.ts +13 -0
- package/dist/src/dataflow/state-store/index.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/index.js +13 -0
- package/dist/src/dataflow/state-store/index.js.map +1 -0
- package/dist/src/dataflow/state-store/interfaces.d.ts +159 -0
- package/dist/src/dataflow/state-store/interfaces.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/interfaces.js +6 -0
- package/dist/src/dataflow/state-store/interfaces.js.map +1 -0
- package/dist/src/dataflow/steps.d.ts +222 -0
- package/dist/src/dataflow/steps.d.ts.map +1 -0
- package/dist/src/dataflow/steps.js +707 -0
- package/dist/src/dataflow/steps.js.map +1 -0
- package/dist/src/dataflow/types.d.ts +127 -0
- package/dist/src/dataflow/types.d.ts.map +1 -0
- package/dist/src/dataflow/types.js +7 -0
- package/dist/src/dataflow/types.js.map +1 -0
- package/dist/src/dataflow.d.ts +113 -38
- package/dist/src/dataflow.d.ts.map +1 -1
- package/dist/src/dataflow.js +269 -416
- package/dist/src/dataflow.js.map +1 -1
- package/dist/src/dataset-refs.d.ts +124 -0
- package/dist/src/dataset-refs.d.ts.map +1 -0
- package/dist/src/dataset-refs.js +319 -0
- package/dist/src/dataset-refs.js.map +1 -0
- package/dist/src/errors.d.ts +39 -9
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +51 -8
- package/dist/src/errors.js.map +1 -1
- package/dist/src/execution/LocalTaskRunner.d.ts +73 -0
- package/dist/src/execution/LocalTaskRunner.d.ts.map +1 -0
- package/dist/src/execution/LocalTaskRunner.js +399 -0
- package/dist/src/execution/LocalTaskRunner.js.map +1 -0
- package/dist/src/execution/MockTaskRunner.d.ts +49 -0
- package/dist/src/execution/MockTaskRunner.d.ts.map +1 -0
- package/dist/src/execution/MockTaskRunner.js +54 -0
- package/dist/src/execution/MockTaskRunner.js.map +1 -0
- package/dist/src/execution/index.d.ts +16 -0
- package/dist/src/execution/index.d.ts.map +1 -0
- package/dist/src/execution/index.js +8 -0
- package/dist/src/execution/index.js.map +1 -0
- package/dist/src/execution/interfaces.d.ts +246 -0
- package/dist/src/execution/interfaces.d.ts.map +1 -0
- package/dist/src/execution/interfaces.js +6 -0
- package/dist/src/execution/interfaces.js.map +1 -0
- package/dist/src/execution/processHelpers.d.ts +20 -0
- package/dist/src/execution/processHelpers.d.ts.map +1 -0
- package/dist/src/execution/processHelpers.js +62 -0
- package/dist/src/execution/processHelpers.js.map +1 -0
- package/dist/src/executions.d.ts +71 -104
- package/dist/src/executions.d.ts.map +1 -1
- package/dist/src/executions.js +110 -476
- package/dist/src/executions.js.map +1 -1
- package/dist/src/index.d.ts +19 -9
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +48 -18
- package/dist/src/index.js.map +1 -1
- package/dist/src/objects.d.ts +8 -51
- package/dist/src/objects.d.ts.map +1 -1
- package/dist/src/objects.js +13 -230
- package/dist/src/objects.js.map +1 -1
- package/dist/src/packages.d.ts +22 -14
- package/dist/src/packages.d.ts.map +1 -1
- package/dist/src/packages.js +134 -88
- package/dist/src/packages.js.map +1 -1
- package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts +35 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts.map +1 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.js +107 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.js.map +1 -0
- package/dist/src/storage/in-memory/InMemoryStorage.d.ts +139 -0
- package/dist/src/storage/in-memory/InMemoryStorage.d.ts.map +1 -0
- package/dist/src/storage/in-memory/InMemoryStorage.js +439 -0
- package/dist/src/storage/in-memory/InMemoryStorage.js.map +1 -0
- package/dist/src/storage/in-memory/index.d.ts +12 -0
- package/dist/src/storage/in-memory/index.d.ts.map +1 -0
- package/dist/src/storage/in-memory/index.js +12 -0
- package/dist/src/storage/in-memory/index.js.map +1 -0
- package/dist/src/storage/index.d.ts +18 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/index.js +10 -0
- package/dist/src/storage/index.js.map +1 -0
- package/dist/src/storage/interfaces.d.ts +581 -0
- package/dist/src/storage/interfaces.d.ts.map +1 -0
- package/dist/src/storage/interfaces.js +6 -0
- package/dist/src/storage/interfaces.js.map +1 -0
- package/dist/src/storage/local/LocalBackend.d.ts +56 -0
- package/dist/src/storage/local/LocalBackend.d.ts.map +1 -0
- package/dist/src/storage/local/LocalBackend.js +145 -0
- package/dist/src/storage/local/LocalBackend.js.map +1 -0
- package/dist/src/storage/local/LocalDatasetRefStore.d.ts +22 -0
- package/dist/src/storage/local/LocalDatasetRefStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalDatasetRefStore.js +118 -0
- package/dist/src/storage/local/LocalDatasetRefStore.js.map +1 -0
- package/dist/src/storage/local/LocalLockService.d.ts +111 -0
- package/dist/src/storage/local/LocalLockService.d.ts.map +1 -0
- package/dist/src/storage/local/LocalLockService.js +355 -0
- package/dist/src/storage/local/LocalLockService.js.map +1 -0
- package/dist/src/storage/local/LocalLogStore.d.ts +23 -0
- package/dist/src/storage/local/LocalLogStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalLogStore.js +66 -0
- package/dist/src/storage/local/LocalLogStore.js.map +1 -0
- package/dist/src/storage/local/LocalObjectStore.d.ts +55 -0
- package/dist/src/storage/local/LocalObjectStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalObjectStore.js +300 -0
- package/dist/src/storage/local/LocalObjectStore.js.map +1 -0
- package/dist/src/storage/local/LocalRefStore.d.ts +50 -0
- package/dist/src/storage/local/LocalRefStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalRefStore.js +337 -0
- package/dist/src/storage/local/LocalRefStore.js.map +1 -0
- package/dist/src/storage/local/LocalRepoStore.d.ts +55 -0
- package/dist/src/storage/local/LocalRepoStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalRepoStore.js +365 -0
- package/dist/src/storage/local/LocalRepoStore.js.map +1 -0
- package/dist/src/storage/local/gc.d.ts +92 -0
- package/dist/src/storage/local/gc.d.ts.map +1 -0
- package/dist/src/storage/local/gc.js +377 -0
- package/dist/src/storage/local/gc.js.map +1 -0
- package/dist/src/storage/local/index.d.ts +18 -0
- package/dist/src/storage/local/index.d.ts.map +1 -0
- package/dist/src/storage/local/index.js +18 -0
- package/dist/src/storage/local/index.js.map +1 -0
- package/dist/src/storage/local/localHelpers.d.ts +25 -0
- package/dist/src/storage/local/localHelpers.d.ts.map +1 -0
- package/dist/src/storage/local/localHelpers.js +69 -0
- package/dist/src/storage/local/localHelpers.js.map +1 -0
- package/dist/src/{repository.d.ts → storage/local/repository.d.ts} +8 -4
- package/dist/src/storage/local/repository.d.ts.map +1 -0
- package/dist/src/{repository.js → storage/local/repository.js} +31 -29
- package/dist/src/storage/local/repository.js.map +1 -0
- package/dist/src/tasks.d.ts +16 -10
- package/dist/src/tasks.d.ts.map +1 -1
- package/dist/src/tasks.js +35 -41
- package/dist/src/tasks.js.map +1 -1
- package/dist/src/test-helpers.d.ts +5 -4
- package/dist/src/test-helpers.d.ts.map +1 -1
- package/dist/src/test-helpers.js +9 -21
- package/dist/src/test-helpers.js.map +1 -1
- package/dist/src/transfer/InMemoryTransferBackend.d.ts +66 -0
- package/dist/src/transfer/InMemoryTransferBackend.d.ts.map +1 -0
- package/dist/src/transfer/InMemoryTransferBackend.js +166 -0
- package/dist/src/transfer/InMemoryTransferBackend.js.map +1 -0
- package/dist/src/transfer/index.d.ts +8 -0
- package/dist/src/transfer/index.d.ts.map +1 -0
- package/dist/src/transfer/index.js +9 -0
- package/dist/src/transfer/index.js.map +1 -0
- package/dist/src/transfer/interfaces.d.ts +103 -0
- package/dist/src/transfer/interfaces.d.ts.map +1 -0
- package/dist/src/transfer/interfaces.js +6 -0
- package/dist/src/transfer/interfaces.js.map +1 -0
- package/dist/src/transfer/types.d.ts +79 -0
- package/dist/src/transfer/types.d.ts.map +1 -0
- package/dist/src/transfer/types.js +58 -0
- package/dist/src/transfer/types.js.map +1 -0
- package/dist/src/trees.d.ts +147 -59
- package/dist/src/trees.d.ts.map +1 -1
- package/dist/src/trees.js +372 -419
- package/dist/src/trees.js.map +1 -1
- package/dist/src/uuid.d.ts +26 -0
- package/dist/src/uuid.d.ts.map +1 -0
- package/dist/src/uuid.js +80 -0
- package/dist/src/uuid.js.map +1 -0
- package/dist/src/workspaceStatus.d.ts +6 -4
- package/dist/src/workspaceStatus.d.ts.map +1 -1
- package/dist/src/workspaceStatus.js +43 -49
- package/dist/src/workspaceStatus.js.map +1 -1
- package/dist/src/workspaces.d.ts +35 -47
- package/dist/src/workspaces.d.ts.map +1 -1
- package/dist/src/workspaces.js +194 -156
- package/dist/src/workspaces.js.map +1 -1
- package/package.json +4 -4
- package/dist/src/gc.d.ts +0 -54
- package/dist/src/gc.d.ts.map +0 -1
- package/dist/src/gc.js +0 -233
- package/dist/src/gc.js.map +0 -1
- package/dist/src/repository.d.ts.map +0 -1
- package/dist/src/repository.js.map +0 -1
- package/dist/src/workspaceLock.d.ts +0 -67
- package/dist/src/workspaceLock.d.ts.map +0 -1
- package/dist/src/workspaceLock.js +0 -217
- package/dist/src/workspaceLock.js.map +0 -1
package/dist/src/trees.js
CHANGED
|
@@ -12,17 +12,16 @@
|
|
|
12
12
|
* tree node. This enables proper encoding/decoding with the correct StructType
|
|
13
13
|
* and supports future tree types (array, variant, etc.).
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Workspace operations use per-dataset ref files (DatasetRef) instead of tree
|
|
16
|
+
* traversal. This enables concurrent writes without serialization.
|
|
17
|
+
*
|
|
18
|
+
* Low-level tree read/write operations remain for computing root hashes and
|
|
19
|
+
* for package operations.
|
|
17
20
|
*/
|
|
18
|
-
import { decodeBeast2, decodeBeast2For, encodeBeast2For, StructType, } from '@elaraai/east';
|
|
21
|
+
import { decodeBeast2, decodeBeast2For, encodeBeast2For, StructType, variant, } from '@elaraai/east';
|
|
19
22
|
import { DataRefType, PackageObjectType, WorkspaceStateType } from '@elaraai/e3-types';
|
|
20
|
-
import { objectRead, objectWrite } from './objects.js';
|
|
21
23
|
import { packageRead } from './packages.js';
|
|
22
|
-
import { WorkspaceNotFoundError, WorkspaceNotDeployedError,
|
|
23
|
-
import { acquireWorkspaceLock } from './workspaceLock.js';
|
|
24
|
-
import * as fs from 'fs/promises';
|
|
25
|
-
import * as path from 'path';
|
|
24
|
+
import { WorkspaceNotFoundError, WorkspaceNotDeployedError, WorkspaceLockError, } from './errors.js';
|
|
26
25
|
/**
|
|
27
26
|
* Build the EastType for a tree object based on its structure.
|
|
28
27
|
*
|
|
@@ -50,32 +49,34 @@ function treeTypeFromStructure(structure) {
|
|
|
50
49
|
/**
|
|
51
50
|
* Read and decode a tree object from the object store.
|
|
52
51
|
*
|
|
53
|
-
* @param
|
|
52
|
+
* @param storage - Storage backend
|
|
53
|
+
* @param repo - Repository identifier
|
|
54
54
|
* @param hash - Hash of the tree object
|
|
55
55
|
* @param structure - The structure describing this tree node's shape
|
|
56
56
|
* @returns The decoded tree object (field name -> DataRef)
|
|
57
57
|
* @throws If object not found, structure is not a tree, or decoding fails
|
|
58
58
|
*/
|
|
59
|
-
export async function treeRead(
|
|
59
|
+
export async function treeRead(storage, repo, hash, structure) {
|
|
60
60
|
const treeType = treeTypeFromStructure(structure);
|
|
61
|
-
const data = await
|
|
61
|
+
const data = await storage.objects.read(repo, hash);
|
|
62
62
|
const decoder = decodeBeast2For(treeType);
|
|
63
63
|
return decoder(Buffer.from(data));
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Encode and write a tree object to the object store.
|
|
67
67
|
*
|
|
68
|
-
* @param
|
|
68
|
+
* @param storage - Storage backend
|
|
69
|
+
* @param repo - Repository identifier
|
|
69
70
|
* @param fields - Object mapping field names to DataRefs
|
|
70
71
|
* @param structure - The structure describing this tree node's shape
|
|
71
72
|
* @returns Hash of the written tree object
|
|
72
73
|
* @throws If structure is not a tree or encoding fails
|
|
73
74
|
*/
|
|
74
|
-
export async function treeWrite(
|
|
75
|
+
export async function treeWrite(storage, repo, fields, structure) {
|
|
75
76
|
const treeType = treeTypeFromStructure(structure);
|
|
76
77
|
const encoder = encodeBeast2For(treeType);
|
|
77
78
|
const data = encoder(fields);
|
|
78
|
-
return
|
|
79
|
+
return storage.objects.write(repo, data);
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
82
|
* Read and decode a dataset value from the object store.
|
|
@@ -83,177 +84,42 @@ export async function treeWrite(repoPath, fields, structure) {
|
|
|
83
84
|
* The .beast2 format includes type information in the header, so values
|
|
84
85
|
* can be decoded without knowing the schema in advance.
|
|
85
86
|
*
|
|
86
|
-
* @param
|
|
87
|
+
* @param storage - Storage backend
|
|
88
|
+
* @param repo - Repository identifier
|
|
87
89
|
* @param hash - Hash of the dataset value
|
|
88
90
|
* @returns The decoded value and its type
|
|
89
91
|
* @throws If object not found or not a valid beast2 object
|
|
90
92
|
*/
|
|
91
|
-
export async function datasetRead(
|
|
92
|
-
const data = await
|
|
93
|
+
export async function datasetRead(storage, repo, hash) {
|
|
94
|
+
const data = await storage.objects.read(repo, hash);
|
|
93
95
|
const result = decodeBeast2(Buffer.from(data));
|
|
94
96
|
return { type: result.type, value: result.value };
|
|
95
97
|
}
|
|
96
98
|
/**
|
|
97
99
|
* Encode and write a dataset value to the object store.
|
|
98
100
|
*
|
|
99
|
-
* @param
|
|
101
|
+
* @param storage - Storage backend
|
|
102
|
+
* @param repo - Repository identifier
|
|
100
103
|
* @param value - The value to encode
|
|
101
104
|
* @param type - The East type for encoding (EastType or EastTypeValue)
|
|
102
105
|
* @returns Hash of the written dataset value
|
|
103
106
|
*/
|
|
104
|
-
export async function datasetWrite(
|
|
107
|
+
export async function datasetWrite(storage, repo, value, type) {
|
|
105
108
|
// encodeBeast2For accepts both EastType and EastTypeValue, but TypeScript
|
|
106
109
|
// overloads don't support union types directly. Cast to EastTypeValue since
|
|
107
110
|
// that's the more general case and the runtime handles both.
|
|
108
111
|
const encoder = encodeBeast2For(type);
|
|
109
112
|
const data = encoder(value);
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Traverse a tree from root to a path, co-walking structure and data.
|
|
114
|
-
*
|
|
115
|
-
* @param repoPath - Path to .e3 repository
|
|
116
|
-
* @param rootHash - Hash of the root tree object
|
|
117
|
-
* @param rootStructure - Structure of the root tree
|
|
118
|
-
* @param path - Path to traverse
|
|
119
|
-
* @returns The structure and DataRef at the path location
|
|
120
|
-
* @throws If path is invalid or traversal fails
|
|
121
|
-
*/
|
|
122
|
-
async function traverse(repoPath, rootHash, rootStructure, path) {
|
|
123
|
-
let currentStructure = rootStructure;
|
|
124
|
-
let currentHash = rootHash;
|
|
125
|
-
for (let i = 0; i < path.length; i++) {
|
|
126
|
-
const segment = path[i];
|
|
127
|
-
if (segment.type !== 'field') {
|
|
128
|
-
throw new Error(`Unsupported path segment type: ${segment.type}`);
|
|
129
|
-
}
|
|
130
|
-
const fieldName = segment.value;
|
|
131
|
-
// Current structure must be a struct tree to descend into
|
|
132
|
-
if (currentStructure.type !== 'struct') {
|
|
133
|
-
const pathSoFar = path.slice(0, i).map(s => s.value).join('.');
|
|
134
|
-
throw new Error(`Cannot descend into non-struct at path '${pathSoFar}'`);
|
|
135
|
-
}
|
|
136
|
-
// Read the current tree object
|
|
137
|
-
const treeObject = await treeRead(repoPath, currentHash, currentStructure);
|
|
138
|
-
// Look up the child ref
|
|
139
|
-
const childRef = treeObject[fieldName];
|
|
140
|
-
if (!childRef) {
|
|
141
|
-
const pathSoFar = path.slice(0, i).map(s => s.value).join('.');
|
|
142
|
-
const available = Object.keys(treeObject).join(', ');
|
|
143
|
-
throw new Error(`Field '${fieldName}' not found at '${pathSoFar}'. Available: ${available}`);
|
|
144
|
-
}
|
|
145
|
-
// Look up the child structure
|
|
146
|
-
const childStructure = currentStructure.value.get(fieldName);
|
|
147
|
-
if (!childStructure) {
|
|
148
|
-
throw new Error(`Field '${fieldName}' not found in structure`);
|
|
149
|
-
}
|
|
150
|
-
// If this is the last segment, return the result
|
|
151
|
-
if (i === path.length - 1) {
|
|
152
|
-
return { structure: childStructure, ref: childRef };
|
|
153
|
-
}
|
|
154
|
-
// Otherwise, continue traversing (must be a tree ref)
|
|
155
|
-
if (childRef.type !== 'tree') {
|
|
156
|
-
const pathSoFar = path.slice(0, i + 1).map(s => s.value).join('.');
|
|
157
|
-
throw new Error(`Expected tree ref at '${pathSoFar}', got '${childRef.type}'`);
|
|
158
|
-
}
|
|
159
|
-
currentStructure = childStructure;
|
|
160
|
-
currentHash = childRef.value;
|
|
161
|
-
}
|
|
162
|
-
// Empty path - return root
|
|
163
|
-
return {
|
|
164
|
-
structure: rootStructure,
|
|
165
|
-
ref: { type: 'tree', value: rootHash },
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* List field names at a tree path within a package's data tree.
|
|
170
|
-
*
|
|
171
|
-
* @param repoPath - Path to .e3 repository
|
|
172
|
-
* @param name - Package name
|
|
173
|
-
* @param version - Package version
|
|
174
|
-
* @param path - Path to the tree node
|
|
175
|
-
* @returns Array of field names at the path
|
|
176
|
-
* @throws If package not found, path invalid, or path points to a dataset
|
|
177
|
-
*/
|
|
178
|
-
export async function packageListTree(repoPath, name, version, path) {
|
|
179
|
-
// Read the package to get root structure and hash
|
|
180
|
-
const pkg = await packageRead(repoPath, name, version);
|
|
181
|
-
const rootStructure = pkg.data.structure;
|
|
182
|
-
const rootHash = pkg.data.value;
|
|
183
|
-
if (path.length === 0) {
|
|
184
|
-
// Empty path - list root tree fields
|
|
185
|
-
if (rootStructure.type !== 'struct') {
|
|
186
|
-
throw new Error('Root is not a tree');
|
|
187
|
-
}
|
|
188
|
-
const treeObject = await treeRead(repoPath, rootHash, rootStructure);
|
|
189
|
-
return Object.keys(treeObject);
|
|
190
|
-
}
|
|
191
|
-
// Traverse to the path
|
|
192
|
-
const { structure, ref } = await traverse(repoPath, rootHash, rootStructure, path);
|
|
193
|
-
// Must be a tree structure
|
|
194
|
-
if (structure.type !== 'struct') {
|
|
195
|
-
const pathStr = path.map(s => s.value).join('.');
|
|
196
|
-
throw new Error(`Path '${pathStr}' points to a dataset, not a tree`);
|
|
197
|
-
}
|
|
198
|
-
// Must be a tree ref
|
|
199
|
-
if (ref.type !== 'tree') {
|
|
200
|
-
const pathStr = path.map(s => s.value).join('.');
|
|
201
|
-
throw new Error(`Path '${pathStr}' has ref type '${ref.type}', expected 'tree'`);
|
|
202
|
-
}
|
|
203
|
-
// Read the tree and return field names
|
|
204
|
-
const treeObject = await treeRead(repoPath, ref.value, structure);
|
|
205
|
-
return Object.keys(treeObject);
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Read and decode a dataset value at a path within a package's data tree.
|
|
209
|
-
*
|
|
210
|
-
* @param repoPath - Path to .e3 repository
|
|
211
|
-
* @param name - Package name
|
|
212
|
-
* @param version - Package version
|
|
213
|
-
* @param path - Path to the dataset
|
|
214
|
-
* @returns The decoded dataset value
|
|
215
|
-
* @throws If package not found, path invalid, or path points to a tree
|
|
216
|
-
*/
|
|
217
|
-
export async function packageGetDataset(repoPath, name, version, path) {
|
|
218
|
-
// Read the package to get root structure and hash
|
|
219
|
-
const pkg = await packageRead(repoPath, name, version);
|
|
220
|
-
const rootStructure = pkg.data.structure;
|
|
221
|
-
const rootHash = pkg.data.value;
|
|
222
|
-
if (path.length === 0) {
|
|
223
|
-
throw new Error('Cannot get dataset at root path - root is always a tree');
|
|
224
|
-
}
|
|
225
|
-
// Traverse to the path
|
|
226
|
-
const { structure, ref } = await traverse(repoPath, rootHash, rootStructure, path);
|
|
227
|
-
// Must be a value structure
|
|
228
|
-
if (structure.type !== 'value') {
|
|
229
|
-
const pathStr = path.map(s => s.value).join('.');
|
|
230
|
-
throw new Error(`Path '${pathStr}' points to a tree, not a dataset`);
|
|
231
|
-
}
|
|
232
|
-
// Handle different ref types
|
|
233
|
-
if (ref.type === 'unassigned') {
|
|
234
|
-
throw new Error(`Dataset at path is unassigned (pending task output)`);
|
|
235
|
-
}
|
|
236
|
-
if (ref.type === 'null') {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
if (ref.type === 'tree') {
|
|
240
|
-
const pathStr = path.map(s => s.value).join('.');
|
|
241
|
-
throw new Error(`Path '${pathStr}' structure says value but ref is tree`);
|
|
242
|
-
}
|
|
243
|
-
// Read and return the dataset value
|
|
244
|
-
const result = await datasetRead(repoPath, ref.value);
|
|
245
|
-
return result.value;
|
|
113
|
+
return storage.objects.write(repo, data);
|
|
246
114
|
}
|
|
247
115
|
/**
|
|
248
116
|
* Update a dataset at a path within a workspace.
|
|
249
117
|
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
118
|
+
* Writes the value to the object store and updates the per-dataset ref file.
|
|
119
|
+
* Uses shared structure lock to allow concurrent writes.
|
|
252
120
|
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* @param repoPath - Path to .e3 repository
|
|
121
|
+
* @param storage - Storage backend
|
|
122
|
+
* @param repo - Repository identifier
|
|
257
123
|
* @param ws - Workspace name
|
|
258
124
|
* @param treePath - Path to the dataset
|
|
259
125
|
* @param value - The new value to write
|
|
@@ -262,375 +128,462 @@ export async function packageGetDataset(repoPath, name, version, path) {
|
|
|
262
128
|
* @throws {WorkspaceLockError} If workspace is locked by another process
|
|
263
129
|
* @throws If workspace not deployed, path invalid, or path points to a tree
|
|
264
130
|
*/
|
|
265
|
-
export async function workspaceSetDataset(
|
|
131
|
+
export async function workspaceSetDataset(storage, repo, ws, treePath, value, type, options = {}) {
|
|
266
132
|
if (treePath.length === 0) {
|
|
267
133
|
throw new Error('Cannot set dataset at root path - root is always a tree');
|
|
268
134
|
}
|
|
269
135
|
// Acquire lock if not provided externally
|
|
270
136
|
const externalLock = options.lock;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
await
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
137
|
+
let lock = externalLock ?? null;
|
|
138
|
+
if (!lock) {
|
|
139
|
+
lock = await storage.locks.acquire(repo, ws, variant('dataset_write', null), { mode: 'shared' });
|
|
140
|
+
if (!lock) {
|
|
141
|
+
const state = await storage.locks.getState(repo, ws);
|
|
142
|
+
throw new WorkspaceLockError(ws, state ? {
|
|
143
|
+
acquiredAt: state.acquiredAt.toISOString(),
|
|
144
|
+
operation: state.operation.type,
|
|
145
|
+
} : undefined);
|
|
279
146
|
}
|
|
280
147
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
148
|
+
try {
|
|
149
|
+
const wsState = await readWorkspaceState(storage, repo, ws);
|
|
150
|
+
// Read the deployed package object to get the structure
|
|
151
|
+
const pkgData = await storage.objects.read(repo, wsState.packageHash);
|
|
152
|
+
const decoder = decodeBeast2For(PackageObjectType);
|
|
153
|
+
const pkgObject = decoder(Buffer.from(pkgData));
|
|
154
|
+
const rootStructure = pkgObject.data.structure;
|
|
155
|
+
// Validate that the path leads to a value structure and check writable
|
|
156
|
+
let currentStructure = rootStructure;
|
|
157
|
+
for (let i = 0; i < treePath.length; i++) {
|
|
158
|
+
const segment = treePath[i];
|
|
159
|
+
if (segment.type !== 'field') {
|
|
160
|
+
throw new Error(`Unsupported path segment type: ${segment.type}`);
|
|
161
|
+
}
|
|
162
|
+
if (currentStructure.type !== 'struct') {
|
|
163
|
+
const pathSoFar = treePath.slice(0, i).map(s => s.value).join('.');
|
|
164
|
+
throw new Error(`Cannot descend into non-struct at path '${pathSoFar}'`);
|
|
165
|
+
}
|
|
166
|
+
const childStructure = currentStructure.value.get(segment.value);
|
|
167
|
+
if (!childStructure) {
|
|
168
|
+
const pathSoFar = treePath.slice(0, i).map(s => s.value).join('.');
|
|
169
|
+
const available = Array.from(currentStructure.value.keys()).join(', ');
|
|
170
|
+
throw new Error(`Field '${segment.value}' not found at '${pathSoFar}'. Available: ${available}`);
|
|
171
|
+
}
|
|
172
|
+
currentStructure = childStructure;
|
|
299
173
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
174
|
+
// Final structure must be a value
|
|
175
|
+
if (currentStructure.type !== 'value') {
|
|
176
|
+
const pathStr = treePath.map(s => s.value).join('.');
|
|
177
|
+
throw new Error(`Path '${pathStr}' points to a tree, not a dataset`);
|
|
303
178
|
}
|
|
304
|
-
|
|
305
|
-
if (!
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
throw new Error(`Field '${segment.value}' not found at '${pathSoFar}'. Available: ${available}`);
|
|
179
|
+
// Check writable flag
|
|
180
|
+
if (!currentStructure.value.writable) {
|
|
181
|
+
const pathStr = treePath.map(s => s.value).join('.');
|
|
182
|
+
throw new Error(`Dataset at '${pathStr}' is not writable`);
|
|
309
183
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
184
|
+
// Write the new dataset value to object store
|
|
185
|
+
const newValueHash = await datasetWrite(storage, repo, value, type);
|
|
186
|
+
// Build ref path from tree path
|
|
187
|
+
const refPath = treePath.map(s => s.value).join('/');
|
|
188
|
+
// Write the DatasetRef with empty version vector (will be populated by dataflow)
|
|
189
|
+
const datasetRef = variant('value', { hash: newValueHash, versions: new Map() });
|
|
190
|
+
await storage.datasets.write(repo, ws, refPath, datasetRef);
|
|
316
191
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
// Collect all tree hashes and structures along the path
|
|
322
|
-
const treeInfos = [];
|
|
323
|
-
let currentHash = state.rootHash;
|
|
324
|
-
currentStructure = rootStructure;
|
|
325
|
-
// Read all trees along the path (except the last segment which is the dataset)
|
|
326
|
-
for (let i = 0; i < treePath.length - 1; i++) {
|
|
327
|
-
treeInfos.push({ hash: currentHash, structure: currentStructure });
|
|
328
|
-
const segment = treePath[i];
|
|
329
|
-
const treeObject = await treeRead(repoPath, currentHash, currentStructure);
|
|
330
|
-
const childRef = treeObject[segment.value];
|
|
331
|
-
if (!childRef || childRef.type !== 'tree') {
|
|
332
|
-
throw new Error(`Expected tree ref at path segment ${i}`);
|
|
192
|
+
finally {
|
|
193
|
+
// Only release the lock if we acquired it internally
|
|
194
|
+
if (!externalLock) {
|
|
195
|
+
await lock.release();
|
|
333
196
|
}
|
|
334
|
-
currentHash = childRef.value;
|
|
335
|
-
currentStructure = currentStructure.value.get(segment.value);
|
|
336
|
-
}
|
|
337
|
-
// Add the final tree that contains the dataset
|
|
338
|
-
treeInfos.push({ hash: currentHash, structure: currentStructure });
|
|
339
|
-
// Now rebuild from leaf to root
|
|
340
|
-
// Start with the new value hash as the new ref
|
|
341
|
-
let newRef = { type: 'value', value: newValueHash };
|
|
342
|
-
for (let i = treeInfos.length - 1; i >= 0; i--) {
|
|
343
|
-
const { hash, structure } = treeInfos[i];
|
|
344
|
-
const fieldName = treePath[i].value;
|
|
345
|
-
// Read the current tree
|
|
346
|
-
const treeObject = await treeRead(repoPath, hash, structure);
|
|
347
|
-
// Create modified tree with the new ref
|
|
348
|
-
const newTreeObject = {
|
|
349
|
-
...treeObject,
|
|
350
|
-
[fieldName]: newRef,
|
|
351
|
-
};
|
|
352
|
-
// Write the new tree
|
|
353
|
-
const newTreeHash = await treeWrite(repoPath, newTreeObject, structure);
|
|
354
|
-
// This becomes the new ref for the parent
|
|
355
|
-
newRef = { type: 'tree', value: newTreeHash };
|
|
356
|
-
}
|
|
357
|
-
// The final newRef is always a tree ref pointing to the new root
|
|
358
|
-
// (because we start with a value ref and wrap it in tree refs bottom-up)
|
|
359
|
-
if (newRef.type !== 'tree' || newRef.value === null) {
|
|
360
|
-
throw new Error('Internal error: expected tree ref after rebuilding path');
|
|
361
197
|
}
|
|
362
|
-
const newRootHash = newRef.value;
|
|
363
|
-
// Update workspace state atomically
|
|
364
|
-
await writeWorkspaceState(repoPath, ws, {
|
|
365
|
-
...state,
|
|
366
|
-
rootHash: newRootHash,
|
|
367
|
-
rootUpdatedAt: new Date(),
|
|
368
|
-
});
|
|
369
198
|
}
|
|
370
199
|
// =============================================================================
|
|
371
200
|
// Workspace Helper Functions
|
|
372
201
|
// =============================================================================
|
|
373
|
-
/**
|
|
374
|
-
* Write workspace state to file atomically.
|
|
375
|
-
*/
|
|
376
|
-
async function writeWorkspaceState(repoPath, ws, state) {
|
|
377
|
-
const wsDir = path.join(repoPath, 'workspaces');
|
|
378
|
-
const stateFile = path.join(wsDir, `${ws}.beast2`);
|
|
379
|
-
// Ensure workspaces directory exists
|
|
380
|
-
await fs.mkdir(wsDir, { recursive: true });
|
|
381
|
-
const encoder = encodeBeast2For(WorkspaceStateType);
|
|
382
|
-
const data = encoder(state);
|
|
383
|
-
// Write atomically: write to temp file, then rename
|
|
384
|
-
const randomSuffix = Math.random().toString(36).slice(2, 10);
|
|
385
|
-
const tempPath = path.join(wsDir, `.${ws}.${Date.now()}.${randomSuffix}.tmp`);
|
|
386
|
-
await fs.writeFile(tempPath, data);
|
|
387
|
-
await fs.rename(tempPath, stateFile);
|
|
388
|
-
}
|
|
389
202
|
/**
|
|
390
203
|
* Read workspace state from file.
|
|
391
204
|
* @throws {WorkspaceNotFoundError} If workspace doesn't exist
|
|
392
205
|
* @throws {WorkspaceNotDeployedError} If workspace exists but not deployed
|
|
393
206
|
*/
|
|
394
|
-
async function readWorkspaceState(
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (data.length === 0) {
|
|
399
|
-
throw new WorkspaceNotDeployedError(ws);
|
|
400
|
-
}
|
|
401
|
-
const decoder = decodeBeast2For(WorkspaceStateType);
|
|
402
|
-
return decoder(data);
|
|
207
|
+
async function readWorkspaceState(storage, repo, ws) {
|
|
208
|
+
const data = await storage.refs.workspaceRead(repo, ws);
|
|
209
|
+
if (data === null) {
|
|
210
|
+
throw new WorkspaceNotFoundError(ws);
|
|
403
211
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
throw err;
|
|
407
|
-
if (isNotFoundError(err)) {
|
|
408
|
-
throw new WorkspaceNotFoundError(ws);
|
|
409
|
-
}
|
|
410
|
-
throw err;
|
|
212
|
+
if (data.length === 0) {
|
|
213
|
+
throw new WorkspaceNotDeployedError(ws);
|
|
411
214
|
}
|
|
215
|
+
const decoder = decodeBeast2For(WorkspaceStateType);
|
|
216
|
+
return decoder(data);
|
|
412
217
|
}
|
|
413
218
|
/**
|
|
414
|
-
* Get root structure
|
|
219
|
+
* Get root structure for a workspace.
|
|
415
220
|
* Reads the deployed package object to get the structure.
|
|
416
221
|
*/
|
|
417
|
-
async function
|
|
418
|
-
const
|
|
222
|
+
async function getWorkspaceStructure(storage, repo, ws) {
|
|
223
|
+
const wsState = await readWorkspaceState(storage, repo, ws);
|
|
419
224
|
// Read the deployed package object using the stored hash
|
|
420
|
-
const pkgData = await
|
|
225
|
+
const pkgData = await storage.objects.read(repo, wsState.packageHash);
|
|
421
226
|
const decoder = decodeBeast2For(PackageObjectType);
|
|
422
227
|
const pkgObject = decoder(Buffer.from(pkgData));
|
|
423
228
|
return {
|
|
424
|
-
rootHash: state.rootHash,
|
|
425
229
|
rootStructure: pkgObject.data.structure,
|
|
426
230
|
};
|
|
427
231
|
}
|
|
428
232
|
// =============================================================================
|
|
429
|
-
// Workspace High-level Operations (by path)
|
|
233
|
+
// Workspace High-level Operations (by path) - Using per-dataset refs
|
|
430
234
|
// =============================================================================
|
|
431
235
|
/**
|
|
432
236
|
* List field names at a tree path within a workspace's data tree.
|
|
433
237
|
*
|
|
434
|
-
*
|
|
238
|
+
* Uses the structure to determine available fields (no tree traversal needed).
|
|
239
|
+
*
|
|
240
|
+
* @param storage - Storage backend
|
|
241
|
+
* @param repo - Repository identifier
|
|
435
242
|
* @param ws - Workspace name
|
|
436
|
-
* @param
|
|
243
|
+
* @param treePath - Path to the tree node
|
|
437
244
|
* @returns Array of field names at the path
|
|
438
245
|
* @throws If workspace not deployed, path invalid, or path points to a dataset
|
|
439
246
|
*/
|
|
440
|
-
export async function workspaceListTree(
|
|
441
|
-
const {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
247
|
+
export async function workspaceListTree(storage, repo, ws, treePath) {
|
|
248
|
+
const { rootStructure } = await getWorkspaceStructure(storage, repo, ws);
|
|
249
|
+
// Navigate structure to find the target node
|
|
250
|
+
let currentStructure = rootStructure;
|
|
251
|
+
for (let i = 0; i < treePath.length; i++) {
|
|
252
|
+
const segment = treePath[i];
|
|
253
|
+
if (segment.type !== 'field') {
|
|
254
|
+
throw new Error(`Unsupported path segment type: ${segment.type}`);
|
|
255
|
+
}
|
|
256
|
+
if (currentStructure.type !== 'struct') {
|
|
257
|
+
const pathStr = treePath.slice(0, i).map(s => s.value).join('.');
|
|
258
|
+
throw new Error(`Path '${pathStr}' points to a dataset, not a tree`);
|
|
259
|
+
}
|
|
260
|
+
const childStructure = currentStructure.value.get(segment.value);
|
|
261
|
+
if (!childStructure) {
|
|
262
|
+
throw new Error(`Field '${segment.value}' not found in structure`);
|
|
446
263
|
}
|
|
447
|
-
|
|
448
|
-
return Object.keys(treeObject);
|
|
264
|
+
currentStructure = childStructure;
|
|
449
265
|
}
|
|
450
|
-
|
|
451
|
-
const { structure, ref } = await traverse(repoPath, rootHash, rootStructure, treePath);
|
|
452
|
-
// Must be a tree structure
|
|
453
|
-
if (structure.type !== 'struct') {
|
|
266
|
+
if (currentStructure.type !== 'struct') {
|
|
454
267
|
const pathStr = treePath.map(s => s.value).join('.');
|
|
455
268
|
throw new Error(`Path '${pathStr}' points to a dataset, not a tree`);
|
|
456
269
|
}
|
|
457
|
-
|
|
458
|
-
if (ref.type !== 'tree') {
|
|
459
|
-
const pathStr = treePath.map(s => s.value).join('.');
|
|
460
|
-
throw new Error(`Path '${pathStr}' has ref type '${ref.type}', expected 'tree'`);
|
|
461
|
-
}
|
|
462
|
-
// Read the tree and return field names
|
|
463
|
-
const treeObject = await treeRead(repoPath, ref.value, structure);
|
|
464
|
-
return Object.keys(treeObject);
|
|
270
|
+
return Array.from(currentStructure.value.keys());
|
|
465
271
|
}
|
|
466
272
|
/**
|
|
467
273
|
* Read and decode a dataset value at a path within a workspace's data tree.
|
|
468
274
|
*
|
|
469
|
-
*
|
|
275
|
+
* Reads the per-dataset ref file to get the value hash, then decodes the value.
|
|
276
|
+
*
|
|
277
|
+
* @param storage - Storage backend
|
|
278
|
+
* @param repo - Repository identifier
|
|
470
279
|
* @param ws - Workspace name
|
|
471
|
-
* @param
|
|
280
|
+
* @param treePath - Path to the dataset
|
|
472
281
|
* @returns The decoded dataset value
|
|
473
282
|
* @throws If workspace not deployed, path invalid, or path points to a tree
|
|
474
283
|
*/
|
|
475
|
-
export async function workspaceGetDataset(
|
|
476
|
-
const { rootHash, rootStructure } = await getWorkspaceRootInfo(repoPath, ws);
|
|
284
|
+
export async function workspaceGetDataset(storage, repo, ws, treePath) {
|
|
477
285
|
if (treePath.length === 0) {
|
|
478
286
|
throw new Error('Cannot get dataset at root path - root is always a tree');
|
|
479
287
|
}
|
|
480
|
-
//
|
|
481
|
-
const {
|
|
482
|
-
|
|
483
|
-
|
|
288
|
+
// Validate path against structure
|
|
289
|
+
const { rootStructure } = await getWorkspaceStructure(storage, repo, ws);
|
|
290
|
+
let currentStructure = rootStructure;
|
|
291
|
+
for (let i = 0; i < treePath.length; i++) {
|
|
292
|
+
const segment = treePath[i];
|
|
293
|
+
if (segment.type !== 'field')
|
|
294
|
+
throw new Error(`Unsupported path segment type: ${segment.type}`);
|
|
295
|
+
if (currentStructure.type !== 'struct')
|
|
296
|
+
throw new Error(`Cannot descend into non-struct`);
|
|
297
|
+
const child = currentStructure.value.get(segment.value);
|
|
298
|
+
if (!child)
|
|
299
|
+
throw new Error(`Field '${segment.value}' not found in structure`);
|
|
300
|
+
currentStructure = child;
|
|
301
|
+
}
|
|
302
|
+
if (currentStructure.type !== 'value') {
|
|
484
303
|
const pathStr = treePath.map(s => s.value).join('.');
|
|
485
304
|
throw new Error(`Path '${pathStr}' points to a tree, not a dataset`);
|
|
486
305
|
}
|
|
487
|
-
//
|
|
488
|
-
|
|
306
|
+
// Read the ref file
|
|
307
|
+
const refPath = treePath.map(s => s.value).join('/');
|
|
308
|
+
const ref = await storage.datasets.read(repo, ws, refPath);
|
|
309
|
+
if (!ref || ref.type === 'unassigned') {
|
|
489
310
|
throw new Error(`Dataset at path is unassigned (pending task output)`);
|
|
490
311
|
}
|
|
491
312
|
if (ref.type === 'null') {
|
|
492
313
|
return null;
|
|
493
314
|
}
|
|
494
|
-
if (ref.type === 'tree') {
|
|
495
|
-
const pathStr = treePath.map(s => s.value).join('.');
|
|
496
|
-
throw new Error(`Path '${pathStr}' structure says value but ref is tree`);
|
|
497
|
-
}
|
|
498
315
|
// Read and return the dataset value
|
|
499
|
-
const result = await datasetRead(
|
|
316
|
+
const result = await datasetRead(storage, repo, ref.value.hash);
|
|
500
317
|
return result.value;
|
|
501
318
|
}
|
|
502
319
|
/**
|
|
503
320
|
* Get the hash of a dataset at a path within a workspace's data tree.
|
|
504
321
|
*
|
|
505
|
-
*
|
|
506
|
-
* hash reference. Useful for dataflow execution which operates on hashes.
|
|
322
|
+
* Reads the per-dataset ref file directly. No tree traversal needed.
|
|
507
323
|
*
|
|
508
|
-
* @param
|
|
324
|
+
* @param storage - Storage backend
|
|
325
|
+
* @param repo - Repository identifier
|
|
509
326
|
* @param ws - Workspace name
|
|
510
327
|
* @param treePath - Path to the dataset
|
|
511
328
|
* @returns Object with ref type and hash (null for unassigned/null refs)
|
|
512
329
|
* @throws If workspace not deployed, path invalid, or path points to a tree
|
|
513
330
|
*/
|
|
514
|
-
export async function workspaceGetDatasetHash(
|
|
515
|
-
const { rootHash, rootStructure } = await getWorkspaceRootInfo(repoPath, ws);
|
|
331
|
+
export async function workspaceGetDatasetHash(storage, repo, ws, treePath) {
|
|
516
332
|
if (treePath.length === 0) {
|
|
517
333
|
throw new Error('Cannot get dataset at root path - root is always a tree');
|
|
518
334
|
}
|
|
519
|
-
//
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
throw new Error(`Path '${pathStr}' points to a tree, not a dataset`);
|
|
525
|
-
}
|
|
526
|
-
// Return ref type and hash
|
|
527
|
-
if (ref.type === 'unassigned' || ref.type === 'null') {
|
|
528
|
-
return { refType: ref.type, hash: null };
|
|
335
|
+
// Read the ref file directly using the path
|
|
336
|
+
const refPath = treePath.map(s => s.value).join('/');
|
|
337
|
+
const ref = await storage.datasets.read(repo, ws, refPath);
|
|
338
|
+
if (!ref || ref.type === 'unassigned') {
|
|
339
|
+
return { refType: 'unassigned', hash: null };
|
|
529
340
|
}
|
|
530
|
-
if (ref.type === '
|
|
531
|
-
|
|
532
|
-
throw new Error(`Path '${pathStr}' structure says value but ref is tree`);
|
|
341
|
+
if (ref.type === 'null') {
|
|
342
|
+
return { refType: 'null', hash: null };
|
|
533
343
|
}
|
|
534
|
-
return { refType:
|
|
344
|
+
return { refType: 'value', hash: ref.value.hash };
|
|
535
345
|
}
|
|
536
346
|
/**
|
|
537
347
|
* Set a dataset at a path within a workspace using a pre-computed hash.
|
|
538
348
|
*
|
|
539
|
-
*
|
|
540
|
-
* directly. Useful for dataflow execution which already has the output hash.
|
|
349
|
+
* Writes a DatasetRef file directly. No tree path-copy needed.
|
|
541
350
|
*
|
|
542
351
|
* IMPORTANT: This function does NOT acquire a workspace lock. The caller must
|
|
543
|
-
* hold
|
|
544
|
-
*
|
|
545
|
-
*
|
|
352
|
+
* hold a lock on the workspace before calling this function. This
|
|
353
|
+
* is typically used by dataflowExecute which holds the lock for the entire
|
|
354
|
+
* execution.
|
|
546
355
|
*
|
|
547
|
-
* @param
|
|
356
|
+
* @param storage - Storage backend
|
|
357
|
+
* @param repo - Repository identifier
|
|
548
358
|
* @param ws - Workspace name
|
|
549
359
|
* @param treePath - Path to the dataset
|
|
550
360
|
* @param valueHash - Hash of the dataset value already in the object store
|
|
551
361
|
* @throws If workspace not deployed, path invalid, or path points to a tree
|
|
552
362
|
*/
|
|
553
|
-
export async function workspaceSetDatasetByHash(
|
|
363
|
+
export async function workspaceSetDatasetByHash(storage, repo, ws, treePath, valueHash, versions) {
|
|
554
364
|
if (treePath.length === 0) {
|
|
555
365
|
throw new Error('Cannot set dataset at root path - root is always a tree');
|
|
556
366
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
367
|
+
// Write the DatasetRef directly
|
|
368
|
+
const refPath = treePath.map(s => s.value).join('/');
|
|
369
|
+
const datasetRef = variant('value', { hash: valueHash, versions });
|
|
370
|
+
await storage.datasets.write(repo, ws, refPath, datasetRef);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Get the status of a single dataset at a path within a workspace.
|
|
374
|
+
*
|
|
375
|
+
* Returns the ref type, hash, East type, and size without downloading the value.
|
|
376
|
+
*
|
|
377
|
+
* @param storage - Storage backend
|
|
378
|
+
* @param repo - Repository identifier
|
|
379
|
+
* @param ws - Workspace name
|
|
380
|
+
* @param treePath - Path to the dataset
|
|
381
|
+
* @returns Dataset status including ref type, hash, type, and size
|
|
382
|
+
* @throws If workspace not deployed, path invalid, or path points to a tree
|
|
383
|
+
*/
|
|
384
|
+
export async function workspaceGetDatasetStatus(storage, repo, ws, treePath) {
|
|
385
|
+
if (treePath.length === 0) {
|
|
386
|
+
throw new Error('Cannot get dataset status at root path - root is always a tree');
|
|
387
|
+
}
|
|
388
|
+
// Validate path and get type from structure
|
|
389
|
+
const { rootStructure } = await getWorkspaceStructure(storage, repo, ws);
|
|
564
390
|
let currentStructure = rootStructure;
|
|
565
391
|
for (let i = 0; i < treePath.length; i++) {
|
|
566
392
|
const segment = treePath[i];
|
|
567
|
-
if (segment.type !== 'field')
|
|
393
|
+
if (segment.type !== 'field')
|
|
568
394
|
throw new Error(`Unsupported path segment type: ${segment.type}`);
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
if (!childStructure) {
|
|
576
|
-
const pathSoFar = treePath.slice(0, i).map(s => s.value).join('.');
|
|
577
|
-
const available = Array.from(currentStructure.value.keys()).join(', ');
|
|
578
|
-
throw new Error(`Field '${segment.value}' not found at '${pathSoFar}'. Available: ${available}`);
|
|
579
|
-
}
|
|
580
|
-
currentStructure = childStructure;
|
|
395
|
+
if (currentStructure.type !== 'struct')
|
|
396
|
+
throw new Error('Cannot descend into non-struct');
|
|
397
|
+
const child = currentStructure.value.get(segment.value);
|
|
398
|
+
if (!child)
|
|
399
|
+
throw new Error(`Field '${segment.value}' not found`);
|
|
400
|
+
currentStructure = child;
|
|
581
401
|
}
|
|
582
|
-
// Final structure must be a value
|
|
583
402
|
if (currentStructure.type !== 'value') {
|
|
584
403
|
const pathStr = treePath.map(s => s.value).join('.');
|
|
585
404
|
throw new Error(`Path '${pathStr}' points to a tree, not a dataset`);
|
|
586
405
|
}
|
|
587
|
-
|
|
588
|
-
//
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
406
|
+
const datasetType = currentStructure.value.type;
|
|
407
|
+
// Read the ref file
|
|
408
|
+
const refPath = treePath.map(s => s.value).join('/');
|
|
409
|
+
const ref = await storage.datasets.read(repo, ws, refPath);
|
|
410
|
+
if (!ref || ref.type === 'unassigned') {
|
|
411
|
+
return { refType: 'unassigned', hash: null, datasetType, size: null };
|
|
412
|
+
}
|
|
413
|
+
if (ref.type === 'null') {
|
|
414
|
+
return { refType: 'null', hash: null, datasetType, size: 0 };
|
|
415
|
+
}
|
|
416
|
+
// value ref - get size from object store
|
|
417
|
+
const { size } = await storage.objects.stat(repo, ref.value.hash);
|
|
418
|
+
return { refType: 'value', hash: ref.value.hash, datasetType, size };
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Check if a structure represents a task (has function_ir and output).
|
|
422
|
+
*/
|
|
423
|
+
function isTaskStructure(structure) {
|
|
424
|
+
if (structure.type !== 'struct')
|
|
425
|
+
return false;
|
|
426
|
+
const fields = structure.value;
|
|
427
|
+
return fields.has('function_ir') && fields.has('output');
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Get the output type from a task structure (from structure, not value).
|
|
431
|
+
*/
|
|
432
|
+
function getTaskOutputTypeFromStructure(structure) {
|
|
433
|
+
if (structure.type !== 'struct')
|
|
434
|
+
return undefined;
|
|
435
|
+
const outputStructure = structure.value.get('output');
|
|
436
|
+
if (outputStructure?.type === 'value') {
|
|
437
|
+
return outputStructure.value.type;
|
|
438
|
+
}
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Get the full tree structure at a path within a workspace.
|
|
443
|
+
*
|
|
444
|
+
* Recursively walks the structure and ref files to build a hierarchical
|
|
445
|
+
* structure suitable for display. Tasks are shown as leaves with their output type.
|
|
446
|
+
*
|
|
447
|
+
* @param storage - Storage backend
|
|
448
|
+
* @param repo - Repository identifier
|
|
449
|
+
* @param ws - Workspace name
|
|
450
|
+
* @param treePath - Path to start from (empty for root)
|
|
451
|
+
* @param options - Optional settings for depth limit and type inclusion
|
|
452
|
+
* @returns Array of tree nodes at the path
|
|
453
|
+
* @throws If workspace not deployed or path invalid
|
|
454
|
+
*/
|
|
455
|
+
export async function workspaceGetTree(storage, repo, ws, treePath, options = {}) {
|
|
456
|
+
const { rootStructure } = await getWorkspaceStructure(storage, repo, ws);
|
|
457
|
+
const { maxDepth, includeTypes, includeStatus } = options;
|
|
458
|
+
// Navigate to the target structure
|
|
459
|
+
let targetStructure = rootStructure;
|
|
460
|
+
let pathPrefix = '';
|
|
461
|
+
for (const segment of treePath) {
|
|
462
|
+
if (segment.type !== 'field')
|
|
463
|
+
throw new Error(`Unsupported path segment type: ${segment.type}`);
|
|
464
|
+
if (targetStructure.type !== 'struct')
|
|
465
|
+
throw new Error('Cannot descend into non-struct');
|
|
466
|
+
const child = targetStructure.value.get(segment.value);
|
|
467
|
+
if (!child)
|
|
468
|
+
throw new Error(`Field '${segment.value}' not found`);
|
|
469
|
+
pathPrefix = pathPrefix ? `${pathPrefix}/${segment.value}` : segment.value;
|
|
470
|
+
targetStructure = child;
|
|
471
|
+
}
|
|
472
|
+
if (targetStructure.type !== 'struct') {
|
|
473
|
+
const pathStr = treePath.map(s => s.value).join('.');
|
|
474
|
+
throw new Error(`Path '${pathStr}' points to a dataset, not a tree`);
|
|
603
475
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
476
|
+
return walkStructure(storage, repo, ws, targetStructure, pathPrefix, 0, maxDepth, includeTypes, includeStatus);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Recursively walk structure and build TreeNode array using ref files.
|
|
480
|
+
*/
|
|
481
|
+
async function walkStructure(storage, repo, ws, structure, pathPrefix, currentDepth, maxDepth, includeTypes, includeStatus) {
|
|
482
|
+
if (structure.type !== 'struct') {
|
|
483
|
+
throw new Error('Expected struct structure for tree walk');
|
|
484
|
+
}
|
|
485
|
+
const entries = Array.from(structure.value.entries());
|
|
486
|
+
const nodes = await Promise.all(entries.map(async ([fieldName, childStructure]) => {
|
|
487
|
+
const childPath = pathPrefix ? `${pathPrefix}/${fieldName}` : fieldName;
|
|
488
|
+
if (childStructure.type === 'value') {
|
|
489
|
+
// Dataset (leaf node)
|
|
490
|
+
const node = {
|
|
491
|
+
name: fieldName,
|
|
492
|
+
kind: 'dataset',
|
|
493
|
+
datasetType: includeTypes ? childStructure.value.type : undefined,
|
|
494
|
+
};
|
|
495
|
+
if (includeStatus) {
|
|
496
|
+
const ref = await storage.datasets.read(repo, ws, childPath);
|
|
497
|
+
if (!ref || ref.type === 'unassigned') {
|
|
498
|
+
node.refType = 'unassigned';
|
|
499
|
+
}
|
|
500
|
+
else if (ref.type === 'null') {
|
|
501
|
+
node.refType = 'null';
|
|
502
|
+
node.size = 0;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
node.refType = 'value';
|
|
506
|
+
node.hash = ref.value.hash;
|
|
507
|
+
const { size } = await storage.objects.stat(repo, ref.value.hash);
|
|
508
|
+
node.size = size;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return node;
|
|
512
|
+
}
|
|
513
|
+
// childStructure.type === 'struct'
|
|
514
|
+
// Task subtree — show as leaf with output type
|
|
515
|
+
if (isTaskStructure(childStructure)) {
|
|
516
|
+
const node = {
|
|
517
|
+
name: fieldName,
|
|
518
|
+
kind: 'dataset',
|
|
519
|
+
datasetType: includeTypes ? getTaskOutputTypeFromStructure(childStructure) : undefined,
|
|
520
|
+
};
|
|
521
|
+
if (includeStatus) {
|
|
522
|
+
// Read the output ref for the task
|
|
523
|
+
const outputRefPath = `${childPath}/output`;
|
|
524
|
+
const outputRef = await storage.datasets.read(repo, ws, outputRefPath);
|
|
525
|
+
if (!outputRef || outputRef.type === 'unassigned') {
|
|
526
|
+
node.refType = 'unassigned';
|
|
527
|
+
}
|
|
528
|
+
else if (outputRef.type === 'null') {
|
|
529
|
+
node.refType = 'null';
|
|
530
|
+
node.size = 0;
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
node.refType = 'value';
|
|
534
|
+
node.hash = outputRef.value.hash;
|
|
535
|
+
const { size } = await storage.objects.stat(repo, outputRef.value.hash);
|
|
536
|
+
node.size = size;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return node;
|
|
540
|
+
}
|
|
541
|
+
// Regular subtree
|
|
542
|
+
let children = [];
|
|
543
|
+
if (maxDepth === undefined || currentDepth < maxDepth) {
|
|
544
|
+
children = await walkStructure(storage, repo, ws, childStructure, childPath, currentDepth + 1, maxDepth, includeTypes, includeStatus);
|
|
545
|
+
}
|
|
546
|
+
return { name: fieldName, kind: 'tree', children };
|
|
547
|
+
}));
|
|
548
|
+
// Sort alphabetically for consistent output
|
|
549
|
+
nodes.sort((a, b) => a.name.localeCompare(b.name));
|
|
550
|
+
return nodes;
|
|
551
|
+
}
|
|
552
|
+
// =============================================================================
|
|
553
|
+
// Package Operations (still use tree traversal for compatibility)
|
|
554
|
+
// =============================================================================
|
|
555
|
+
/**
|
|
556
|
+
* List field names at a tree path within a package's data tree.
|
|
557
|
+
*
|
|
558
|
+
* Note: In the new format, packages store per-dataset refs in data/ dir
|
|
559
|
+
* rather than tree objects. This function uses the structure directly.
|
|
560
|
+
*
|
|
561
|
+
* @param storage - Storage backend
|
|
562
|
+
* @param repo - Repository identifier
|
|
563
|
+
* @param name - Package name
|
|
564
|
+
* @param version - Package version
|
|
565
|
+
* @param path - Path to the tree node
|
|
566
|
+
* @returns Array of field names at the path
|
|
567
|
+
* @throws If package not found, path invalid, or path points to a dataset
|
|
568
|
+
*/
|
|
569
|
+
export async function packageListTree(storage, repo, name, version, path) {
|
|
570
|
+
const pkg = await packageRead(storage, repo, name, version);
|
|
571
|
+
const rootStructure = pkg.data.structure;
|
|
572
|
+
let currentStructure = rootStructure;
|
|
573
|
+
for (let i = 0; i < path.length; i++) {
|
|
574
|
+
const segment = path[i];
|
|
575
|
+
if (segment.type !== 'field')
|
|
576
|
+
throw new Error(`Unsupported path segment type: ${segment.type}`);
|
|
577
|
+
if (currentStructure.type !== 'struct')
|
|
578
|
+
throw new Error('Path points to a dataset, not a tree');
|
|
579
|
+
const child = currentStructure.value.get(segment.value);
|
|
580
|
+
if (!child)
|
|
581
|
+
throw new Error(`Field '${segment.value}' not found`);
|
|
582
|
+
currentStructure = child;
|
|
623
583
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
throw new Error('Internal error: expected tree ref after rebuilding path');
|
|
584
|
+
if (currentStructure.type !== 'struct') {
|
|
585
|
+
throw new Error('Path points to a dataset, not a tree');
|
|
627
586
|
}
|
|
628
|
-
|
|
629
|
-
// Update workspace state atomically
|
|
630
|
-
await writeWorkspaceState(repoPath, ws, {
|
|
631
|
-
...state,
|
|
632
|
-
rootHash: newRootHash,
|
|
633
|
-
rootUpdatedAt: new Date(),
|
|
634
|
-
});
|
|
587
|
+
return Array.from(currentStructure.value.keys());
|
|
635
588
|
}
|
|
636
589
|
//# sourceMappingURL=trees.js.map
|