@elaraai/e3-core 0.0.2-beta.35 → 0.0.2-beta.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/dataflow/api-compat.d.ts.map +1 -1
- package/dist/src/dataflow/api-compat.js +6 -1
- package/dist/src/dataflow/api-compat.js.map +1 -1
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts +22 -4
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts.map +1 -1
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.js +353 -79
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.js.map +1 -1
- package/dist/src/dataflow/orchestrator/interfaces.d.ts +6 -0
- package/dist/src/dataflow/orchestrator/interfaces.d.ts.map +1 -1
- package/dist/src/dataflow/orchestrator/interfaces.js +1 -0
- package/dist/src/dataflow/orchestrator/interfaces.js.map +1 -1
- package/dist/src/dataflow/steps.d.ts +74 -28
- package/dist/src/dataflow/steps.d.ts.map +1 -1
- package/dist/src/dataflow/steps.js +221 -42
- package/dist/src/dataflow/steps.js.map +1 -1
- package/dist/src/dataflow/types.d.ts +13 -2
- package/dist/src/dataflow/types.d.ts.map +1 -1
- package/dist/src/dataflow.d.ts +37 -95
- package/dist/src/dataflow.d.ts.map +1 -1
- package/dist/src/dataflow.js +121 -631
- 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/execution/MockTaskRunner.d.ts +1 -1
- package/dist/src/execution/MockTaskRunner.d.ts.map +1 -1
- package/dist/src/execution/MockTaskRunner.js +1 -2
- package/dist/src/execution/MockTaskRunner.js.map +1 -1
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/packages.d.ts.map +1 -1
- package/dist/src/packages.js +20 -7
- package/dist/src/packages.js.map +1 -1
- package/dist/src/storage/in-memory/InMemoryStorage.d.ts +26 -4
- package/dist/src/storage/in-memory/InMemoryStorage.d.ts.map +1 -1
- package/dist/src/storage/in-memory/InMemoryStorage.js +104 -21
- package/dist/src/storage/in-memory/InMemoryStorage.js.map +1 -1
- package/dist/src/storage/index.d.ts +2 -2
- package/dist/src/storage/index.d.ts.map +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/index.js.map +1 -1
- package/dist/src/storage/interfaces.d.ts +52 -1
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/local/LocalBackend.d.ts +3 -1
- package/dist/src/storage/local/LocalBackend.d.ts.map +1 -1
- package/dist/src/storage/local/LocalBackend.js +5 -1
- package/dist/src/storage/local/LocalBackend.js.map +1 -1
- 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 +6 -0
- package/dist/src/storage/local/LocalLockService.d.ts.map +1 -1
- package/dist/src/storage/local/LocalLockService.js +17 -4
- package/dist/src/storage/local/LocalLockService.js.map +1 -1
- package/dist/src/storage/local/LocalRepoStore.d.ts +4 -2
- package/dist/src/storage/local/LocalRepoStore.d.ts.map +1 -1
- package/dist/src/storage/local/LocalRepoStore.js +14 -2
- package/dist/src/storage/local/LocalRepoStore.js.map +1 -1
- package/dist/src/storage/local/gc.d.ts.map +1 -1
- package/dist/src/storage/local/gc.js +8 -1
- package/dist/src/storage/local/gc.js.map +1 -1
- package/dist/src/storage/local/index.d.ts +1 -0
- package/dist/src/storage/local/index.d.ts.map +1 -1
- package/dist/src/storage/local/index.js +1 -0
- package/dist/src/storage/local/index.js.map +1 -1
- package/dist/src/trees.d.ts +35 -43
- package/dist/src/trees.d.ts.map +1 -1
- package/dist/src/trees.js +228 -449
- package/dist/src/trees.js.map +1 -1
- package/dist/src/workspaces.d.ts +6 -27
- package/dist/src/workspaces.d.ts.map +1 -1
- package/dist/src/workspaces.js +42 -55
- package/dist/src/workspaces.js.map +1 -1
- package/package.json +1 -1
package/dist/src/trees.js
CHANGED
|
@@ -12,8 +12,11 @@
|
|
|
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
21
|
import { decodeBeast2, decodeBeast2For, encodeBeast2For, StructType, variant, } from '@elaraai/east';
|
|
19
22
|
import { DataRefType, PackageObjectType, WorkspaceStateType } from '@elaraai/e3-types';
|
|
@@ -109,152 +112,11 @@ export async function datasetWrite(storage, repo, value, type) {
|
|
|
109
112
|
const data = encoder(value);
|
|
110
113
|
return storage.objects.write(repo, data);
|
|
111
114
|
}
|
|
112
|
-
/**
|
|
113
|
-
* Traverse a tree from root to a path, co-walking structure and data.
|
|
114
|
-
*
|
|
115
|
-
* @param storage - Storage backend
|
|
116
|
-
* @param repo - Repository identifier
|
|
117
|
-
* @param rootHash - Hash of the root tree object
|
|
118
|
-
* @param rootStructure - Structure of the root tree
|
|
119
|
-
* @param path - Path to traverse
|
|
120
|
-
* @returns The structure and DataRef at the path location
|
|
121
|
-
* @throws If path is invalid or traversal fails
|
|
122
|
-
*/
|
|
123
|
-
async function traverse(storage, repo, rootHash, rootStructure, path) {
|
|
124
|
-
let currentStructure = rootStructure;
|
|
125
|
-
let currentHash = rootHash;
|
|
126
|
-
for (let i = 0; i < path.length; i++) {
|
|
127
|
-
const segment = path[i];
|
|
128
|
-
if (segment.type !== 'field') {
|
|
129
|
-
throw new Error(`Unsupported path segment type: ${segment.type}`);
|
|
130
|
-
}
|
|
131
|
-
const fieldName = segment.value;
|
|
132
|
-
// Current structure must be a struct tree to descend into
|
|
133
|
-
if (currentStructure.type !== 'struct') {
|
|
134
|
-
const pathSoFar = path.slice(0, i).map(s => s.value).join('.');
|
|
135
|
-
throw new Error(`Cannot descend into non-struct at path '${pathSoFar}'`);
|
|
136
|
-
}
|
|
137
|
-
// Read the current tree object
|
|
138
|
-
const treeObject = await treeRead(storage, repo, currentHash, currentStructure);
|
|
139
|
-
// Look up the child ref
|
|
140
|
-
const childRef = treeObject[fieldName];
|
|
141
|
-
if (!childRef) {
|
|
142
|
-
const pathSoFar = path.slice(0, i).map(s => s.value).join('.');
|
|
143
|
-
const available = Object.keys(treeObject).join(', ');
|
|
144
|
-
throw new Error(`Field '${fieldName}' not found at '${pathSoFar}'. Available: ${available}`);
|
|
145
|
-
}
|
|
146
|
-
// Look up the child structure
|
|
147
|
-
const childStructure = currentStructure.value.get(fieldName);
|
|
148
|
-
if (!childStructure) {
|
|
149
|
-
throw new Error(`Field '${fieldName}' not found in structure`);
|
|
150
|
-
}
|
|
151
|
-
// If this is the last segment, return the result
|
|
152
|
-
if (i === path.length - 1) {
|
|
153
|
-
return { structure: childStructure, ref: childRef };
|
|
154
|
-
}
|
|
155
|
-
// Otherwise, continue traversing (must be a tree ref)
|
|
156
|
-
if (childRef.type !== 'tree') {
|
|
157
|
-
const pathSoFar = path.slice(0, i + 1).map(s => s.value).join('.');
|
|
158
|
-
throw new Error(`Expected tree ref at '${pathSoFar}', got '${childRef.type}'`);
|
|
159
|
-
}
|
|
160
|
-
currentStructure = childStructure;
|
|
161
|
-
currentHash = childRef.value;
|
|
162
|
-
}
|
|
163
|
-
// Empty path - return root
|
|
164
|
-
return {
|
|
165
|
-
structure: rootStructure,
|
|
166
|
-
ref: { type: 'tree', value: rootHash },
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* List field names at a tree path within a package's data tree.
|
|
171
|
-
*
|
|
172
|
-
* @param storage - Storage backend
|
|
173
|
-
* @param repo - Repository identifier
|
|
174
|
-
* @param name - Package name
|
|
175
|
-
* @param version - Package version
|
|
176
|
-
* @param path - Path to the tree node
|
|
177
|
-
* @returns Array of field names at the path
|
|
178
|
-
* @throws If package not found, path invalid, or path points to a dataset
|
|
179
|
-
*/
|
|
180
|
-
export async function packageListTree(storage, repo, name, version, path) {
|
|
181
|
-
// Read the package to get root structure and hash
|
|
182
|
-
const pkg = await packageRead(storage, repo, name, version);
|
|
183
|
-
const rootStructure = pkg.data.structure;
|
|
184
|
-
const rootHash = pkg.data.value;
|
|
185
|
-
if (path.length === 0) {
|
|
186
|
-
// Empty path - list root tree fields
|
|
187
|
-
if (rootStructure.type !== 'struct') {
|
|
188
|
-
throw new Error('Root is not a tree');
|
|
189
|
-
}
|
|
190
|
-
const treeObject = await treeRead(storage, repo, rootHash, rootStructure);
|
|
191
|
-
return Object.keys(treeObject);
|
|
192
|
-
}
|
|
193
|
-
// Traverse to the path
|
|
194
|
-
const { structure, ref } = await traverse(storage, repo, rootHash, rootStructure, path);
|
|
195
|
-
// Must be a tree structure
|
|
196
|
-
if (structure.type !== 'struct') {
|
|
197
|
-
const pathStr = path.map(s => s.value).join('.');
|
|
198
|
-
throw new Error(`Path '${pathStr}' points to a dataset, not a tree`);
|
|
199
|
-
}
|
|
200
|
-
// Must be a tree ref
|
|
201
|
-
if (ref.type !== 'tree') {
|
|
202
|
-
const pathStr = path.map(s => s.value).join('.');
|
|
203
|
-
throw new Error(`Path '${pathStr}' has ref type '${ref.type}', expected 'tree'`);
|
|
204
|
-
}
|
|
205
|
-
// Read the tree and return field names
|
|
206
|
-
const treeObject = await treeRead(storage, repo, ref.value, structure);
|
|
207
|
-
return Object.keys(treeObject);
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Read and decode a dataset value at a path within a package's data tree.
|
|
211
|
-
*
|
|
212
|
-
* @param storage - Storage backend
|
|
213
|
-
* @param repo - Repository identifier
|
|
214
|
-
* @param name - Package name
|
|
215
|
-
* @param version - Package version
|
|
216
|
-
* @param path - Path to the dataset
|
|
217
|
-
* @returns The decoded dataset value
|
|
218
|
-
* @throws If package not found, path invalid, or path points to a tree
|
|
219
|
-
*/
|
|
220
|
-
export async function packageGetDataset(storage, repo, name, version, path) {
|
|
221
|
-
// Read the package to get root structure and hash
|
|
222
|
-
const pkg = await packageRead(storage, repo, name, version);
|
|
223
|
-
const rootStructure = pkg.data.structure;
|
|
224
|
-
const rootHash = pkg.data.value;
|
|
225
|
-
if (path.length === 0) {
|
|
226
|
-
throw new Error('Cannot get dataset at root path - root is always a tree');
|
|
227
|
-
}
|
|
228
|
-
// Traverse to the path
|
|
229
|
-
const { structure, ref } = await traverse(storage, repo, rootHash, rootStructure, path);
|
|
230
|
-
// Must be a value structure
|
|
231
|
-
if (structure.type !== 'value') {
|
|
232
|
-
const pathStr = path.map(s => s.value).join('.');
|
|
233
|
-
throw new Error(`Path '${pathStr}' points to a tree, not a dataset`);
|
|
234
|
-
}
|
|
235
|
-
// Handle different ref types
|
|
236
|
-
if (ref.type === 'unassigned') {
|
|
237
|
-
throw new Error(`Dataset at path is unassigned (pending task output)`);
|
|
238
|
-
}
|
|
239
|
-
if (ref.type === 'null') {
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
if (ref.type === 'tree') {
|
|
243
|
-
const pathStr = path.map(s => s.value).join('.');
|
|
244
|
-
throw new Error(`Path '${pathStr}' structure says value but ref is tree`);
|
|
245
|
-
}
|
|
246
|
-
// Read and return the dataset value
|
|
247
|
-
const result = await datasetRead(storage, repo, ref.value);
|
|
248
|
-
return result.value;
|
|
249
|
-
}
|
|
250
115
|
/**
|
|
251
116
|
* Update a dataset at a path within a workspace.
|
|
252
117
|
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* Acquires an exclusive lock on the workspace for the duration of the write
|
|
257
|
-
* to prevent concurrent modifications.
|
|
118
|
+
* Writes the value to the object store and updates the per-dataset ref file.
|
|
119
|
+
* Uses shared structure lock to allow concurrent writes.
|
|
258
120
|
*
|
|
259
121
|
* @param storage - Storage backend
|
|
260
122
|
* @param repo - Repository identifier
|
|
@@ -274,7 +136,7 @@ export async function workspaceSetDataset(storage, repo, ws, treePath, value, ty
|
|
|
274
136
|
const externalLock = options.lock;
|
|
275
137
|
let lock = externalLock ?? null;
|
|
276
138
|
if (!lock) {
|
|
277
|
-
lock = await storage.locks.acquire(repo, ws, variant('dataset_write', null));
|
|
139
|
+
lock = await storage.locks.acquire(repo, ws, variant('dataset_write', null), { mode: 'shared' });
|
|
278
140
|
if (!lock) {
|
|
279
141
|
const state = await storage.locks.getState(repo, ws);
|
|
280
142
|
throw new WorkspaceLockError(ws, state ? {
|
|
@@ -284,7 +146,48 @@ export async function workspaceSetDataset(storage, repo, ws, treePath, value, ty
|
|
|
284
146
|
}
|
|
285
147
|
}
|
|
286
148
|
try {
|
|
287
|
-
await
|
|
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;
|
|
173
|
+
}
|
|
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`);
|
|
178
|
+
}
|
|
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`);
|
|
183
|
+
}
|
|
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);
|
|
288
191
|
}
|
|
289
192
|
finally {
|
|
290
193
|
// Only release the lock if we acquired it internally
|
|
@@ -293,105 +196,9 @@ export async function workspaceSetDataset(storage, repo, ws, treePath, value, ty
|
|
|
293
196
|
}
|
|
294
197
|
}
|
|
295
198
|
}
|
|
296
|
-
/**
|
|
297
|
-
* Internal: Update a dataset without acquiring a lock.
|
|
298
|
-
* Caller must hold the workspace lock.
|
|
299
|
-
*/
|
|
300
|
-
async function workspaceSetDatasetUnlocked(storage, repo, ws, treePath, value, type) {
|
|
301
|
-
const state = await readWorkspaceState(storage, repo, ws);
|
|
302
|
-
// Read the deployed package object to get the structure
|
|
303
|
-
const pkgData = await storage.objects.read(repo, state.packageHash);
|
|
304
|
-
const decoder = decodeBeast2For(PackageObjectType);
|
|
305
|
-
const pkgObject = decoder(Buffer.from(pkgData));
|
|
306
|
-
const rootStructure = pkgObject.data.structure;
|
|
307
|
-
// Validate that the path leads to a value structure
|
|
308
|
-
let currentStructure = rootStructure;
|
|
309
|
-
for (let i = 0; i < treePath.length; i++) {
|
|
310
|
-
const segment = treePath[i];
|
|
311
|
-
if (segment.type !== 'field') {
|
|
312
|
-
throw new Error(`Unsupported path segment type: ${segment.type}`);
|
|
313
|
-
}
|
|
314
|
-
if (currentStructure.type !== 'struct') {
|
|
315
|
-
const pathSoFar = treePath.slice(0, i).map(s => s.value).join('.');
|
|
316
|
-
throw new Error(`Cannot descend into non-struct at path '${pathSoFar}'`);
|
|
317
|
-
}
|
|
318
|
-
const childStructure = currentStructure.value.get(segment.value);
|
|
319
|
-
if (!childStructure) {
|
|
320
|
-
const pathSoFar = treePath.slice(0, i).map(s => s.value).join('.');
|
|
321
|
-
const available = Array.from(currentStructure.value.keys()).join(', ');
|
|
322
|
-
throw new Error(`Field '${segment.value}' not found at '${pathSoFar}'. Available: ${available}`);
|
|
323
|
-
}
|
|
324
|
-
currentStructure = childStructure;
|
|
325
|
-
}
|
|
326
|
-
// Final structure must be a value
|
|
327
|
-
if (currentStructure.type !== 'value') {
|
|
328
|
-
const pathStr = treePath.map(s => s.value).join('.');
|
|
329
|
-
throw new Error(`Path '${pathStr}' points to a tree, not a dataset`);
|
|
330
|
-
}
|
|
331
|
-
// Write the new dataset value
|
|
332
|
-
const newValueHash = await datasetWrite(storage, repo, value, type);
|
|
333
|
-
// Now rebuild the tree path from leaf to root (structural sharing)
|
|
334
|
-
// We need to read each tree along the path, modify it, and write a new version
|
|
335
|
-
// Collect all tree hashes and structures along the path
|
|
336
|
-
const treeInfos = [];
|
|
337
|
-
let currentHash = state.rootHash;
|
|
338
|
-
currentStructure = rootStructure;
|
|
339
|
-
// Read all trees along the path (except the last segment which is the dataset)
|
|
340
|
-
for (let i = 0; i < treePath.length - 1; i++) {
|
|
341
|
-
treeInfos.push({ hash: currentHash, structure: currentStructure });
|
|
342
|
-
const segment = treePath[i];
|
|
343
|
-
const treeObject = await treeRead(storage, repo, currentHash, currentStructure);
|
|
344
|
-
const childRef = treeObject[segment.value];
|
|
345
|
-
if (!childRef || childRef.type !== 'tree') {
|
|
346
|
-
throw new Error(`Expected tree ref at path segment ${i}`);
|
|
347
|
-
}
|
|
348
|
-
currentHash = childRef.value;
|
|
349
|
-
currentStructure = currentStructure.value.get(segment.value);
|
|
350
|
-
}
|
|
351
|
-
// Add the final tree that contains the dataset
|
|
352
|
-
treeInfos.push({ hash: currentHash, structure: currentStructure });
|
|
353
|
-
// Now rebuild from leaf to root
|
|
354
|
-
// Start with the new value hash as the new ref
|
|
355
|
-
let newRef = { type: 'value', value: newValueHash };
|
|
356
|
-
for (let i = treeInfos.length - 1; i >= 0; i--) {
|
|
357
|
-
const { hash, structure } = treeInfos[i];
|
|
358
|
-
const fieldName = treePath[i].value;
|
|
359
|
-
// Read the current tree
|
|
360
|
-
const treeObject = await treeRead(storage, repo, hash, structure);
|
|
361
|
-
// Create modified tree with the new ref
|
|
362
|
-
const newTreeObject = {
|
|
363
|
-
...treeObject,
|
|
364
|
-
[fieldName]: newRef,
|
|
365
|
-
};
|
|
366
|
-
// Write the new tree
|
|
367
|
-
const newTreeHash = await treeWrite(storage, repo, newTreeObject, structure);
|
|
368
|
-
// This becomes the new ref for the parent
|
|
369
|
-
newRef = { type: 'tree', value: newTreeHash };
|
|
370
|
-
}
|
|
371
|
-
// The final newRef is always a tree ref pointing to the new root
|
|
372
|
-
// (because we start with a value ref and wrap it in tree refs bottom-up)
|
|
373
|
-
if (newRef.type !== 'tree' || newRef.value === null) {
|
|
374
|
-
throw new Error('Internal error: expected tree ref after rebuilding path');
|
|
375
|
-
}
|
|
376
|
-
const newRootHash = newRef.value;
|
|
377
|
-
// Update workspace state atomically
|
|
378
|
-
await writeWorkspaceState(storage, repo, ws, {
|
|
379
|
-
...state,
|
|
380
|
-
rootHash: newRootHash,
|
|
381
|
-
rootUpdatedAt: new Date(),
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
199
|
// =============================================================================
|
|
385
200
|
// Workspace Helper Functions
|
|
386
201
|
// =============================================================================
|
|
387
|
-
/**
|
|
388
|
-
* Write workspace state to file atomically.
|
|
389
|
-
*/
|
|
390
|
-
async function writeWorkspaceState(storage, repo, ws, state) {
|
|
391
|
-
const encoder = encodeBeast2For(WorkspaceStateType);
|
|
392
|
-
const data = encoder(state);
|
|
393
|
-
await storage.refs.workspaceWrite(repo, ws, data);
|
|
394
|
-
}
|
|
395
202
|
/**
|
|
396
203
|
* Read workspace state from file.
|
|
397
204
|
* @throws {WorkspaceNotFoundError} If workspace doesn't exist
|
|
@@ -409,101 +216,110 @@ async function readWorkspaceState(storage, repo, ws) {
|
|
|
409
216
|
return decoder(data);
|
|
410
217
|
}
|
|
411
218
|
/**
|
|
412
|
-
* Get root structure
|
|
219
|
+
* Get root structure for a workspace.
|
|
413
220
|
* Reads the deployed package object to get the structure.
|
|
414
221
|
*/
|
|
415
|
-
async function
|
|
416
|
-
const
|
|
222
|
+
async function getWorkspaceStructure(storage, repo, ws) {
|
|
223
|
+
const wsState = await readWorkspaceState(storage, repo, ws);
|
|
417
224
|
// Read the deployed package object using the stored hash
|
|
418
|
-
const pkgData = await storage.objects.read(repo,
|
|
225
|
+
const pkgData = await storage.objects.read(repo, wsState.packageHash);
|
|
419
226
|
const decoder = decodeBeast2For(PackageObjectType);
|
|
420
227
|
const pkgObject = decoder(Buffer.from(pkgData));
|
|
421
228
|
return {
|
|
422
|
-
rootHash: state.rootHash,
|
|
423
229
|
rootStructure: pkgObject.data.structure,
|
|
424
230
|
};
|
|
425
231
|
}
|
|
426
232
|
// =============================================================================
|
|
427
|
-
// Workspace High-level Operations (by path)
|
|
233
|
+
// Workspace High-level Operations (by path) - Using per-dataset refs
|
|
428
234
|
// =============================================================================
|
|
429
235
|
/**
|
|
430
236
|
* List field names at a tree path within a workspace's data tree.
|
|
431
237
|
*
|
|
238
|
+
* Uses the structure to determine available fields (no tree traversal needed).
|
|
239
|
+
*
|
|
432
240
|
* @param storage - Storage backend
|
|
433
241
|
* @param repo - Repository identifier
|
|
434
242
|
* @param ws - Workspace name
|
|
435
|
-
* @param
|
|
243
|
+
* @param treePath - Path to the tree node
|
|
436
244
|
* @returns Array of field names at the path
|
|
437
245
|
* @throws If workspace not deployed, path invalid, or path points to a dataset
|
|
438
246
|
*/
|
|
439
247
|
export async function workspaceListTree(storage, repo, ws, treePath) {
|
|
440
|
-
const {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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`);
|
|
445
263
|
}
|
|
446
|
-
|
|
447
|
-
return Object.keys(treeObject);
|
|
264
|
+
currentStructure = childStructure;
|
|
448
265
|
}
|
|
449
|
-
|
|
450
|
-
const { structure, ref } = await traverse(storage, repo, rootHash, rootStructure, treePath);
|
|
451
|
-
// Must be a tree structure
|
|
452
|
-
if (structure.type !== 'struct') {
|
|
266
|
+
if (currentStructure.type !== 'struct') {
|
|
453
267
|
const pathStr = treePath.map(s => s.value).join('.');
|
|
454
268
|
throw new Error(`Path '${pathStr}' points to a dataset, not a tree`);
|
|
455
269
|
}
|
|
456
|
-
|
|
457
|
-
if (ref.type !== 'tree') {
|
|
458
|
-
const pathStr = treePath.map(s => s.value).join('.');
|
|
459
|
-
throw new Error(`Path '${pathStr}' has ref type '${ref.type}', expected 'tree'`);
|
|
460
|
-
}
|
|
461
|
-
// Read the tree and return field names
|
|
462
|
-
const treeObject = await treeRead(storage, repo, ref.value, structure);
|
|
463
|
-
return Object.keys(treeObject);
|
|
270
|
+
return Array.from(currentStructure.value.keys());
|
|
464
271
|
}
|
|
465
272
|
/**
|
|
466
273
|
* Read and decode a dataset value at a path within a workspace's data tree.
|
|
467
274
|
*
|
|
275
|
+
* Reads the per-dataset ref file to get the value hash, then decodes the value.
|
|
276
|
+
*
|
|
468
277
|
* @param storage - Storage backend
|
|
469
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
284
|
export async function workspaceGetDataset(storage, repo, ws, treePath) {
|
|
476
|
-
const { rootHash, rootStructure } = await getWorkspaceRootInfo(storage, repo, ws);
|
|
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(storage, repo, ref.value);
|
|
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
324
|
* @param storage - Storage backend
|
|
509
325
|
* @param repo - Repository identifier
|
|
@@ -513,35 +329,27 @@ export async function workspaceGetDataset(storage, repo, ws, treePath) {
|
|
|
513
329
|
* @throws If workspace not deployed, path invalid, or path points to a tree
|
|
514
330
|
*/
|
|
515
331
|
export async function workspaceGetDatasetHash(storage, repo, ws, treePath) {
|
|
516
|
-
const { rootHash, rootStructure } = await getWorkspaceRootInfo(storage, repo, ws);
|
|
517
332
|
if (treePath.length === 0) {
|
|
518
333
|
throw new Error('Cannot get dataset at root path - root is always a tree');
|
|
519
334
|
}
|
|
520
|
-
//
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
throw new Error(`Path '${pathStr}' points to a tree, not a dataset`);
|
|
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 };
|
|
526
340
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return { refType: ref.type, hash: null };
|
|
530
|
-
}
|
|
531
|
-
if (ref.type === 'tree') {
|
|
532
|
-
const pathStr = treePath.map(s => s.value).join('.');
|
|
533
|
-
throw new Error(`Path '${pathStr}' structure says value but ref is tree`);
|
|
341
|
+
if (ref.type === 'null') {
|
|
342
|
+
return { refType: 'null', hash: null };
|
|
534
343
|
}
|
|
535
|
-
return { refType:
|
|
344
|
+
return { refType: 'value', hash: ref.value.hash };
|
|
536
345
|
}
|
|
537
346
|
/**
|
|
538
347
|
* Set a dataset at a path within a workspace using a pre-computed hash.
|
|
539
348
|
*
|
|
540
|
-
*
|
|
541
|
-
* directly. Useful for dataflow execution which already has the output hash.
|
|
349
|
+
* Writes a DatasetRef file directly. No tree path-copy needed.
|
|
542
350
|
*
|
|
543
351
|
* IMPORTANT: This function does NOT acquire a workspace lock. The caller must
|
|
544
|
-
* hold
|
|
352
|
+
* hold a lock on the workspace before calling this function. This
|
|
545
353
|
* is typically used by dataflowExecute which holds the lock for the entire
|
|
546
354
|
* execution.
|
|
547
355
|
*
|
|
@@ -550,92 +358,16 @@ export async function workspaceGetDatasetHash(storage, repo, ws, treePath) {
|
|
|
550
358
|
* @param ws - Workspace name
|
|
551
359
|
* @param treePath - Path to the dataset
|
|
552
360
|
* @param valueHash - Hash of the dataset value already in the object store
|
|
553
|
-
* @returns The new root hash after updating the tree
|
|
554
361
|
* @throws If workspace not deployed, path invalid, or path points to a tree
|
|
555
362
|
*/
|
|
556
|
-
export async function workspaceSetDatasetByHash(storage, repo, ws, treePath, valueHash) {
|
|
363
|
+
export async function workspaceSetDatasetByHash(storage, repo, ws, treePath, valueHash, versions) {
|
|
557
364
|
if (treePath.length === 0) {
|
|
558
365
|
throw new Error('Cannot set dataset at root path - root is always a tree');
|
|
559
366
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
const pkgObject = decoder(Buffer.from(pkgData));
|
|
565
|
-
const rootStructure = pkgObject.data.structure;
|
|
566
|
-
// Validate that the path leads to a value structure
|
|
567
|
-
let currentStructure = rootStructure;
|
|
568
|
-
for (let i = 0; i < treePath.length; i++) {
|
|
569
|
-
const segment = treePath[i];
|
|
570
|
-
if (segment.type !== 'field') {
|
|
571
|
-
throw new Error(`Unsupported path segment type: ${segment.type}`);
|
|
572
|
-
}
|
|
573
|
-
if (currentStructure.type !== 'struct') {
|
|
574
|
-
const pathSoFar = treePath.slice(0, i).map(s => s.value).join('.');
|
|
575
|
-
throw new Error(`Cannot descend into non-struct at path '${pathSoFar}'`);
|
|
576
|
-
}
|
|
577
|
-
const childStructure = currentStructure.value.get(segment.value);
|
|
578
|
-
if (!childStructure) {
|
|
579
|
-
const pathSoFar = treePath.slice(0, i).map(s => s.value).join('.');
|
|
580
|
-
const available = Array.from(currentStructure.value.keys()).join(', ');
|
|
581
|
-
throw new Error(`Field '${segment.value}' not found at '${pathSoFar}'. Available: ${available}`);
|
|
582
|
-
}
|
|
583
|
-
currentStructure = childStructure;
|
|
584
|
-
}
|
|
585
|
-
// Final structure must be a value
|
|
586
|
-
if (currentStructure.type !== 'value') {
|
|
587
|
-
const pathStr = treePath.map(s => s.value).join('.');
|
|
588
|
-
throw new Error(`Path '${pathStr}' points to a tree, not a dataset`);
|
|
589
|
-
}
|
|
590
|
-
// Rebuild the tree path from leaf to root (structural sharing)
|
|
591
|
-
// Collect all tree hashes and structures along the path
|
|
592
|
-
const treeInfos = [];
|
|
593
|
-
let currentHash = state.rootHash;
|
|
594
|
-
currentStructure = rootStructure;
|
|
595
|
-
// Read all trees along the path (except the last segment which is the dataset)
|
|
596
|
-
for (let i = 0; i < treePath.length - 1; i++) {
|
|
597
|
-
treeInfos.push({ hash: currentHash, structure: currentStructure });
|
|
598
|
-
const segment = treePath[i];
|
|
599
|
-
const treeObject = await treeRead(storage, repo, currentHash, currentStructure);
|
|
600
|
-
const childRef = treeObject[segment.value];
|
|
601
|
-
if (!childRef || childRef.type !== 'tree') {
|
|
602
|
-
throw new Error(`Expected tree ref at path segment ${i}`);
|
|
603
|
-
}
|
|
604
|
-
currentHash = childRef.value;
|
|
605
|
-
currentStructure = currentStructure.value.get(segment.value);
|
|
606
|
-
}
|
|
607
|
-
// Add the final tree that contains the dataset
|
|
608
|
-
treeInfos.push({ hash: currentHash, structure: currentStructure });
|
|
609
|
-
// Now rebuild from leaf to root
|
|
610
|
-
// Start with the provided value hash as the new ref
|
|
611
|
-
let newRef = { type: 'value', value: valueHash };
|
|
612
|
-
for (let i = treeInfos.length - 1; i >= 0; i--) {
|
|
613
|
-
const { hash, structure } = treeInfos[i];
|
|
614
|
-
const fieldName = treePath[i].value;
|
|
615
|
-
// Read the current tree
|
|
616
|
-
const treeObject = await treeRead(storage, repo, hash, structure);
|
|
617
|
-
// Create modified tree with the new ref
|
|
618
|
-
const newTreeObject = {
|
|
619
|
-
...treeObject,
|
|
620
|
-
[fieldName]: newRef,
|
|
621
|
-
};
|
|
622
|
-
// Write the new tree
|
|
623
|
-
const newTreeHash = await treeWrite(storage, repo, newTreeObject, structure);
|
|
624
|
-
// This becomes the new ref for the parent
|
|
625
|
-
newRef = { type: 'tree', value: newTreeHash };
|
|
626
|
-
}
|
|
627
|
-
// The final newRef is always a tree ref pointing to the new root
|
|
628
|
-
if (newRef.type !== 'tree' || newRef.value === null) {
|
|
629
|
-
throw new Error('Internal error: expected tree ref after rebuilding path');
|
|
630
|
-
}
|
|
631
|
-
const newRootHash = newRef.value;
|
|
632
|
-
// Update workspace state atomically
|
|
633
|
-
await writeWorkspaceState(storage, repo, ws, {
|
|
634
|
-
...state,
|
|
635
|
-
rootHash: newRootHash,
|
|
636
|
-
rootUpdatedAt: new Date(),
|
|
637
|
-
});
|
|
638
|
-
return newRootHash;
|
|
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);
|
|
639
371
|
}
|
|
640
372
|
/**
|
|
641
373
|
* Get the status of a single dataset at a path within a workspace.
|
|
@@ -650,32 +382,40 @@ export async function workspaceSetDatasetByHash(storage, repo, ws, treePath, val
|
|
|
650
382
|
* @throws If workspace not deployed, path invalid, or path points to a tree
|
|
651
383
|
*/
|
|
652
384
|
export async function workspaceGetDatasetStatus(storage, repo, ws, treePath) {
|
|
653
|
-
const { rootHash, rootStructure } = await getWorkspaceRootInfo(storage, repo, ws);
|
|
654
385
|
if (treePath.length === 0) {
|
|
655
386
|
throw new Error('Cannot get dataset status at root path - root is always a tree');
|
|
656
387
|
}
|
|
657
|
-
//
|
|
658
|
-
const {
|
|
659
|
-
|
|
660
|
-
|
|
388
|
+
// Validate path and get type from structure
|
|
389
|
+
const { rootStructure } = await getWorkspaceStructure(storage, repo, ws);
|
|
390
|
+
let currentStructure = rootStructure;
|
|
391
|
+
for (let i = 0; i < treePath.length; i++) {
|
|
392
|
+
const segment = treePath[i];
|
|
393
|
+
if (segment.type !== 'field')
|
|
394
|
+
throw new Error(`Unsupported path segment type: ${segment.type}`);
|
|
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;
|
|
401
|
+
}
|
|
402
|
+
if (currentStructure.type !== 'value') {
|
|
661
403
|
const pathStr = treePath.map(s => s.value).join('.');
|
|
662
404
|
throw new Error(`Path '${pathStr}' points to a tree, not a dataset`);
|
|
663
405
|
}
|
|
664
|
-
const datasetType =
|
|
665
|
-
//
|
|
666
|
-
|
|
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') {
|
|
667
411
|
return { refType: 'unassigned', hash: null, datasetType, size: null };
|
|
668
412
|
}
|
|
669
413
|
if (ref.type === 'null') {
|
|
670
414
|
return { refType: 'null', hash: null, datasetType, size: 0 };
|
|
671
415
|
}
|
|
672
|
-
if (ref.type === 'tree') {
|
|
673
|
-
const pathStr = treePath.map(s => s.value).join('.');
|
|
674
|
-
throw new Error(`Path '${pathStr}' structure says value but ref is tree`);
|
|
675
|
-
}
|
|
676
416
|
// value ref - get size from object store
|
|
677
|
-
const { size } = await storage.objects.stat(repo, ref.value);
|
|
678
|
-
return { refType: 'value', hash: ref.value, datasetType, size };
|
|
417
|
+
const { size } = await storage.objects.stat(repo, ref.value.hash);
|
|
418
|
+
return { refType: 'value', hash: ref.value.hash, datasetType, size };
|
|
679
419
|
}
|
|
680
420
|
/**
|
|
681
421
|
* Check if a structure represents a task (has function_ir and output).
|
|
@@ -694,15 +434,15 @@ function getTaskOutputTypeFromStructure(structure) {
|
|
|
694
434
|
return undefined;
|
|
695
435
|
const outputStructure = structure.value.get('output');
|
|
696
436
|
if (outputStructure?.type === 'value') {
|
|
697
|
-
return outputStructure.value;
|
|
437
|
+
return outputStructure.value.type;
|
|
698
438
|
}
|
|
699
439
|
return undefined;
|
|
700
440
|
}
|
|
701
441
|
/**
|
|
702
442
|
* Get the full tree structure at a path within a workspace.
|
|
703
443
|
*
|
|
704
|
-
* Recursively walks the
|
|
705
|
-
* suitable for display. Tasks are shown as leaves with their output type.
|
|
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.
|
|
706
446
|
*
|
|
707
447
|
* @param storage - Storage backend
|
|
708
448
|
* @param repo - Repository identifier
|
|
@@ -713,83 +453,87 @@ function getTaskOutputTypeFromStructure(structure) {
|
|
|
713
453
|
* @throws If workspace not deployed or path invalid
|
|
714
454
|
*/
|
|
715
455
|
export async function workspaceGetTree(storage, repo, ws, treePath, options = {}) {
|
|
716
|
-
const {
|
|
456
|
+
const { rootStructure } = await getWorkspaceStructure(storage, repo, ws);
|
|
717
457
|
const { maxDepth, includeTypes, includeStatus } = options;
|
|
718
|
-
//
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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') {
|
|
729
473
|
const pathStr = treePath.map(s => s.value).join('.');
|
|
730
474
|
throw new Error(`Path '${pathStr}' points to a dataset, not a tree`);
|
|
731
475
|
}
|
|
732
|
-
|
|
733
|
-
if (ref.type !== 'tree') {
|
|
734
|
-
const pathStr = treePath.map(s => s.value).join('.');
|
|
735
|
-
throw new Error(`Path '${pathStr}' has ref type '${ref.type}', expected 'tree'`);
|
|
736
|
-
}
|
|
737
|
-
return walkTree(storage, repo, ref.value, structure, 0, maxDepth, includeTypes, includeStatus);
|
|
476
|
+
return walkStructure(storage, repo, ws, targetStructure, pathPrefix, 0, maxDepth, includeTypes, includeStatus);
|
|
738
477
|
}
|
|
739
478
|
/**
|
|
740
|
-
* Recursively walk
|
|
479
|
+
* Recursively walk structure and build TreeNode array using ref files.
|
|
741
480
|
*/
|
|
742
|
-
async function
|
|
481
|
+
async function walkStructure(storage, repo, ws, structure, pathPrefix, currentDepth, maxDepth, includeTypes, includeStatus) {
|
|
743
482
|
if (structure.type !== 'struct') {
|
|
744
483
|
throw new Error('Expected struct structure for tree walk');
|
|
745
484
|
}
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
.filter(([fieldName]) => structure.value.has(fieldName));
|
|
750
|
-
const nodes = await Promise.all(entries.map(async ([fieldName, childRef]) => {
|
|
751
|
-
const childStructure = structure.value.get(fieldName);
|
|
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;
|
|
752
488
|
if (childStructure.type === 'value') {
|
|
753
489
|
// Dataset (leaf node)
|
|
754
490
|
const node = {
|
|
755
491
|
name: fieldName,
|
|
756
492
|
kind: 'dataset',
|
|
757
|
-
datasetType: includeTypes ? childStructure.value : undefined,
|
|
493
|
+
datasetType: includeTypes ? childStructure.value.type : undefined,
|
|
758
494
|
};
|
|
759
495
|
if (includeStatus) {
|
|
760
|
-
|
|
761
|
-
if (
|
|
762
|
-
node.
|
|
763
|
-
const { size } = await storage.objects.stat(repo, childRef.value);
|
|
764
|
-
node.size = size;
|
|
496
|
+
const ref = await storage.datasets.read(repo, ws, childPath);
|
|
497
|
+
if (!ref || ref.type === 'unassigned') {
|
|
498
|
+
node.refType = 'unassigned';
|
|
765
499
|
}
|
|
766
|
-
else if (
|
|
500
|
+
else if (ref.type === 'null') {
|
|
501
|
+
node.refType = 'null';
|
|
767
502
|
node.size = 0;
|
|
768
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
|
+
}
|
|
769
510
|
}
|
|
770
511
|
return node;
|
|
771
512
|
}
|
|
772
513
|
// childStructure.type === 'struct'
|
|
773
514
|
// Task subtree — show as leaf with output type
|
|
774
|
-
if (isTaskStructure(childStructure)
|
|
515
|
+
if (isTaskStructure(childStructure)) {
|
|
775
516
|
const node = {
|
|
776
517
|
name: fieldName,
|
|
777
518
|
kind: 'dataset',
|
|
778
519
|
datasetType: includeTypes ? getTaskOutputTypeFromStructure(childStructure) : undefined,
|
|
779
520
|
};
|
|
780
521
|
if (includeStatus) {
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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;
|
|
793
537
|
}
|
|
794
538
|
}
|
|
795
539
|
return node;
|
|
@@ -797,9 +541,7 @@ async function walkTree(storage, repo, treeHash, structure, currentDepth, maxDep
|
|
|
797
541
|
// Regular subtree
|
|
798
542
|
let children = [];
|
|
799
543
|
if (maxDepth === undefined || currentDepth < maxDepth) {
|
|
800
|
-
|
|
801
|
-
children = await walkTree(storage, repo, childRef.value, childStructure, currentDepth + 1, maxDepth, includeTypes, includeStatus);
|
|
802
|
-
}
|
|
544
|
+
children = await walkStructure(storage, repo, ws, childStructure, childPath, currentDepth + 1, maxDepth, includeTypes, includeStatus);
|
|
803
545
|
}
|
|
804
546
|
return { name: fieldName, kind: 'tree', children };
|
|
805
547
|
}));
|
|
@@ -807,4 +549,41 @@ async function walkTree(storage, repo, treeHash, structure, currentDepth, maxDep
|
|
|
807
549
|
nodes.sort((a, b) => a.name.localeCompare(b.name));
|
|
808
550
|
return nodes;
|
|
809
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;
|
|
583
|
+
}
|
|
584
|
+
if (currentStructure.type !== 'struct') {
|
|
585
|
+
throw new Error('Path points to a dataset, not a tree');
|
|
586
|
+
}
|
|
587
|
+
return Array.from(currentStructure.value.keys());
|
|
588
|
+
}
|
|
810
589
|
//# sourceMappingURL=trees.js.map
|