@elaraai/e3-core 0.0.2-beta.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/LICENSE.md +4 -0
  2. package/README.md +74 -35
  3. package/dist/src/dataflow/api-compat.d.ts +90 -0
  4. package/dist/src/dataflow/api-compat.d.ts.map +1 -0
  5. package/dist/src/dataflow/api-compat.js +139 -0
  6. package/dist/src/dataflow/api-compat.js.map +1 -0
  7. package/dist/src/dataflow/api-compat.spec.d.ts +6 -0
  8. package/dist/src/dataflow/api-compat.spec.d.ts.map +1 -0
  9. package/dist/src/dataflow/api-compat.spec.js +182 -0
  10. package/dist/src/dataflow/api-compat.spec.js.map +1 -0
  11. package/dist/src/dataflow/index.d.ts +18 -0
  12. package/dist/src/dataflow/index.d.ts.map +1 -0
  13. package/dist/src/dataflow/index.js +23 -0
  14. package/dist/src/dataflow/index.js.map +1 -0
  15. package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts +76 -0
  16. package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts.map +1 -0
  17. package/dist/src/dataflow/orchestrator/LocalOrchestrator.js +729 -0
  18. package/dist/src/dataflow/orchestrator/LocalOrchestrator.js.map +1 -0
  19. package/dist/src/dataflow/orchestrator/index.d.ts +12 -0
  20. package/dist/src/dataflow/orchestrator/index.d.ts.map +1 -0
  21. package/dist/src/dataflow/orchestrator/index.js +12 -0
  22. package/dist/src/dataflow/orchestrator/index.js.map +1 -0
  23. package/dist/src/dataflow/orchestrator/interfaces.d.ts +163 -0
  24. package/dist/src/dataflow/orchestrator/interfaces.d.ts.map +1 -0
  25. package/dist/src/dataflow/orchestrator/interfaces.js +52 -0
  26. package/dist/src/dataflow/orchestrator/interfaces.js.map +1 -0
  27. package/dist/src/dataflow/state-store/FileStateStore.d.ts +67 -0
  28. package/dist/src/dataflow/state-store/FileStateStore.d.ts.map +1 -0
  29. package/dist/src/dataflow/state-store/FileStateStore.js +300 -0
  30. package/dist/src/dataflow/state-store/FileStateStore.js.map +1 -0
  31. package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts +42 -0
  32. package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts.map +1 -0
  33. package/dist/src/dataflow/state-store/InMemoryStateStore.js +229 -0
  34. package/dist/src/dataflow/state-store/InMemoryStateStore.js.map +1 -0
  35. package/dist/src/dataflow/state-store/InMemoryStateStore.spec.d.ts +6 -0
  36. package/dist/src/dataflow/state-store/InMemoryStateStore.spec.d.ts.map +1 -0
  37. package/dist/src/dataflow/state-store/InMemoryStateStore.spec.js +114 -0
  38. package/dist/src/dataflow/state-store/InMemoryStateStore.spec.js.map +1 -0
  39. package/dist/src/dataflow/state-store/index.d.ts +13 -0
  40. package/dist/src/dataflow/state-store/index.d.ts.map +1 -0
  41. package/dist/src/dataflow/state-store/index.js +13 -0
  42. package/dist/src/dataflow/state-store/index.js.map +1 -0
  43. package/dist/src/dataflow/state-store/interfaces.d.ts +159 -0
  44. package/dist/src/dataflow/state-store/interfaces.d.ts.map +1 -0
  45. package/dist/src/dataflow/state-store/interfaces.js +6 -0
  46. package/dist/src/dataflow/state-store/interfaces.js.map +1 -0
  47. package/dist/src/dataflow/steps.d.ts +222 -0
  48. package/dist/src/dataflow/steps.d.ts.map +1 -0
  49. package/dist/src/dataflow/steps.js +707 -0
  50. package/dist/src/dataflow/steps.js.map +1 -0
  51. package/dist/src/dataflow/steps.spec.d.ts +6 -0
  52. package/dist/src/dataflow/steps.spec.d.ts.map +1 -0
  53. package/dist/src/dataflow/steps.spec.js +343 -0
  54. package/dist/src/dataflow/steps.spec.js.map +1 -0
  55. package/dist/src/dataflow/types.d.ts +127 -0
  56. package/dist/src/dataflow/types.d.ts.map +1 -0
  57. package/dist/src/dataflow/types.js +7 -0
  58. package/dist/src/dataflow/types.js.map +1 -0
  59. package/dist/src/dataflow-orchestration.spec.d.ts +6 -0
  60. package/dist/src/dataflow-orchestration.spec.d.ts.map +1 -0
  61. package/dist/src/dataflow-orchestration.spec.js +1025 -0
  62. package/dist/src/dataflow-orchestration.spec.js.map +1 -0
  63. package/dist/src/dataflow.d.ts +113 -38
  64. package/dist/src/dataflow.d.ts.map +1 -1
  65. package/dist/src/dataflow.js +269 -416
  66. package/dist/src/dataflow.js.map +1 -1
  67. package/dist/src/dataflow.spec.d.ts +6 -0
  68. package/dist/src/dataflow.spec.d.ts.map +1 -0
  69. package/dist/src/dataflow.spec.js +663 -0
  70. package/dist/src/dataflow.spec.js.map +1 -0
  71. package/dist/src/dataset-refs.d.ts +124 -0
  72. package/dist/src/dataset-refs.d.ts.map +1 -0
  73. package/dist/src/dataset-refs.js +319 -0
  74. package/dist/src/dataset-refs.js.map +1 -0
  75. package/dist/src/errors.d.ts +39 -9
  76. package/dist/src/errors.d.ts.map +1 -1
  77. package/dist/src/errors.js +51 -8
  78. package/dist/src/errors.js.map +1 -1
  79. package/dist/src/errors.spec.d.ts +6 -0
  80. package/dist/src/errors.spec.d.ts.map +1 -0
  81. package/dist/src/errors.spec.js +276 -0
  82. package/dist/src/errors.spec.js.map +1 -0
  83. package/dist/src/execution/LocalTaskRunner.d.ts +73 -0
  84. package/dist/src/execution/LocalTaskRunner.d.ts.map +1 -0
  85. package/dist/src/execution/LocalTaskRunner.js +399 -0
  86. package/dist/src/execution/LocalTaskRunner.js.map +1 -0
  87. package/dist/src/execution/MockTaskRunner.d.ts +49 -0
  88. package/dist/src/execution/MockTaskRunner.d.ts.map +1 -0
  89. package/dist/src/execution/MockTaskRunner.js +54 -0
  90. package/dist/src/execution/MockTaskRunner.js.map +1 -0
  91. package/dist/src/execution/index.d.ts +16 -0
  92. package/dist/src/execution/index.d.ts.map +1 -0
  93. package/dist/src/execution/index.js +8 -0
  94. package/dist/src/execution/index.js.map +1 -0
  95. package/dist/src/execution/interfaces.d.ts +246 -0
  96. package/dist/src/execution/interfaces.d.ts.map +1 -0
  97. package/dist/src/execution/interfaces.js +6 -0
  98. package/dist/src/execution/interfaces.js.map +1 -0
  99. package/dist/src/execution/processHelpers.d.ts +20 -0
  100. package/dist/src/execution/processHelpers.d.ts.map +1 -0
  101. package/dist/src/execution/processHelpers.js +62 -0
  102. package/dist/src/execution/processHelpers.js.map +1 -0
  103. package/dist/src/executions.d.ts +71 -104
  104. package/dist/src/executions.d.ts.map +1 -1
  105. package/dist/src/executions.js +113 -481
  106. package/dist/src/executions.js.map +1 -1
  107. package/dist/src/executions.spec.d.ts +6 -0
  108. package/dist/src/executions.spec.d.ts.map +1 -0
  109. package/dist/src/executions.spec.js +387 -0
  110. package/dist/src/executions.spec.js.map +1 -0
  111. package/dist/src/formats.d.ts +18 -2
  112. package/dist/src/formats.d.ts.map +1 -1
  113. package/dist/src/formats.js +34 -2
  114. package/dist/src/formats.js.map +1 -1
  115. package/dist/src/gc.spec.d.ts +6 -0
  116. package/dist/src/gc.spec.d.ts.map +1 -0
  117. package/dist/src/gc.spec.js +512 -0
  118. package/dist/src/gc.spec.js.map +1 -0
  119. package/dist/src/index.d.ts +20 -10
  120. package/dist/src/index.d.ts.map +1 -1
  121. package/dist/src/index.js +48 -18
  122. package/dist/src/index.js.map +1 -1
  123. package/dist/src/objects.d.ts +7 -53
  124. package/dist/src/objects.d.ts.map +1 -1
  125. package/dist/src/objects.js +13 -232
  126. package/dist/src/objects.js.map +1 -1
  127. package/dist/src/objects.spec.d.ts +6 -0
  128. package/dist/src/objects.spec.d.ts.map +1 -0
  129. package/dist/src/objects.spec.js +247 -0
  130. package/dist/src/objects.spec.js.map +1 -0
  131. package/dist/src/packages.d.ts +41 -14
  132. package/dist/src/packages.d.ts.map +1 -1
  133. package/dist/src/packages.js +151 -89
  134. package/dist/src/packages.js.map +1 -1
  135. package/dist/src/packages.spec.d.ts +6 -0
  136. package/dist/src/packages.spec.d.ts.map +1 -0
  137. package/dist/src/packages.spec.js +324 -0
  138. package/dist/src/packages.spec.js.map +1 -0
  139. package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts +35 -0
  140. package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts.map +1 -0
  141. package/dist/src/storage/in-memory/InMemoryRepoStore.js +107 -0
  142. package/dist/src/storage/in-memory/InMemoryRepoStore.js.map +1 -0
  143. package/dist/src/storage/in-memory/InMemoryRepoStore.spec.d.ts +6 -0
  144. package/dist/src/storage/in-memory/InMemoryRepoStore.spec.d.ts.map +1 -0
  145. package/dist/src/storage/in-memory/InMemoryRepoStore.spec.js +187 -0
  146. package/dist/src/storage/in-memory/InMemoryRepoStore.spec.js.map +1 -0
  147. package/dist/src/storage/in-memory/InMemoryStorage.d.ts +139 -0
  148. package/dist/src/storage/in-memory/InMemoryStorage.d.ts.map +1 -0
  149. package/dist/src/storage/in-memory/InMemoryStorage.js +439 -0
  150. package/dist/src/storage/in-memory/InMemoryStorage.js.map +1 -0
  151. package/dist/src/storage/in-memory/index.d.ts +12 -0
  152. package/dist/src/storage/in-memory/index.d.ts.map +1 -0
  153. package/dist/src/storage/in-memory/index.js +12 -0
  154. package/dist/src/storage/in-memory/index.js.map +1 -0
  155. package/dist/src/storage/index.d.ts +18 -0
  156. package/dist/src/storage/index.d.ts.map +1 -0
  157. package/dist/src/storage/index.js +10 -0
  158. package/dist/src/storage/index.js.map +1 -0
  159. package/dist/src/storage/interfaces.d.ts +581 -0
  160. package/dist/src/storage/interfaces.d.ts.map +1 -0
  161. package/dist/src/storage/interfaces.js +6 -0
  162. package/dist/src/storage/interfaces.js.map +1 -0
  163. package/dist/src/storage/local/LocalBackend.d.ts +56 -0
  164. package/dist/src/storage/local/LocalBackend.d.ts.map +1 -0
  165. package/dist/src/storage/local/LocalBackend.js +145 -0
  166. package/dist/src/storage/local/LocalBackend.js.map +1 -0
  167. package/dist/src/storage/local/LocalDatasetRefStore.d.ts +22 -0
  168. package/dist/src/storage/local/LocalDatasetRefStore.d.ts.map +1 -0
  169. package/dist/src/storage/local/LocalDatasetRefStore.js +118 -0
  170. package/dist/src/storage/local/LocalDatasetRefStore.js.map +1 -0
  171. package/dist/src/storage/local/LocalLockService.d.ts +111 -0
  172. package/dist/src/storage/local/LocalLockService.d.ts.map +1 -0
  173. package/dist/src/storage/local/LocalLockService.js +364 -0
  174. package/dist/src/storage/local/LocalLockService.js.map +1 -0
  175. package/dist/src/storage/local/LocalLockService.spec.d.ts +6 -0
  176. package/dist/src/storage/local/LocalLockService.spec.d.ts.map +1 -0
  177. package/dist/src/storage/local/LocalLockService.spec.js +148 -0
  178. package/dist/src/storage/local/LocalLockService.spec.js.map +1 -0
  179. package/dist/src/storage/local/LocalLogStore.d.ts +23 -0
  180. package/dist/src/storage/local/LocalLogStore.d.ts.map +1 -0
  181. package/dist/src/storage/local/LocalLogStore.js +66 -0
  182. package/dist/src/storage/local/LocalLogStore.js.map +1 -0
  183. package/dist/src/storage/local/LocalObjectStore.d.ts +55 -0
  184. package/dist/src/storage/local/LocalObjectStore.d.ts.map +1 -0
  185. package/dist/src/storage/local/LocalObjectStore.js +300 -0
  186. package/dist/src/storage/local/LocalObjectStore.js.map +1 -0
  187. package/dist/src/storage/local/LocalRefStore.d.ts +50 -0
  188. package/dist/src/storage/local/LocalRefStore.d.ts.map +1 -0
  189. package/dist/src/storage/local/LocalRefStore.js +337 -0
  190. package/dist/src/storage/local/LocalRefStore.js.map +1 -0
  191. package/dist/src/storage/local/LocalRepoStore.d.ts +55 -0
  192. package/dist/src/storage/local/LocalRepoStore.d.ts.map +1 -0
  193. package/dist/src/storage/local/LocalRepoStore.js +365 -0
  194. package/dist/src/storage/local/LocalRepoStore.js.map +1 -0
  195. package/dist/src/storage/local/LocalRepoStore.spec.d.ts +6 -0
  196. package/dist/src/storage/local/LocalRepoStore.spec.d.ts.map +1 -0
  197. package/dist/src/storage/local/LocalRepoStore.spec.js +255 -0
  198. package/dist/src/storage/local/LocalRepoStore.spec.js.map +1 -0
  199. package/dist/src/storage/local/gc.d.ts +92 -0
  200. package/dist/src/storage/local/gc.d.ts.map +1 -0
  201. package/dist/src/storage/local/gc.js +377 -0
  202. package/dist/src/storage/local/gc.js.map +1 -0
  203. package/dist/src/storage/local/index.d.ts +18 -0
  204. package/dist/src/storage/local/index.d.ts.map +1 -0
  205. package/dist/src/storage/local/index.js +18 -0
  206. package/dist/src/storage/local/index.js.map +1 -0
  207. package/dist/src/storage/local/localHelpers.d.ts +25 -0
  208. package/dist/src/storage/local/localHelpers.d.ts.map +1 -0
  209. package/dist/src/storage/local/localHelpers.js +69 -0
  210. package/dist/src/storage/local/localHelpers.js.map +1 -0
  211. package/dist/src/{repository.d.ts → storage/local/repository.d.ts} +8 -4
  212. package/dist/src/storage/local/repository.d.ts.map +1 -0
  213. package/dist/src/{repository.js → storage/local/repository.js} +31 -29
  214. package/dist/src/storage/local/repository.js.map +1 -0
  215. package/dist/src/storage/local/repository.spec.d.ts +6 -0
  216. package/dist/src/storage/local/repository.spec.d.ts.map +1 -0
  217. package/dist/src/storage/local/repository.spec.js +186 -0
  218. package/dist/src/storage/local/repository.spec.js.map +1 -0
  219. package/dist/src/tasks.d.ts +16 -10
  220. package/dist/src/tasks.d.ts.map +1 -1
  221. package/dist/src/tasks.js +35 -41
  222. package/dist/src/tasks.js.map +1 -1
  223. package/dist/src/tasks.spec.d.ts +6 -0
  224. package/dist/src/tasks.spec.d.ts.map +1 -0
  225. package/dist/src/tasks.spec.js +105 -0
  226. package/dist/src/tasks.spec.js.map +1 -0
  227. package/dist/src/test-helpers.d.ts +5 -4
  228. package/dist/src/test-helpers.d.ts.map +1 -1
  229. package/dist/src/test-helpers.js +9 -21
  230. package/dist/src/test-helpers.js.map +1 -1
  231. package/dist/src/transfer/InMemoryTransferBackend.d.ts +75 -0
  232. package/dist/src/transfer/InMemoryTransferBackend.d.ts.map +1 -0
  233. package/dist/src/transfer/InMemoryTransferBackend.js +211 -0
  234. package/dist/src/transfer/InMemoryTransferBackend.js.map +1 -0
  235. package/dist/src/transfer/index.d.ts +9 -0
  236. package/dist/src/transfer/index.d.ts.map +1 -0
  237. package/dist/src/transfer/index.js +11 -0
  238. package/dist/src/transfer/index.js.map +1 -0
  239. package/dist/src/transfer/interfaces.d.ts +103 -0
  240. package/dist/src/transfer/interfaces.d.ts.map +1 -0
  241. package/dist/src/transfer/interfaces.js +6 -0
  242. package/dist/src/transfer/interfaces.js.map +1 -0
  243. package/dist/src/transfer/process.d.ts +55 -0
  244. package/dist/src/transfer/process.d.ts.map +1 -0
  245. package/dist/src/transfer/process.js +144 -0
  246. package/dist/src/transfer/process.js.map +1 -0
  247. package/dist/src/transfer/types.d.ts +106 -0
  248. package/dist/src/transfer/types.d.ts.map +1 -0
  249. package/dist/src/transfer/types.js +61 -0
  250. package/dist/src/transfer/types.js.map +1 -0
  251. package/dist/src/trees.d.ts +102 -63
  252. package/dist/src/trees.d.ts.map +1 -1
  253. package/dist/src/trees.js +319 -479
  254. package/dist/src/trees.js.map +1 -1
  255. package/dist/src/trees.spec.d.ts +6 -0
  256. package/dist/src/trees.spec.d.ts.map +1 -0
  257. package/dist/src/trees.spec.js +635 -0
  258. package/dist/src/trees.spec.js.map +1 -0
  259. package/dist/src/uuid.d.ts +26 -0
  260. package/dist/src/uuid.d.ts.map +1 -0
  261. package/dist/src/uuid.js +80 -0
  262. package/dist/src/uuid.js.map +1 -0
  263. package/dist/src/workspaceStatus.d.ts +6 -4
  264. package/dist/src/workspaceStatus.d.ts.map +1 -1
  265. package/dist/src/workspaceStatus.js +46 -60
  266. package/dist/src/workspaceStatus.js.map +1 -1
  267. package/dist/src/workspaces.d.ts +46 -47
  268. package/dist/src/workspaces.d.ts.map +1 -1
  269. package/dist/src/workspaces.js +281 -221
  270. package/dist/src/workspaces.js.map +1 -1
  271. package/dist/src/workspaces.spec.d.ts +6 -0
  272. package/dist/src/workspaces.spec.d.ts.map +1 -0
  273. package/dist/src/workspaces.spec.js +273 -0
  274. package/dist/src/workspaces.spec.js.map +1 -0
  275. package/package.json +15 -15
  276. package/dist/src/gc.d.ts +0 -54
  277. package/dist/src/gc.d.ts.map +0 -1
  278. package/dist/src/gc.js +0 -233
  279. package/dist/src/gc.js.map +0 -1
  280. package/dist/src/repository.d.ts.map +0 -1
  281. package/dist/src/repository.js.map +0 -1
  282. package/dist/src/workspaceLock.d.ts +0 -67
  283. package/dist/src/workspaceLock.d.ts.map +0 -1
  284. package/dist/src/workspaceLock.js +0 -217
  285. package/dist/src/workspaceLock.js.map +0 -1
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ /**
6
+ * Local filesystem implementation of workspace locking.
7
+ *
8
+ * Provides exclusive locks on workspaces to prevent concurrent dataflow
9
+ * executions or writes that could corrupt workspace state. Uses Linux
10
+ * flock() for automatic lock release on process death.
11
+ *
12
+ * Lock mechanism:
13
+ * - Uses flock(LOCK_EX | LOCK_NB) via the `flock` command for kernel-managed locking
14
+ * - Lock is automatically released when the process dies (kernel handles this)
15
+ * - Lock state stored in beast2 format using LockStateType from e3-types
16
+ * - Holder stored as East text string (e.g., `.process (pid=1234, ...)`)
17
+ * - Stale lock detection via bootId comparison (handles system restarts)
18
+ */
19
+ import * as fs from 'fs/promises';
20
+ import * as path from 'path';
21
+ import { spawn } from 'child_process';
22
+ import { encodeBeast2For, decodeBeast2For, printFor, parseInferred, variant, none, VariantType } from '@elaraai/east';
23
+ import { LockStateType, ProcessHolderType } from '@elaraai/e3-types';
24
+ import { WorkspaceLockError } from '../../errors.js';
25
+ import { getBootId, getPidStartTime, isProcessAlive } from '../../execution/processHelpers.js';
26
+ // =============================================================================
27
+ // Holder Encoding
28
+ // =============================================================================
29
+ /**
30
+ * Variant type for encoding holder as East text.
31
+ * The holder string stores `.process (...)` or other backend-specific variants.
32
+ */
33
+ const HolderVariantType = VariantType({
34
+ process: ProcessHolderType,
35
+ });
36
+ /** Print a process holder to East text format */
37
+ const printProcessHolder = printFor(HolderVariantType);
38
+ /**
39
+ * Parse an East text holder string.
40
+ * Returns the parsed variant or null if parsing fails.
41
+ */
42
+ function parseHolder(holderStr) {
43
+ try {
44
+ const [_type, value] = parseInferred(holderStr);
45
+ return value;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ // =============================================================================
52
+ // Lock File Helpers
53
+ // =============================================================================
54
+ /**
55
+ * Get the lock file path for a workspace.
56
+ */
57
+ export function workspaceLockPath(repoPath, workspace) {
58
+ return path.join(repoPath, 'workspaces', `${workspace}.lock`);
59
+ }
60
+ /**
61
+ * Read lock state from a lock file.
62
+ * Returns null if file doesn't exist or is invalid.
63
+ */
64
+ async function readLockState(lockPath) {
65
+ try {
66
+ const data = await fs.readFile(lockPath);
67
+ if (data.length === 0)
68
+ return null;
69
+ const decoder = decodeBeast2For(LockStateType);
70
+ return decoder(data);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Write lock state to a lock file in beast2 format.
78
+ */
79
+ async function writeLockState(lockPath, state) {
80
+ const encoder = encodeBeast2For(LockStateType);
81
+ const data = encoder(state);
82
+ await fs.writeFile(lockPath, data);
83
+ }
84
+ /**
85
+ * Convert LockState to LockHolderInfo for error display.
86
+ */
87
+ export function lockStateToHolderInfo(state) {
88
+ const info = {
89
+ acquiredAt: state.acquiredAt.toISOString(),
90
+ operation: state.operation.type,
91
+ };
92
+ // Parse the holder string to extract process-specific fields
93
+ const holder = parseHolder(state.holder);
94
+ if (holder?.type === 'process') {
95
+ info.pid = Number(holder.value.pid);
96
+ info.bootId = holder.value.bootId;
97
+ info.startTime = Number(holder.value.startTime);
98
+ info.command = holder.value.command;
99
+ }
100
+ return info;
101
+ }
102
+ /**
103
+ * Check if a lock holder is still alive.
104
+ * @param holderStr - East text-encoded holder string
105
+ */
106
+ export async function isLockHolderAlive(holderStr) {
107
+ const holder = parseHolder(holderStr);
108
+ if (!holder)
109
+ return true; // Can't parse - assume alive (safer)
110
+ if (holder.type === 'process') {
111
+ return isProcessAlive(Number(holder.value.pid), Number(holder.value.startTime), holder.value.bootId);
112
+ }
113
+ // Unknown holder type - assume alive (safer default)
114
+ return true;
115
+ }
116
+ // =============================================================================
117
+ // Lock Acquisition
118
+ // =============================================================================
119
+ /**
120
+ * Acquire an exclusive lock on a workspace.
121
+ *
122
+ * Uses Linux flock() for kernel-managed locking. The lock is automatically
123
+ * released when the process exits (even on crash/kill).
124
+ *
125
+ * @param repoPath - Path to e3 repository
126
+ * @param workspace - Workspace name to lock
127
+ * @param operation - What operation is acquiring the lock
128
+ * @param options - Lock acquisition options
129
+ * @returns Lock handle - call release() when done
130
+ * @throws {WorkspaceLockError} If workspace is locked by another process
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * const lock = await acquireWorkspaceLock(repoPath, 'production', { type: 'dataflow', value: null });
135
+ * try {
136
+ * await dataflowExecute(repoPath, 'production', { lock });
137
+ * } finally {
138
+ * await lock.release();
139
+ * }
140
+ * ```
141
+ */
142
+ export async function acquireWorkspaceLock(repoPath, workspace, operation, options = {}) {
143
+ const lockPath = workspaceLockPath(repoPath, workspace);
144
+ // Ensure workspaces directory exists
145
+ await fs.mkdir(path.dirname(lockPath), { recursive: true });
146
+ // Gather our process identification
147
+ const pid = process.pid;
148
+ const bootId = await getBootId();
149
+ const startTime = await getPidStartTime(pid);
150
+ const command = process.argv.join(' ');
151
+ const acquiredAt = new Date();
152
+ // Encode holder as East text: .process (pid=..., bootId="...", ...)
153
+ const holderVariant = variant('process', {
154
+ pid: BigInt(pid),
155
+ bootId,
156
+ startTime: BigInt(startTime),
157
+ command,
158
+ });
159
+ const holder = printProcessHolder(holderVariant);
160
+ const lockState = {
161
+ operation,
162
+ holder,
163
+ acquiredAt,
164
+ expiresAt: none,
165
+ };
166
+ // Try to acquire flock via subprocess
167
+ // The subprocess holds the lock and we communicate with it via stdin/signals
168
+ const flockProcess = await tryAcquireFlock(lockPath, lockState, options);
169
+ if (!flockProcess) {
170
+ // Failed to acquire - read lock state to report who has it
171
+ const existingState = await readLockState(lockPath);
172
+ const holderInfo = existingState ? lockStateToHolderInfo(existingState) : undefined;
173
+ throw new WorkspaceLockError(workspace, holderInfo);
174
+ }
175
+ // Lock acquired! Create handle
176
+ let released = false;
177
+ const handle = {
178
+ resource: workspace,
179
+ workspace,
180
+ lockPath,
181
+ async release() {
182
+ if (released)
183
+ return;
184
+ released = true;
185
+ // Kill the flock subprocess to release the lock
186
+ flockProcess.kill('SIGTERM');
187
+ // Clean up lock file (best effort)
188
+ try {
189
+ await fs.unlink(lockPath);
190
+ }
191
+ catch {
192
+ // Ignore - file might already be gone
193
+ }
194
+ },
195
+ };
196
+ return handle;
197
+ }
198
+ /**
199
+ * Try to acquire flock using a subprocess.
200
+ *
201
+ * We spawn `flock --nonblock <lockfile> cat` which:
202
+ * 1. Tries to acquire exclusive lock (non-blocking)
203
+ * 2. If successful, runs `cat` which blocks reading stdin forever
204
+ * 3. We keep the subprocess alive to hold the lock
205
+ * 4. When we kill the subprocess, the lock is released
206
+ *
207
+ * Returns the subprocess if lock acquired, null if lock is held by another.
208
+ */
209
+ async function tryAcquireFlock(lockPath, lockState, options) {
210
+ // First, check if there's a stale lock we can clean up
211
+ // (only for exclusive mode — shared locks don't need stale checking)
212
+ if (options.mode !== 'shared') {
213
+ await checkAndCleanStaleLock(lockPath);
214
+ }
215
+ const isShared = options.mode === 'shared';
216
+ const args = [];
217
+ if (isShared) {
218
+ args.push('--shared');
219
+ }
220
+ if (options.wait) {
221
+ args.push('--timeout', String((options.timeout ?? 30000) / 1000));
222
+ }
223
+ else {
224
+ args.push('--nonblock');
225
+ }
226
+ // Use 'sh -c "echo ready && cat"' as the inner command so that "ready"
227
+ // on stdout is a deterministic signal that flock acquired the lock and
228
+ // started the inner command. `cat` then blocks on stdin to keep the
229
+ // subprocess (and therefore the lock) alive until we kill it.
230
+ args.push(lockPath, 'sh', '-c', 'echo ready && cat');
231
+ const child = spawn('flock', args, {
232
+ stdio: ['pipe', 'pipe', 'pipe'],
233
+ detached: false,
234
+ });
235
+ return new Promise((resolve) => {
236
+ let resolved = false;
237
+ // If flock fails to acquire, it exits with code 1
238
+ child.on('error', () => {
239
+ if (!resolved) {
240
+ resolved = true;
241
+ resolve(null);
242
+ }
243
+ });
244
+ child.on('exit', () => {
245
+ if (!resolved) {
246
+ resolved = true;
247
+ // Exit code 1 means lock is held by another
248
+ resolve(null);
249
+ }
250
+ });
251
+ // When flock acquires the lock, the inner command prints "ready" to
252
+ // stdout. This is a deterministic signal — no timing assumptions.
253
+ child.stdout.on('data', (data) => {
254
+ if (!resolved && data.toString().includes('ready')) {
255
+ resolved = true;
256
+ // Write lock state before resolving so release() can safely unlink
257
+ writeLockState(lockPath, lockState)
258
+ .then(() => {
259
+ resolve(child);
260
+ })
261
+ .catch(() => {
262
+ // Can't write state — release the kernel lock and report failure
263
+ child.kill('SIGTERM');
264
+ resolve(null);
265
+ });
266
+ }
267
+ });
268
+ });
269
+ }
270
+ /**
271
+ * Check if a lock file exists with stale lock state and clean it up.
272
+ * A lock is stale if the holder process no longer exists.
273
+ */
274
+ async function checkAndCleanStaleLock(lockPath) {
275
+ const state = await readLockState(lockPath);
276
+ if (!state)
277
+ return;
278
+ // Check if the holder is still alive
279
+ const alive = await isLockHolderAlive(state.holder);
280
+ if (!alive) {
281
+ // Stale lock - try to remove it
282
+ try {
283
+ await fs.unlink(lockPath);
284
+ }
285
+ catch {
286
+ // Ignore - another process might have cleaned it up
287
+ }
288
+ }
289
+ }
290
+ /**
291
+ * Get the lock state for a workspace.
292
+ *
293
+ * @param repoPath - Path to e3 repository
294
+ * @param workspace - Workspace name to check
295
+ * @returns Lock state if locked, null if not locked
296
+ */
297
+ export async function getWorkspaceLockState(repoPath, workspace) {
298
+ const lockPath = workspaceLockPath(repoPath, workspace);
299
+ const state = await readLockState(lockPath);
300
+ if (!state)
301
+ return null;
302
+ // Check if the holder is still alive
303
+ const alive = await isLockHolderAlive(state.holder);
304
+ if (!alive) {
305
+ // Stale lock - clean it up and report as not locked
306
+ try {
307
+ await fs.unlink(lockPath);
308
+ }
309
+ catch {
310
+ // Ignore
311
+ }
312
+ return null;
313
+ }
314
+ return state;
315
+ }
316
+ /**
317
+ * Get lock holder info for a workspace (for backwards compatibility).
318
+ *
319
+ * @param repoPath - Path to e3 repository
320
+ * @param workspace - Workspace name to check
321
+ * @returns Lock holder info if locked, null if not locked
322
+ * @deprecated Use getWorkspaceLockState for full lock information
323
+ */
324
+ export async function getWorkspaceLockHolder(repoPath, workspace) {
325
+ const state = await getWorkspaceLockState(repoPath, workspace);
326
+ return state ? lockStateToHolderInfo(state) : null;
327
+ }
328
+ // =============================================================================
329
+ // LockService Interface Implementation
330
+ // =============================================================================
331
+ /**
332
+ * Local filesystem implementation of LockService.
333
+ *
334
+ * Uses flock() for kernel-managed locking with lock state
335
+ * stored in beast2 format using LockStateType.
336
+ * The `repo` parameter is the path to the e3 repository directory.
337
+ */
338
+ export class LocalLockService {
339
+ async acquire(repo, resource, operation, options) {
340
+ const acquireOptions = {
341
+ wait: options?.wait ?? false,
342
+ timeout: options?.timeout,
343
+ mode: options?.mode ?? 'exclusive',
344
+ };
345
+ try {
346
+ const handle = await acquireWorkspaceLock(repo, resource, operation, acquireOptions);
347
+ return {
348
+ resource,
349
+ release: () => handle.release(),
350
+ };
351
+ }
352
+ catch {
353
+ // Lock couldn't be acquired
354
+ return null;
355
+ }
356
+ }
357
+ getState(repo, resource) {
358
+ return getWorkspaceLockState(repo, resource);
359
+ }
360
+ isHolderAlive(holder) {
361
+ return isLockHolderAlive(holder);
362
+ }
363
+ }
364
+ //# sourceMappingURL=LocalLockService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalLockService.js","sourceRoot":"","sources":["../../../../src/storage/local/LocalLockService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=LocalLockService.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalLockService.spec.d.ts","sourceRoot":"","sources":["../../../../src/storage/local/LocalLockService.spec.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ /**
6
+ * Tests for LocalLockService - workspace locking mechanism
7
+ */
8
+ import { describe, it, beforeEach, afterEach } from 'node:test';
9
+ import assert from 'node:assert';
10
+ import * as fs from 'fs/promises';
11
+ import * as path from 'path';
12
+ import * as os from 'os';
13
+ import { variant, encodeBeast2For, printFor, VariantType } from '@elaraai/east';
14
+ import { LockStateType, ProcessHolderType } from '@elaraai/e3-types';
15
+ import { acquireWorkspaceLock, getWorkspaceLockHolder, workspaceLockPath, } from './LocalLockService.js';
16
+ import { WorkspaceLockError } from '../../errors.js';
17
+ describe('LocalLockService', () => {
18
+ let testDir;
19
+ let repoPath;
20
+ beforeEach(async () => {
21
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'e3-lock-test-'));
22
+ repoPath = path.join(testDir, 'repo');
23
+ await fs.mkdir(path.join(repoPath, 'workspaces'), { recursive: true });
24
+ });
25
+ afterEach(async () => {
26
+ await fs.rm(testDir, { recursive: true, force: true });
27
+ });
28
+ describe('workspaceLockPath', () => {
29
+ it('returns correct path', () => {
30
+ const lockPath = workspaceLockPath('/repo', 'myws');
31
+ assert.strictEqual(lockPath, '/repo/workspaces/myws.lock');
32
+ });
33
+ });
34
+ describe('acquireWorkspaceLock', () => {
35
+ it('acquires lock on unlocked workspace', async () => {
36
+ const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
37
+ assert.strictEqual(lock.workspace, 'test-ws');
38
+ assert.ok(lock.lockPath.endsWith('test-ws.lock'));
39
+ await lock.release();
40
+ });
41
+ it('creates lock file with metadata', async () => {
42
+ const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
43
+ // Use getWorkspaceLockHolder to read the metadata (it handles beast2 format)
44
+ const holder = await getWorkspaceLockHolder(repoPath, 'test-ws');
45
+ assert.ok(holder);
46
+ assert.strictEqual(holder.pid, process.pid);
47
+ assert.ok(holder.bootId);
48
+ assert.ok(holder.acquiredAt);
49
+ assert.ok(holder.command);
50
+ assert.strictEqual(holder.operation, 'dataflow');
51
+ await lock.release();
52
+ });
53
+ it('removes lock file on release', async () => {
54
+ const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
55
+ const lockPath = workspaceLockPath(repoPath, 'test-ws');
56
+ // Lock file should exist
57
+ await fs.access(lockPath);
58
+ await lock.release();
59
+ // Lock file should be gone
60
+ await assert.rejects(fs.access(lockPath), { code: 'ENOENT' });
61
+ });
62
+ it('release is idempotent', async () => {
63
+ const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
64
+ await lock.release();
65
+ await lock.release(); // Should not throw
66
+ await lock.release(); // Should not throw
67
+ });
68
+ it('throws WorkspaceLockError when already locked', async () => {
69
+ const lock1 = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
70
+ try {
71
+ await assert.rejects(acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null)), (err) => {
72
+ assert.ok(err instanceof WorkspaceLockError);
73
+ assert.strictEqual(err.workspace, 'test-ws');
74
+ // Should have holder info since we wrote metadata
75
+ const holder = err.holder;
76
+ assert.ok(holder);
77
+ assert.strictEqual(holder.pid, process.pid);
78
+ return true;
79
+ });
80
+ }
81
+ finally {
82
+ await lock1.release();
83
+ }
84
+ });
85
+ it('allows acquiring lock after release', async () => {
86
+ const lock1 = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
87
+ await lock1.release();
88
+ const lock2 = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
89
+ assert.ok(lock2);
90
+ await lock2.release();
91
+ });
92
+ it('allows different workspaces to be locked independently', async () => {
93
+ const lock1 = await acquireWorkspaceLock(repoPath, 'ws1', variant('dataflow', null));
94
+ const lock2 = await acquireWorkspaceLock(repoPath, 'ws2', variant('dataflow', null));
95
+ assert.strictEqual(lock1.workspace, 'ws1');
96
+ assert.strictEqual(lock2.workspace, 'ws2');
97
+ await lock1.release();
98
+ await lock2.release();
99
+ });
100
+ });
101
+ describe('getWorkspaceLockHolder', () => {
102
+ it('returns null for unlocked workspace', async () => {
103
+ const holder = await getWorkspaceLockHolder(repoPath, 'test-ws');
104
+ assert.strictEqual(holder, null);
105
+ });
106
+ it('returns holder info for locked workspace', async () => {
107
+ const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
108
+ const holder = await getWorkspaceLockHolder(repoPath, 'test-ws');
109
+ assert.ok(holder);
110
+ assert.strictEqual(holder.pid, process.pid);
111
+ assert.ok(holder.acquiredAt);
112
+ await lock.release();
113
+ });
114
+ it('returns null after lock is released', async () => {
115
+ const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
116
+ await lock.release();
117
+ const holder = await getWorkspaceLockHolder(repoPath, 'test-ws');
118
+ assert.strictEqual(holder, null);
119
+ });
120
+ it('cleans up stale lock with dead PID', async () => {
121
+ // Write a fake lock file in beast2 format with a non-existent PID
122
+ const lockPath = workspaceLockPath(repoPath, 'test-ws');
123
+ // Create holder as East text string
124
+ const HolderVariantType = VariantType({ process: ProcessHolderType });
125
+ const printHolder = printFor(HolderVariantType);
126
+ const holderString = printHolder(variant('process', {
127
+ pid: 99999999n, // Very unlikely to exist
128
+ bootId: 'fake-boot-id',
129
+ startTime: 0n,
130
+ command: 'fake command',
131
+ }));
132
+ const fakeLockState = {
133
+ operation: variant('dataflow', null),
134
+ holder: holderString,
135
+ acquiredAt: new Date(),
136
+ expiresAt: variant('none', null),
137
+ };
138
+ const encoder = encodeBeast2For(LockStateType);
139
+ await fs.writeFile(lockPath, encoder(fakeLockState));
140
+ // getWorkspaceLockHolder should detect this as stale and clean up
141
+ const holder = await getWorkspaceLockHolder(repoPath, 'test-ws');
142
+ assert.strictEqual(holder, null);
143
+ // Lock file should be cleaned up
144
+ await assert.rejects(fs.access(lockPath), { code: 'ENOENT' });
145
+ });
146
+ });
147
+ });
148
+ //# sourceMappingURL=LocalLockService.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalLockService.spec.js","sourceRoot":"","sources":["../../../../src/storage/local/LocalLockService.spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ import type { LogChunk, LogStore } from '../interfaces.js';
6
+ /**
7
+ * Local filesystem implementation of LogStore.
8
+ *
9
+ * Logs are stored as text files in the execution directory:
10
+ * executions/<taskHash>/<inputsHash>/<executionId>/stdout.txt
11
+ * executions/<taskHash>/<inputsHash>/<executionId>/stderr.txt
12
+ *
13
+ * The `repo` parameter is the path to the e3 repository directory.
14
+ */
15
+ export declare class LocalLogStore implements LogStore {
16
+ private logPath;
17
+ append(repo: string, taskHash: string, inputsHash: string, executionId: string, stream: 'stdout' | 'stderr', data: string): Promise<void>;
18
+ read(repo: string, taskHash: string, inputsHash: string, executionId: string, stream: 'stdout' | 'stderr', options?: {
19
+ offset?: number;
20
+ limit?: number;
21
+ }): Promise<LogChunk>;
22
+ }
23
+ //# sourceMappingURL=LocalLogStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalLogStore.d.ts","sourceRoot":"","sources":["../../../../src/storage/local/LocalLogStore.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG3D;;;;;;;;GAQG;AACH,qBAAa,aAAc,YAAW,QAAQ;IAC5C,OAAO,CAAC,OAAO;IAWT,MAAM,CACV,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC;IAQV,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAC3B,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5C,OAAO,CAAC,QAAQ,CAAC;CAwCrB"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import { isNotFoundError } from '../../errors.js';
8
+ /**
9
+ * Local filesystem implementation of LogStore.
10
+ *
11
+ * Logs are stored as text files in the execution directory:
12
+ * executions/<taskHash>/<inputsHash>/<executionId>/stdout.txt
13
+ * executions/<taskHash>/<inputsHash>/<executionId>/stderr.txt
14
+ *
15
+ * The `repo` parameter is the path to the e3 repository directory.
16
+ */
17
+ export class LocalLogStore {
18
+ logPath(repo, taskHash, inputsHash, executionId, stream) {
19
+ return path.join(repo, 'executions', taskHash, inputsHash, executionId, `${stream}.txt`);
20
+ }
21
+ async append(repo, taskHash, inputsHash, executionId, stream, data) {
22
+ const logFile = this.logPath(repo, taskHash, inputsHash, executionId, stream);
23
+ const dir = path.dirname(logFile);
24
+ await fs.mkdir(dir, { recursive: true });
25
+ await fs.appendFile(logFile, data);
26
+ }
27
+ async read(repo, taskHash, inputsHash, executionId, stream, options) {
28
+ const logFile = this.logPath(repo, taskHash, inputsHash, executionId, stream);
29
+ const offset = options?.offset ?? 0;
30
+ const limit = options?.limit ?? 65536; // 64KB default
31
+ try {
32
+ const stat = await fs.stat(logFile);
33
+ const totalSize = stat.size;
34
+ // Open file and read chunk
35
+ const fd = await fs.open(logFile, 'r');
36
+ try {
37
+ const buffer = Buffer.alloc(Math.min(limit, Math.max(0, totalSize - offset)));
38
+ const { bytesRead } = await fd.read(buffer, 0, buffer.length, offset);
39
+ return {
40
+ data: buffer.slice(0, bytesRead).toString('utf-8'),
41
+ offset,
42
+ size: bytesRead,
43
+ totalSize,
44
+ complete: offset + bytesRead >= totalSize,
45
+ };
46
+ }
47
+ finally {
48
+ await fd.close();
49
+ }
50
+ }
51
+ catch (err) {
52
+ if (isNotFoundError(err)) {
53
+ // Log file doesn't exist yet
54
+ return {
55
+ data: '',
56
+ offset: 0,
57
+ size: 0,
58
+ totalSize: 0,
59
+ complete: true,
60
+ };
61
+ }
62
+ throw err;
63
+ }
64
+ }
65
+ }
66
+ //# sourceMappingURL=LocalLogStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalLogStore.js","sourceRoot":"","sources":["../../../../src/storage/local/LocalLogStore.ts"],"names":[],"mappings":"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"}