@elaraai/e3-core 0.0.2-beta.4 → 0.0.2-beta.40

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