@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.
Files changed (78) hide show
  1. package/dist/src/dataflow/api-compat.d.ts.map +1 -1
  2. package/dist/src/dataflow/api-compat.js +6 -1
  3. package/dist/src/dataflow/api-compat.js.map +1 -1
  4. package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts +22 -4
  5. package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts.map +1 -1
  6. package/dist/src/dataflow/orchestrator/LocalOrchestrator.js +353 -79
  7. package/dist/src/dataflow/orchestrator/LocalOrchestrator.js.map +1 -1
  8. package/dist/src/dataflow/orchestrator/interfaces.d.ts +6 -0
  9. package/dist/src/dataflow/orchestrator/interfaces.d.ts.map +1 -1
  10. package/dist/src/dataflow/orchestrator/interfaces.js +1 -0
  11. package/dist/src/dataflow/orchestrator/interfaces.js.map +1 -1
  12. package/dist/src/dataflow/steps.d.ts +74 -28
  13. package/dist/src/dataflow/steps.d.ts.map +1 -1
  14. package/dist/src/dataflow/steps.js +221 -42
  15. package/dist/src/dataflow/steps.js.map +1 -1
  16. package/dist/src/dataflow/types.d.ts +13 -2
  17. package/dist/src/dataflow/types.d.ts.map +1 -1
  18. package/dist/src/dataflow.d.ts +37 -95
  19. package/dist/src/dataflow.d.ts.map +1 -1
  20. package/dist/src/dataflow.js +121 -631
  21. package/dist/src/dataflow.js.map +1 -1
  22. package/dist/src/dataset-refs.d.ts +124 -0
  23. package/dist/src/dataset-refs.d.ts.map +1 -0
  24. package/dist/src/dataset-refs.js +319 -0
  25. package/dist/src/dataset-refs.js.map +1 -0
  26. package/dist/src/execution/MockTaskRunner.d.ts +1 -1
  27. package/dist/src/execution/MockTaskRunner.d.ts.map +1 -1
  28. package/dist/src/execution/MockTaskRunner.js +1 -2
  29. package/dist/src/execution/MockTaskRunner.js.map +1 -1
  30. package/dist/src/index.d.ts +5 -4
  31. package/dist/src/index.d.ts.map +1 -1
  32. package/dist/src/index.js +6 -4
  33. package/dist/src/index.js.map +1 -1
  34. package/dist/src/packages.d.ts.map +1 -1
  35. package/dist/src/packages.js +20 -7
  36. package/dist/src/packages.js.map +1 -1
  37. package/dist/src/storage/in-memory/InMemoryStorage.d.ts +26 -4
  38. package/dist/src/storage/in-memory/InMemoryStorage.d.ts.map +1 -1
  39. package/dist/src/storage/in-memory/InMemoryStorage.js +104 -21
  40. package/dist/src/storage/in-memory/InMemoryStorage.js.map +1 -1
  41. package/dist/src/storage/index.d.ts +2 -2
  42. package/dist/src/storage/index.d.ts.map +1 -1
  43. package/dist/src/storage/index.js +1 -1
  44. package/dist/src/storage/index.js.map +1 -1
  45. package/dist/src/storage/interfaces.d.ts +52 -1
  46. package/dist/src/storage/interfaces.d.ts.map +1 -1
  47. package/dist/src/storage/local/LocalBackend.d.ts +3 -1
  48. package/dist/src/storage/local/LocalBackend.d.ts.map +1 -1
  49. package/dist/src/storage/local/LocalBackend.js +5 -1
  50. package/dist/src/storage/local/LocalBackend.js.map +1 -1
  51. package/dist/src/storage/local/LocalDatasetRefStore.d.ts +22 -0
  52. package/dist/src/storage/local/LocalDatasetRefStore.d.ts.map +1 -0
  53. package/dist/src/storage/local/LocalDatasetRefStore.js +118 -0
  54. package/dist/src/storage/local/LocalDatasetRefStore.js.map +1 -0
  55. package/dist/src/storage/local/LocalLockService.d.ts +6 -0
  56. package/dist/src/storage/local/LocalLockService.d.ts.map +1 -1
  57. package/dist/src/storage/local/LocalLockService.js +17 -4
  58. package/dist/src/storage/local/LocalLockService.js.map +1 -1
  59. package/dist/src/storage/local/LocalRepoStore.d.ts +4 -2
  60. package/dist/src/storage/local/LocalRepoStore.d.ts.map +1 -1
  61. package/dist/src/storage/local/LocalRepoStore.js +14 -2
  62. package/dist/src/storage/local/LocalRepoStore.js.map +1 -1
  63. package/dist/src/storage/local/gc.d.ts.map +1 -1
  64. package/dist/src/storage/local/gc.js +8 -1
  65. package/dist/src/storage/local/gc.js.map +1 -1
  66. package/dist/src/storage/local/index.d.ts +1 -0
  67. package/dist/src/storage/local/index.d.ts.map +1 -1
  68. package/dist/src/storage/local/index.js +1 -0
  69. package/dist/src/storage/local/index.js.map +1 -1
  70. package/dist/src/trees.d.ts +35 -43
  71. package/dist/src/trees.d.ts.map +1 -1
  72. package/dist/src/trees.js +228 -449
  73. package/dist/src/trees.js.map +1 -1
  74. package/dist/src/workspaces.d.ts +6 -27
  75. package/dist/src/workspaces.d.ts.map +1 -1
  76. package/dist/src/workspaces.js +42 -55
  77. package/dist/src/workspaces.js.map +1 -1
  78. 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
- * Low-level operations work with hashes directly (by-hash).
16
- * High-level operations traverse paths from a root (by-path).
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
- * This creates new tree objects along the path with structural sharing,
254
- * then atomically updates the workspace root.
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 workspaceSetDatasetUnlocked(storage, repo, ws, treePath, value, type);
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 and hash for a workspace.
219
+ * Get root structure for a workspace.
413
220
  * Reads the deployed package object to get the structure.
414
221
  */
415
- async function getWorkspaceRootInfo(storage, repo, ws) {
416
- const state = await readWorkspaceState(storage, repo, ws);
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, state.packageHash);
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 path - Path to the tree node
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 { rootHash, rootStructure } = await getWorkspaceRootInfo(storage, repo, ws);
441
- if (treePath.length === 0) {
442
- // Empty path - list root tree fields
443
- if (rootStructure.type !== 'struct') {
444
- throw new Error('Root is not a tree');
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
- const treeObject = await treeRead(storage, repo, rootHash, rootStructure);
447
- return Object.keys(treeObject);
264
+ currentStructure = childStructure;
448
265
  }
449
- // Traverse to the path
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
- // Must be a tree ref
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 path - Path to the dataset
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
- // Traverse to the path
481
- const { structure, ref } = await traverse(storage, repo, rootHash, rootStructure, treePath);
482
- // Must be a value structure
483
- if (structure.type !== 'value') {
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
- // Handle different ref types
488
- if (ref.type === 'unassigned') {
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
- * Unlike workspaceGetDataset which decodes the value, this returns the raw
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
- // Traverse to the path
521
- const { structure, ref } = await traverse(storage, repo, rootHash, rootStructure, treePath);
522
- // Must be a value structure
523
- if (structure.type !== 'value') {
524
- const pathStr = treePath.map(s => s.value).join('.');
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
- // Return ref type and hash
528
- if (ref.type === 'unassigned' || ref.type === 'null') {
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: ref.type, hash: ref.value };
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
- * Unlike workspaceSetDataset which encodes a value, this takes a hash
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 an exclusive lock on the workspace before calling this function. This
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
- const state = await readWorkspaceState(storage, repo, ws);
561
- // Read the deployed package object to get the structure
562
- const pkgData = await storage.objects.read(repo, state.packageHash);
563
- const decoder = decodeBeast2For(PackageObjectType);
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
- // Traverse to the path
658
- const { structure, ref } = await traverse(storage, repo, rootHash, rootStructure, treePath);
659
- // Must be a value structure
660
- if (structure.type !== 'value') {
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 = structure.value;
665
- // Handle different ref types
666
- if (ref.type === 'unassigned') {
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 tree and returns a hierarchical structure
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 { rootHash, rootStructure } = await getWorkspaceRootInfo(storage, repo, ws);
456
+ const { rootStructure } = await getWorkspaceStructure(storage, repo, ws);
717
457
  const { maxDepth, includeTypes, includeStatus } = options;
718
- // If path is empty, start from root
719
- if (treePath.length === 0) {
720
- if (rootStructure.type !== 'struct') {
721
- throw new Error('Root is not a tree');
722
- }
723
- return walkTree(storage, repo, rootHash, rootStructure, 0, maxDepth, includeTypes, includeStatus);
724
- }
725
- // Traverse to the path first
726
- const { structure, ref } = await traverse(storage, repo, rootHash, rootStructure, treePath);
727
- // Must be a tree structure
728
- if (structure.type !== 'struct') {
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
- // Must be a tree ref
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 a tree and build TreeNode array.
479
+ * Recursively walk structure and build TreeNode array using ref files.
741
480
  */
742
- async function walkTree(storage, repo, treeHash, structure, currentDepth, maxDepth, includeTypes, includeStatus) {
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 treeObject = await treeRead(storage, repo, treeHash, structure);
747
- // Filter to fields that exist in the structure, then process in parallel
748
- const entries = Object.entries(treeObject)
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
- node.refType = childRef.type;
761
- if (childRef.type === 'value') {
762
- node.hash = childRef.value;
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 (childRef.type === 'null') {
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) && childRef.type === 'tree') {
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
- const taskTree = await treeRead(storage, repo, childRef.value, childStructure);
782
- const outputRef = taskTree['output'];
783
- if (outputRef) {
784
- node.refType = outputRef.type;
785
- if (outputRef.type === 'value') {
786
- node.hash = outputRef.value;
787
- const { size } = await storage.objects.stat(repo, outputRef.value);
788
- node.size = size;
789
- }
790
- else if (outputRef.type === 'null') {
791
- node.size = 0;
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
- if (childRef.type === 'tree') {
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