@elaraai/e3-rack 0.0.0-bootstrap.0 → 1.0.94

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 (306) hide show
  1. package/README.md +407 -2
  2. package/dist/src/client/client.spec.d.ts +6 -0
  3. package/dist/src/client/client.spec.d.ts.map +1 -0
  4. package/dist/src/client/client.spec.js +150 -0
  5. package/dist/src/client/client.spec.js.map +1 -0
  6. package/dist/src/client/connect.d.ts +52 -0
  7. package/dist/src/client/connect.d.ts.map +1 -0
  8. package/dist/src/client/connect.js +112 -0
  9. package/dist/src/client/connect.js.map +1 -0
  10. package/dist/src/client/hub-client.d.ts +57 -0
  11. package/dist/src/client/hub-client.d.ts.map +1 -0
  12. package/dist/src/client/hub-client.js +87 -0
  13. package/dist/src/client/hub-client.js.map +1 -0
  14. package/dist/src/client/local-http.d.ts +78 -0
  15. package/dist/src/client/local-http.d.ts.map +1 -0
  16. package/dist/src/client/local-http.js +149 -0
  17. package/dist/src/client/local-http.js.map +1 -0
  18. package/dist/src/client/session-attempts.d.ts +20 -0
  19. package/dist/src/client/session-attempts.d.ts.map +1 -0
  20. package/dist/src/client/session-attempts.js +88 -0
  21. package/dist/src/client/session-attempts.js.map +1 -0
  22. package/dist/src/client/session-data-server.d.ts +37 -0
  23. package/dist/src/client/session-data-server.d.ts.map +1 -0
  24. package/dist/src/client/session-data-server.js +280 -0
  25. package/dist/src/client/session-data-server.js.map +1 -0
  26. package/dist/src/client/session-data-server.spec.d.ts +6 -0
  27. package/dist/src/client/session-data-server.spec.d.ts.map +1 -0
  28. package/dist/src/client/session-data-server.spec.js +192 -0
  29. package/dist/src/client/session-data-server.spec.js.map +1 -0
  30. package/dist/src/client/session.d.ts +119 -0
  31. package/dist/src/client/session.d.ts.map +1 -0
  32. package/dist/src/client/session.js +290 -0
  33. package/dist/src/client/session.js.map +1 -0
  34. package/dist/src/hub/config.d.ts +74 -0
  35. package/dist/src/hub/config.d.ts.map +1 -0
  36. package/dist/src/hub/config.js +159 -0
  37. package/dist/src/hub/config.js.map +1 -0
  38. package/dist/src/hub/fair-lease-store.d.ts +36 -0
  39. package/dist/src/hub/fair-lease-store.d.ts.map +1 -0
  40. package/dist/src/hub/fair-lease-store.js +73 -0
  41. package/dist/src/hub/fair-lease-store.js.map +1 -0
  42. package/dist/src/hub/hub.d.ts +91 -0
  43. package/dist/src/hub/hub.d.ts.map +1 -0
  44. package/dist/src/hub/hub.js +641 -0
  45. package/dist/src/hub/hub.js.map +1 -0
  46. package/dist/src/hub/hub.spec.d.ts +6 -0
  47. package/dist/src/hub/hub.spec.d.ts.map +1 -0
  48. package/dist/src/hub/hub.spec.js +363 -0
  49. package/dist/src/hub/hub.spec.js.map +1 -0
  50. package/dist/src/hub/infrastructure.spec.d.ts +6 -0
  51. package/dist/src/hub/infrastructure.spec.d.ts.map +1 -0
  52. package/dist/src/hub/infrastructure.spec.js +211 -0
  53. package/dist/src/hub/infrastructure.spec.js.map +1 -0
  54. package/dist/src/hub/lock.d.ts +62 -0
  55. package/dist/src/hub/lock.d.ts.map +1 -0
  56. package/dist/src/hub/lock.js +165 -0
  57. package/dist/src/hub/lock.js.map +1 -0
  58. package/dist/src/hub/main.d.ts +16 -0
  59. package/dist/src/hub/main.d.ts.map +1 -0
  60. package/dist/src/hub/main.js +56 -0
  61. package/dist/src/hub/main.js.map +1 -0
  62. package/dist/src/hub/session-bridge.d.ts +116 -0
  63. package/dist/src/hub/session-bridge.d.ts.map +1 -0
  64. package/dist/src/hub/session-bridge.js +336 -0
  65. package/dist/src/hub/session-bridge.js.map +1 -0
  66. package/dist/src/identity/file-identity-store.d.ts +18 -0
  67. package/dist/src/identity/file-identity-store.d.ts.map +1 -0
  68. package/dist/src/identity/file-identity-store.js +34 -0
  69. package/dist/src/identity/file-identity-store.js.map +1 -0
  70. package/dist/src/identity/file-identity-store.spec.d.ts +6 -0
  71. package/dist/src/identity/file-identity-store.spec.d.ts.map +1 -0
  72. package/dist/src/identity/file-identity-store.spec.js +59 -0
  73. package/dist/src/identity/file-identity-store.spec.js.map +1 -0
  74. package/dist/src/identity/in-memory-identity-store.d.ts +48 -0
  75. package/dist/src/identity/in-memory-identity-store.d.ts.map +1 -0
  76. package/dist/src/identity/in-memory-identity-store.js +102 -0
  77. package/dist/src/identity/in-memory-identity-store.js.map +1 -0
  78. package/dist/src/identity/machine-identity.d.ts +134 -0
  79. package/dist/src/identity/machine-identity.d.ts.map +1 -0
  80. package/dist/src/identity/machine-identity.js +65 -0
  81. package/dist/src/identity/machine-identity.js.map +1 -0
  82. package/dist/src/index.d.ts +34 -0
  83. package/dist/src/index.d.ts.map +1 -0
  84. package/dist/src/index.js +34 -0
  85. package/dist/src/index.js.map +1 -0
  86. package/dist/src/lease/closure.d.ts +96 -0
  87. package/dist/src/lease/closure.d.ts.map +1 -0
  88. package/dist/src/lease/closure.js +192 -0
  89. package/dist/src/lease/closure.js.map +1 -0
  90. package/dist/src/lease/closure.spec.d.ts +6 -0
  91. package/dist/src/lease/closure.spec.d.ts.map +1 -0
  92. package/dist/src/lease/closure.spec.js +35 -0
  93. package/dist/src/lease/closure.spec.js.map +1 -0
  94. package/dist/src/lease/coordinator.d.ts +34 -0
  95. package/dist/src/lease/coordinator.d.ts.map +1 -0
  96. package/dist/src/lease/coordinator.js +42 -0
  97. package/dist/src/lease/coordinator.js.map +1 -0
  98. package/dist/src/lease/in-memory-lease-store.d.ts +58 -0
  99. package/dist/src/lease/in-memory-lease-store.d.ts.map +1 -0
  100. package/dist/src/lease/in-memory-lease-store.js +132 -0
  101. package/dist/src/lease/in-memory-lease-store.js.map +1 -0
  102. package/dist/src/lease/lease-store.d.ts +169 -0
  103. package/dist/src/lease/lease-store.d.ts.map +1 -0
  104. package/dist/src/lease/lease-store.js +94 -0
  105. package/dist/src/lease/lease-store.js.map +1 -0
  106. package/dist/src/lease/lease-store.spec.d.ts +6 -0
  107. package/dist/src/lease/lease-store.spec.d.ts.map +1 -0
  108. package/dist/src/lease/lease-store.spec.js +8 -0
  109. package/dist/src/lease/lease-store.spec.js.map +1 -0
  110. package/dist/src/lease/rack-dispatch.d.ts +105 -0
  111. package/dist/src/lease/rack-dispatch.d.ts.map +1 -0
  112. package/dist/src/lease/rack-dispatch.js +6 -0
  113. package/dist/src/lease/rack-dispatch.js.map +1 -0
  114. package/dist/src/mutation-queue.d.ts +13 -0
  115. package/dist/src/mutation-queue.d.ts.map +1 -0
  116. package/dist/src/mutation-queue.js +17 -0
  117. package/dist/src/mutation-queue.js.map +1 -0
  118. package/dist/src/paths.d.ts +56 -0
  119. package/dist/src/paths.d.ts.map +1 -0
  120. package/dist/src/paths.js +82 -0
  121. package/dist/src/paths.js.map +1 -0
  122. package/dist/src/protocol/control.d.ts +295 -0
  123. package/dist/src/protocol/control.d.ts.map +1 -0
  124. package/dist/src/protocol/control.js +74 -0
  125. package/dist/src/protocol/control.js.map +1 -0
  126. package/dist/src/protocol/index.d.ts +9 -0
  127. package/dist/src/protocol/index.d.ts.map +1 -0
  128. package/dist/src/protocol/index.js +9 -0
  129. package/dist/src/protocol/index.js.map +1 -0
  130. package/dist/src/protocol/rack-wire.d.ts +357 -0
  131. package/dist/src/protocol/rack-wire.d.ts.map +1 -0
  132. package/dist/src/protocol/rack-wire.js +262 -0
  133. package/dist/src/protocol/rack-wire.js.map +1 -0
  134. package/dist/src/protocol/rack-wire.spec.d.ts +6 -0
  135. package/dist/src/protocol/rack-wire.spec.d.ts.map +1 -0
  136. package/dist/src/protocol/rack-wire.spec.js +14 -0
  137. package/dist/src/protocol/rack-wire.spec.js.map +1 -0
  138. package/dist/src/protocol/registration.d.ts +70 -0
  139. package/dist/src/protocol/registration.d.ts.map +1 -0
  140. package/dist/src/protocol/registration.js +67 -0
  141. package/dist/src/protocol/registration.js.map +1 -0
  142. package/dist/src/protocol/session-data.d.ts +69 -0
  143. package/dist/src/protocol/session-data.d.ts.map +1 -0
  144. package/dist/src/protocol/session-data.js +26 -0
  145. package/dist/src/protocol/session-data.js.map +1 -0
  146. package/dist/src/protocol/task-envelope.d.ts +198 -0
  147. package/dist/src/protocol/task-envelope.d.ts.map +1 -0
  148. package/dist/src/protocol/task-envelope.js +93 -0
  149. package/dist/src/protocol/task-envelope.js.map +1 -0
  150. package/dist/src/protocol/wire-golden.d.ts +8 -0
  151. package/dist/src/protocol/wire-golden.d.ts.map +1 -0
  152. package/dist/src/protocol/wire-golden.js +34 -0
  153. package/dist/src/protocol/wire-golden.js.map +1 -0
  154. package/dist/src/protocol/wire-samples.d.ts +11 -0
  155. package/dist/src/protocol/wire-samples.d.ts.map +1 -0
  156. package/dist/src/protocol/wire-samples.js +52 -0
  157. package/dist/src/protocol/wire-samples.js.map +1 -0
  158. package/dist/src/registry/file-rack-registry.d.ts +68 -0
  159. package/dist/src/registry/file-rack-registry.d.ts.map +1 -0
  160. package/dist/src/registry/file-rack-registry.js +138 -0
  161. package/dist/src/registry/file-rack-registry.js.map +1 -0
  162. package/dist/src/registry/file-rack-registry.spec.d.ts +6 -0
  163. package/dist/src/registry/file-rack-registry.spec.d.ts.map +1 -0
  164. package/dist/src/registry/file-rack-registry.spec.js +50 -0
  165. package/dist/src/registry/file-rack-registry.spec.js.map +1 -0
  166. package/dist/src/registry/rack-registry.d.ts +64 -0
  167. package/dist/src/registry/rack-registry.d.ts.map +1 -0
  168. package/dist/src/registry/rack-registry.js +65 -0
  169. package/dist/src/registry/rack-registry.js.map +1 -0
  170. package/dist/src/registry/rack-registry.spec.d.ts +6 -0
  171. package/dist/src/registry/rack-registry.spec.d.ts.map +1 -0
  172. package/dist/src/registry/rack-registry.spec.js +8 -0
  173. package/dist/src/registry/rack-registry.spec.js.map +1 -0
  174. package/dist/src/routing/eligibility.d.ts +38 -0
  175. package/dist/src/routing/eligibility.d.ts.map +1 -0
  176. package/dist/src/routing/eligibility.js +78 -0
  177. package/dist/src/routing/eligibility.js.map +1 -0
  178. package/dist/src/routing/host-coupling.d.ts +26 -0
  179. package/dist/src/routing/host-coupling.d.ts.map +1 -0
  180. package/dist/src/routing/host-coupling.js +62 -0
  181. package/dist/src/routing/host-coupling.js.map +1 -0
  182. package/dist/src/routing/policy.d.ts +188 -0
  183. package/dist/src/routing/policy.d.ts.map +1 -0
  184. package/dist/src/routing/policy.js +212 -0
  185. package/dist/src/routing/policy.js.map +1 -0
  186. package/dist/src/routing/routing.spec.d.ts +6 -0
  187. package/dist/src/routing/routing.spec.d.ts.map +1 -0
  188. package/dist/src/routing/routing.spec.js +100 -0
  189. package/dist/src/routing/routing.spec.js.map +1 -0
  190. package/dist/src/routing/tiers.d.ts +22 -0
  191. package/dist/src/routing/tiers.d.ts.map +1 -0
  192. package/dist/src/routing/tiers.js +26 -0
  193. package/dist/src/routing/tiers.js.map +1 -0
  194. package/dist/src/runner/api-server.d.ts +45 -0
  195. package/dist/src/runner/api-server.d.ts.map +1 -0
  196. package/dist/src/runner/api-server.js +21 -0
  197. package/dist/src/runner/api-server.js.map +1 -0
  198. package/dist/src/runner/capacity.d.ts +13 -0
  199. package/dist/src/runner/capacity.d.ts.map +1 -0
  200. package/dist/src/runner/capacity.js +18 -0
  201. package/dist/src/runner/capacity.js.map +1 -0
  202. package/dist/src/runner/rack-body.d.ts +43 -0
  203. package/dist/src/runner/rack-body.d.ts.map +1 -0
  204. package/dist/src/runner/rack-body.js +208 -0
  205. package/dist/src/runner/rack-body.js.map +1 -0
  206. package/dist/src/runner/rack-runner.d.ts +62 -0
  207. package/dist/src/runner/rack-runner.d.ts.map +1 -0
  208. package/dist/src/runner/rack-runner.js +75 -0
  209. package/dist/src/runner/rack-runner.js.map +1 -0
  210. package/dist/src/runner/rack-runner.spec.d.ts +6 -0
  211. package/dist/src/runner/rack-runner.spec.d.ts.map +1 -0
  212. package/dist/src/runner/rack-runner.spec.js +302 -0
  213. package/dist/src/runner/rack-runner.spec.js.map +1 -0
  214. package/dist/src/runner/split.spec.d.ts +6 -0
  215. package/dist/src/runner/split.spec.d.ts.map +1 -0
  216. package/dist/src/runner/split.spec.js +188 -0
  217. package/dist/src/runner/split.spec.js.map +1 -0
  218. package/dist/src/runner-event.d.ts +261 -0
  219. package/dist/src/runner-event.d.ts.map +1 -0
  220. package/dist/src/runner-event.js +332 -0
  221. package/dist/src/runner-event.js.map +1 -0
  222. package/dist/src/server/beast2.d.ts +19 -0
  223. package/dist/src/server/beast2.d.ts.map +1 -0
  224. package/dist/src/server/beast2.js +18 -0
  225. package/dist/src/server/beast2.js.map +1 -0
  226. package/dist/src/server/body-limit.d.ts +16 -0
  227. package/dist/src/server/body-limit.d.ts.map +1 -0
  228. package/dist/src/server/body-limit.js +53 -0
  229. package/dist/src/server/body-limit.js.map +1 -0
  230. package/dist/src/server/body-limit.spec.d.ts +2 -0
  231. package/dist/src/server/body-limit.spec.d.ts.map +1 -0
  232. package/dist/src/server/body-limit.spec.js +32 -0
  233. package/dist/src/server/body-limit.spec.js.map +1 -0
  234. package/dist/src/server/bounded.d.ts +16 -0
  235. package/dist/src/server/bounded.d.ts.map +1 -0
  236. package/dist/src/server/bounded.js +39 -0
  237. package/dist/src/server/bounded.js.map +1 -0
  238. package/dist/src/server/dispatch.d.ts +137 -0
  239. package/dist/src/server/dispatch.d.ts.map +1 -0
  240. package/dist/src/server/dispatch.js +267 -0
  241. package/dist/src/server/dispatch.js.map +1 -0
  242. package/dist/src/server/env-publication.d.ts +45 -0
  243. package/dist/src/server/env-publication.d.ts.map +1 -0
  244. package/dist/src/server/env-publication.js +10 -0
  245. package/dist/src/server/env-publication.js.map +1 -0
  246. package/dist/src/server/index.d.ts +16 -0
  247. package/dist/src/server/index.d.ts.map +1 -0
  248. package/dist/src/server/index.js +17 -0
  249. package/dist/src/server/index.js.map +1 -0
  250. package/dist/src/server/protocol.spec.d.ts +6 -0
  251. package/dist/src/server/protocol.spec.d.ts.map +1 -0
  252. package/dist/src/server/protocol.spec.js +78 -0
  253. package/dist/src/server/protocol.spec.js.map +1 -0
  254. package/dist/src/server/rack-routes.d.ts +175 -0
  255. package/dist/src/server/rack-routes.d.ts.map +1 -0
  256. package/dist/src/server/rack-routes.js +755 -0
  257. package/dist/src/server/rack-routes.js.map +1 -0
  258. package/dist/src/server/rack-routes.spec.d.ts +6 -0
  259. package/dist/src/server/rack-routes.spec.d.ts.map +1 -0
  260. package/dist/src/server/rack-routes.spec.js +477 -0
  261. package/dist/src/server/rack-routes.spec.js.map +1 -0
  262. package/dist/src/server/storage-bridge.d.ts +140 -0
  263. package/dist/src/server/storage-bridge.d.ts.map +1 -0
  264. package/dist/src/server/storage-bridge.js +6 -0
  265. package/dist/src/server/storage-bridge.js.map +1 -0
  266. package/dist/src/state-file.d.ts +10 -0
  267. package/dist/src/state-file.d.ts.map +1 -0
  268. package/dist/src/state-file.js +38 -0
  269. package/dist/src/state-file.js.map +1 -0
  270. package/dist/src/testing/in-memory.d.ts +116 -0
  271. package/dist/src/testing/in-memory.d.ts.map +1 -0
  272. package/dist/src/testing/in-memory.js +193 -0
  273. package/dist/src/testing/in-memory.js.map +1 -0
  274. package/dist/src/testing/index.d.ts +14 -0
  275. package/dist/src/testing/index.d.ts.map +1 -0
  276. package/dist/src/testing/index.js +14 -0
  277. package/dist/src/testing/index.js.map +1 -0
  278. package/dist/src/testing/lease-store-contract.d.ts +14 -0
  279. package/dist/src/testing/lease-store-contract.d.ts.map +1 -0
  280. package/dist/src/testing/lease-store-contract.js +78 -0
  281. package/dist/src/testing/lease-store-contract.js.map +1 -0
  282. package/dist/src/testing/protocol-suite.d.ts +38 -0
  283. package/dist/src/testing/protocol-suite.d.ts.map +1 -0
  284. package/dist/src/testing/protocol-suite.js +207 -0
  285. package/dist/src/testing/protocol-suite.js.map +1 -0
  286. package/dist/src/testing/rack-registry-contract.d.ts +15 -0
  287. package/dist/src/testing/rack-registry-contract.d.ts.map +1 -0
  288. package/dist/src/testing/rack-registry-contract.js +106 -0
  289. package/dist/src/testing/rack-registry-contract.js.map +1 -0
  290. package/dist/src/testing/test-rack-agent.d.ts +121 -0
  291. package/dist/src/testing/test-rack-agent.d.ts.map +1 -0
  292. package/dist/src/testing/test-rack-agent.js +316 -0
  293. package/dist/src/testing/test-rack-agent.js.map +1 -0
  294. package/dist/src/tls/tls.d.ts +67 -0
  295. package/dist/src/tls/tls.d.ts.map +1 -0
  296. package/dist/src/tls/tls.js +200 -0
  297. package/dist/src/tls/tls.js.map +1 -0
  298. package/dist/src/tls/tls.spec.d.ts +6 -0
  299. package/dist/src/tls/tls.spec.d.ts.map +1 -0
  300. package/dist/src/tls/tls.spec.js +101 -0
  301. package/dist/src/tls/tls.spec.js.map +1 -0
  302. package/dist/src/version.d.ts +9 -0
  303. package/dist/src/version.d.ts.map +1 -0
  304. package/dist/src/version.js +10 -0
  305. package/dist/src/version.js.map +1 -0
  306. package/package.json +67 -7
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ import { afterEach, it } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { createHash, randomUUID } from 'node:crypto';
8
+ import { mkdtemp, readdir, rm } from 'node:fs/promises';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { PassThrough, Readable } from 'node:stream';
12
+ import { setTimeout as delay } from 'node:timers/promises';
13
+ import { BooleanType, IntegerType, StringType, encodeBeast2For, variant } from '@elaraai/east';
14
+ import { LocalStorage, inputsHash, processOwner, repoInit, uuidv7 } from '@elaraai/e3-core';
15
+ import { startSessionDataServer } from './session-data-server.js';
16
+ import { RackHubError, socketRequest, socketStream } from './local-http.js';
17
+ import { LeaseOutcomeType, LeaseRunningType, SessionLogsType, SessionUploadType, VerifyOutputType } from '../protocol/session-data.js';
18
+ import { makeLeaseId } from '../lease/lease-store.js';
19
+ const cleanup = [];
20
+ afterEach(async () => { for (const close of cleanup.splice(0).reverse())
21
+ await close(); });
22
+ async function setup() {
23
+ const home = await mkdtemp(join(tmpdir(), 'e3d-'));
24
+ cleanup.push(() => rm(home, { recursive: true, force: true }));
25
+ const repo = join(home, 'repo');
26
+ assert.equal(repoInit(repo).success, true);
27
+ const storage = new LocalStorage();
28
+ const logs = [];
29
+ const options = { storage, repoPath: repo, home, dataToken: randomUUID(), onLog: (_id, _stream, data) => { logs.push(data); } };
30
+ const data = await startSessionDataServer(options);
31
+ cleanup.push(() => data.close());
32
+ return { home, repo, storage, options, logs, data, auth: { bearer: options.dataToken } };
33
+ }
34
+ const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
35
+ function uploadUrl(id, info) {
36
+ return `/v1/uploads/${id}?${new URLSearchParams({ lease: info.leaseId, hash: info.hash, size: `${info.size}`, receipt: info.receipt })}`;
37
+ }
38
+ it('rejects an already-cancelled streamed upload without crashing the process', async () => {
39
+ const { data, auth } = await setup();
40
+ const bytes = Buffer.from('cancelled upload');
41
+ const body = Readable.from([bytes]);
42
+ const info = { leaseId: 'lease', hash: digest(bytes), size: BigInt(bytes.length), receipt: randomUUID() };
43
+ await assert.rejects(socketStream(data.socketPath, 'PUT', uploadUrl(randomUUID(), info), {
44
+ ...auth, rawBody: body, signal: AbortSignal.abort(),
45
+ }), { code: 'aborted' });
46
+ await delay(0); // Allow asynchronous stream errors to reach the test runner.
47
+ assert.equal(body.destroyed, true);
48
+ const response = await socketStream(data.socketPath, 'HEAD', `/v1/objects/${info.hash}`, auth);
49
+ response.resume();
50
+ assert.equal(response.statusCode, 404);
51
+ });
52
+ it('streams large objects, verifies bytes before adoption and preserves an adopted inode on retries', async () => {
53
+ const { data, auth, repo, storage } = await setup();
54
+ const bytes = Buffer.alloc(9 * 1024 * 1024, 42);
55
+ const hash = digest(bytes);
56
+ const id = randomUUID();
57
+ const info = { leaseId: 'lease', hash, size: BigInt(bytes.length), receipt: randomUUID() };
58
+ const upload = async (body, path = uploadUrl(id, info)) => {
59
+ const response = await socketStream(data.socketPath, 'PUT', path, { ...auth, rawBody: Readable.from((function* () {
60
+ for (let at = 0; at < body.length; at += 65536)
61
+ yield body.subarray(at, at + 65536);
62
+ })()) });
63
+ response.resume();
64
+ return response;
65
+ };
66
+ const denied = await socketStream(data.socketPath, 'HEAD', `/v1/objects/${hash}`);
67
+ denied.resume();
68
+ assert.equal(denied.statusCode, 401);
69
+ const wrong = Buffer.from(bytes);
70
+ wrong[0] = 1;
71
+ assert.equal((await upload(wrong)).statusCode, 400);
72
+ const commit = (declaration = info) => socketRequest(data.socketPath, 'POST', `/v1/uploads/${id}/commit`, {
73
+ ...auth, requestType: SessionUploadType, body: declaration, responseType: BooleanType,
74
+ });
75
+ assert.equal(await commit(), false);
76
+ assert.equal(await storage.objects.exists(repo, hash), false);
77
+ const accepted = await upload(bytes);
78
+ assert.equal(accepted.statusCode, 200);
79
+ assert.equal(accepted.headers['x-amz-version-id'], info.receipt);
80
+ assert.equal(await commit({ ...info, receipt: randomUUID() }), false);
81
+ assert.equal(await commit({ ...info, leaseId: 'another' }), false);
82
+ assert.equal(await commit({ ...info, size: info.size + 1n }), false);
83
+ assert.equal(await commit(), true);
84
+ assert.equal(await commit(), true);
85
+ // A retry with a lost acknowledgment must not reopen the committed inode.
86
+ assert.equal((await upload(wrong)).statusCode, 200);
87
+ assert.equal(digest(await storage.objects.read(repo, hash)), hash);
88
+ const response = await socketStream(data.socketPath, 'GET', `/v1/objects/${hash}`, auth);
89
+ assert.equal(Number(response.headers['content-length']), bytes.length);
90
+ const received = createHash('sha256');
91
+ let size = 0;
92
+ for await (const chunk of response) {
93
+ received.update(chunk);
94
+ size += chunk.length;
95
+ }
96
+ assert.equal(size, bytes.length);
97
+ assert.equal(received.digest('hex'), hash);
98
+ assert.equal(await socketRequest(data.socketPath, 'DELETE', `/v1/uploads/${id}`, { ...auth, responseType: BooleanType }), true);
99
+ assert.equal(await storage.objects.exists(repo, hash), true);
100
+ });
101
+ it('recovers an acknowledged upload in a successor session and refuses inline and size overflows', async () => {
102
+ const { data, auth, options, storage, repo } = await setup();
103
+ const bytes = encodeBeast2For(IntegerType)(42n);
104
+ const id = randomUUID();
105
+ const info = { leaseId: 'lease', hash: digest(bytes), size: BigInt(bytes.length), receipt: randomUUID() };
106
+ const response = await socketStream(data.socketPath, 'PUT', uploadUrl(id, info), { ...auth, rawBody: bytes });
107
+ response.resume();
108
+ assert.equal(response.statusCode, 200);
109
+ await data.close();
110
+ const successor = await startSessionDataServer(options);
111
+ cleanup.push(() => successor.close());
112
+ assert.equal(await socketRequest(successor.socketPath, 'POST', `/v1/uploads/${id}/commit`, {
113
+ ...auth, requestType: SessionUploadType, body: info, responseType: BooleanType,
114
+ }), true);
115
+ assert.equal(await storage.objects.exists(repo, info.hash), true);
116
+ await assert.rejects(socketRequest(successor.socketPath, 'POST', '/v1/objects', {
117
+ ...auth, rawBody: new Uint8Array(4097), responseType: StringType,
118
+ }), (error) => error instanceof RackHubError && error.code === 'http_413' && /exceeds 4096 bytes/.test(error.message));
119
+ const bad = await socketStream(successor.socketPath, 'PUT', uploadUrl(randomUUID(), { ...info, size: 1n << 100n }), { ...auth, rawBody: new Uint8Array() });
120
+ bad.resume();
121
+ assert.equal(bad.statusCode, 400);
122
+ const oversized = await socketStream(successor.socketPath, 'PUT', uploadUrl(randomUUID(), { ...info, size: 1n }), { ...auth, rawBody: bytes });
123
+ oversized.resume();
124
+ assert.equal(oversized.statusCode, 400);
125
+ });
126
+ it('writes a claim-time attempt and flushes ordered logs before recording a verified outcome', async () => {
127
+ const { data, auth, storage, repo, logs } = await setup();
128
+ const taskHash = 'a'.repeat(64);
129
+ const outputHash = await storage.objects.write(repo, encodeBeast2For(IntegerType)(42n));
130
+ const executionId = uuidv7();
131
+ const lease = {
132
+ leaseId: makeLeaseId(taskHash, inputsHash([])), repo: 'opaque-alias', workspace: 'dev', taskHash, inputsHash: inputsHash([]),
133
+ tier: 'node', status: 'claimed', createdAtMs: Date.now(), attempt: { executionId, startedAtMs: Date.now() },
134
+ event: { version: 2, workspace: 'dev', taskName: 'task', closure: { hashes: [] }, runnerEvent: {
135
+ mode: 'task', repo: 'opaque-alias', taskHash, launchId: 'launch', inputHashes: [], timeoutMs: 1000,
136
+ } },
137
+ };
138
+ await socketRequest(data.socketPath, 'POST', '/v1/attempts/running', {
139
+ ...auth, requestType: LeaseRunningType, body: { leaseJson: JSON.stringify(lease), owner: await processOwner() }, responseType: BooleanType,
140
+ });
141
+ assert.equal((await storage.refs.executionGet(repo, taskHash, lease.inputsHash, executionId))?.type, 'running');
142
+ await socketRequest(data.socketPath, 'POST', '/v1/logs', { ...auth, requestType: SessionLogsType, responseType: BooleanType, body: {
143
+ taskHash, inputsHash: lease.inputsHash, executionId, chunks: [{ stream: variant('stdout', null), data: 'one\n' }, { stream: variant('stdout', null), data: 'two\n' }],
144
+ } });
145
+ assert.deepEqual(logs, ['one\n', 'two\n']);
146
+ assert.equal(await socketRequest(data.socketPath, 'POST', '/v1/verify', {
147
+ ...auth, requestType: VerifyOutputType, body: { hash: 'b'.repeat(64) }, responseType: BooleanType,
148
+ }), false);
149
+ await socketRequest(data.socketPath, 'POST', '/v1/attempts/outcome', { ...auth, requestType: LeaseOutcomeType, responseType: BooleanType,
150
+ body: { leaseJson: JSON.stringify(lease), resultJson: JSON.stringify({ taskName: 'task', status: 'success', outputHash }) },
151
+ });
152
+ assert.equal((await storage.refs.executionGet(repo, taskHash, lease.inputsHash, executionId))?.type, 'success');
153
+ assert.equal((await storage.logs.read(repo, taskHash, lease.inputsHash, executionId, 'stdout')).data, 'one\ntwo\n');
154
+ });
155
+ it('keeps a successor upload when a predecessor PUT fails after takeover', async () => {
156
+ const { data, auth, options, storage, repo } = await setup();
157
+ const bytes = Buffer.alloc(1024, 42);
158
+ const id = randomUUID();
159
+ const info = { leaseId: 'lease', hash: digest(bytes), size: BigInt(bytes.length), receipt: randomUUID() };
160
+ const stream = new PassThrough();
161
+ cleanup.push(async () => { stream.destroy(); });
162
+ const predecessor = socketStream(data.socketPath, 'PUT', uploadUrl(id, info), { ...auth, rawBody: stream });
163
+ stream.write(bytes.subarray(0, 512));
164
+ // Wait until the first listener has opened its real staging file. Keep
165
+ // that PUT in progress while another session takes the same upload over.
166
+ const staging = join(repo, 'tmp', 'transfers');
167
+ const deadline = Date.now() + 5000;
168
+ for (;;) {
169
+ const files = await readdir(staging).catch((error) => { if (error.code === 'ENOENT')
170
+ return []; throw error; });
171
+ if (files.some((name) => name.startsWith('rack-')))
172
+ break;
173
+ assert(Date.now() < deadline, 'predecessor never opened its staging file');
174
+ await delay(10);
175
+ }
176
+ const successor = await startSessionDataServer(options);
177
+ cleanup.push(() => successor.close());
178
+ const response = await socketStream(successor.socketPath, 'PUT', uploadUrl(id, info), { ...auth, rawBody: bytes });
179
+ response.resume();
180
+ assert.equal(response.statusCode, 200);
181
+ // Its checksum failure must clean only its own file, not the successor's.
182
+ stream.end(Buffer.alloc(512, 43));
183
+ const failed = await predecessor;
184
+ failed.resume();
185
+ assert.equal(failed.statusCode, 400);
186
+ await data.close();
187
+ assert.equal(await socketRequest(successor.socketPath, 'POST', `/v1/uploads/${id}/commit`, {
188
+ ...auth, requestType: SessionUploadType, body: info, responseType: BooleanType,
189
+ }), true);
190
+ assert.deepEqual(new Uint8Array(await storage.objects.read(repo, info.hash)), new Uint8Array(bytes));
191
+ });
192
+ //# sourceMappingURL=session-data-server.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-data-server.spec.js","sourceRoot":"","sources":["../../../src/client/session-data-server.spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ import { type ValueTypeOf } from '@elaraai/east';
6
+ import { type StorageBackend } from '@elaraai/e3-core';
7
+ import { HubAttemptType, type CapacitySnapshot, type CreateLease } from '../protocol/control.js';
8
+ import type { RackLeaseResult } from '../protocol/task-envelope.js';
9
+ import type { HubClient } from './hub-client.js';
10
+ /** Names the claim-time attempt; absent until an agent claims the lease. */
11
+ export type LeaseAttempt = ValueTypeOf<typeof HubAttemptType>;
12
+ /** Reports one lease transition after its repository effects have completed. */
13
+ export type LeaseUpdate = {
14
+ kind: 'claimed';
15
+ rackId: string;
16
+ rackLabel: string;
17
+ attempt: LeaseAttempt;
18
+ } | {
19
+ kind: 'completed';
20
+ result: RackLeaseResult;
21
+ attempt: LeaseAttempt | null;
22
+ at: Date;
23
+ } | {
24
+ kind: 'lost';
25
+ reason: string;
26
+ } | {
27
+ kind: 'cancelled';
28
+ };
29
+ /** A task/unit lease requested through the current control protocol. */
30
+ export type LeaseRequest = CreateLease;
31
+ /** One subscriber's ordered view of shared work. */
32
+ export interface LeaseHandle {
33
+ /** Queue identity shared by all subscribers of identical work. */
34
+ readonly leaseId: string;
35
+ /** Whether the hub attached this request to work already queued. */
36
+ readonly attached: boolean;
37
+ /** Latest claim-time identity, including a fresh identity after reclaim. */
38
+ readonly attempt: LeaseAttempt | null;
39
+ /** Resolves with the next transition; terminal updates remain repeatable. */
40
+ next(): Promise<LeaseUpdate>;
41
+ /** Leaves this subscription; other subscribers continue to own shared work. */
42
+ cancel(): Promise<void>;
43
+ }
44
+ /** Configures one run's attachment and repository data endpoint. */
45
+ export interface RackSessionOptions {
46
+ /** Backend belonging to the run's e3 release. */
47
+ storage: StorageBackend;
48
+ /** Repository path; canonicalized before registration. */
49
+ repoPath: string;
50
+ /** Workspace shown in rack status. */
51
+ workspace: string;
52
+ /** Description shown in rack status, usually the command and PID. */
53
+ label: string;
54
+ /** The run's exact e3 release, defaulting to the package release. */
55
+ e3Version?: string;
56
+ }
57
+ /**
58
+ * Keeps a run attached to the hub and serves its repository through a private
59
+ * socket. A lost hub ends every pending wait; this session never reconnects
60
+ * to a replacement whose in-memory leases cannot be the old ones.
61
+ * @example
62
+ * const session = await RackSession.open(client, { storage, repoPath, workspace: 'dev', label: 'run dev' });
63
+ * try { const lease = await session.createLease(request); await lease.next(); }
64
+ * finally { await session.close(); }
65
+ */
66
+ export declare class RackSession {
67
+ private readonly client;
68
+ readonly sessionId: string;
69
+ readonly repoAlias: string;
70
+ private readonly data;
71
+ private lost;
72
+ private closing?;
73
+ private readonly abort;
74
+ private readonly handles;
75
+ private readonly early;
76
+ private creating;
77
+ private snapshot;
78
+ private readonly tee;
79
+ private loop?;
80
+ private constructor();
81
+ /**
82
+ * Starts a private data server, registers the live process and starts polling.
83
+ * @param client - A connected, compatible hub
84
+ * @param options - Repository backend, workspace, label and exact release
85
+ * @returns The live session
86
+ * @throws {Error} On registration failure, after removing the data socket
87
+ */
88
+ static open(client: HubClient, options: RackSessionOptions): Promise<RackSession>;
89
+ /** Whether the original hub has been lost; later creates refuse immediately. */
90
+ get hubLost(): boolean;
91
+ /** Returns the latest capacity snapshot without a network round trip. */
92
+ capacity(): CapacitySnapshot;
93
+ /** Refreshes fleet capacity without disturbing the ordered event cursor. */
94
+ refreshCapacity(): Promise<CapacitySnapshot>;
95
+ /**
96
+ * Submits work or attaches to an existing lease, handling events that race
97
+ * the create response. Attempt identity is assigned at claim, never locally.
98
+ * @param request - Current task/unit event and rack shape
99
+ * @returns An ordered subscription to the lease
100
+ */
101
+ createLease(request: LeaseRequest): Promise<LeaseHandle>;
102
+ /**
103
+ * Attaches to identical work already queued or held, which costs the racks
104
+ * no further capacity, and queues nothing otherwise: how a run spills work
105
+ * locally when the racks are full, without first offering it to them.
106
+ * @param request - Current task/unit event and rack shape
107
+ * @returns An ordered subscription, or null when no such work is queued
108
+ */
109
+ attachLease(request: LeaseRequest): Promise<LeaseHandle | null>;
110
+ private subscribe;
111
+ private unsubscribe;
112
+ /** Registers a live log tee; persistence remains the data server's job. */
113
+ onLog(executionId: string, sink: (stream: 'stdout' | 'stderr', data: string) => void): () => void;
114
+ private update;
115
+ private poll;
116
+ /** Unregisters first, stops polling and removes the private data endpoint once. */
117
+ close(): Promise<void>;
118
+ }
119
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/client/session.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAErE,OAAO,EAAyB,cAAc,EAAE,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAqB,MAAM,wBAAwB,CAAC;AAC3I,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,4EAA4E;AAC5E,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,OAAO,cAAc,CAAC,CAAC;AAC9D,gFAAgF;AAChF,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GAC7E;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;IAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GACtF;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAC1B,wEAAwE;AACxE,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC;AACvC,oDAAoD;AACpD,MAAM,WAAW,WAAW;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;IACtC,6EAA6E;IAC7E,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7B,+EAA+E;IAC/E,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AA+BD,oEAAoE;AACpE,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,OAAO,EAAE,cAAc,CAAC;IACxB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAOD;;;;;;;;GAQG;AACH,qBAAa,WAAW;IAWF,OAAO,CAAC,QAAQ,CAAC,MAAM;IAAa,QAAQ,CAAC,SAAS,EAAE,MAAM;IAAE,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC5G,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXvB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,OAAO,CAAC,CAAgB;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAC1D,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAmF;IACnG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA+E;IACnG,OAAO,CAAC,IAAI,CAAC,CAAgB;IAE7B,OAAO;IAGP;;;;;;OAMG;WACU,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,CAAC;IAsBvF,gFAAgF;IAChF,IAAI,OAAO,IAAI,OAAO,CAAsB;IAC5C,yEAAyE;IACzE,QAAQ,IAAI,gBAAgB;IAE5B,4EAA4E;IACtE,eAAe,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAOlD;;;;;OAKG;IACG,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAI9D;;;;;;OAMG;IACH,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;YAOjD,SAAS;YAkBT,WAAW;IAoBzB,2EAA2E;IAC3E,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAKjG,OAAO,CAAC,MAAM;YAUA,IAAI;IAgClB,mFAAmF;IACnF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAYvB"}
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ import { randomBytes } from 'node:crypto';
6
+ import { realpath } from 'node:fs/promises';
7
+ import { setTimeout as delay } from 'node:timers/promises';
8
+ import { IntegerType, compareFor } from '@elaraai/east';
9
+ import { processOwner } from '@elaraai/e3-core';
10
+ import { E3_RACK_VERSION, HUB_PROTOCOL } from '../version.js';
11
+ import { startSessionDataServer } from './session-data-server.js';
12
+ import { RackHubUnavailableError } from './connect.js';
13
+ class Handle {
14
+ leaseId;
15
+ attached;
16
+ unsubscribe;
17
+ attempt;
18
+ queue = [];
19
+ waiters = [];
20
+ terminal;
21
+ lastClaim;
22
+ cancelling;
23
+ constructor(leaseId, attached, attempt, unsubscribe) {
24
+ this.leaseId = leaseId;
25
+ this.attached = attached;
26
+ this.unsubscribe = unsubscribe;
27
+ this.attempt = attempt;
28
+ }
29
+ push(update) {
30
+ if (this.terminal !== undefined)
31
+ return;
32
+ if (update.kind === 'claimed') {
33
+ if (this.lastClaim === update.attempt.executionId)
34
+ return;
35
+ this.lastClaim = update.attempt.executionId;
36
+ this.attempt = update.attempt;
37
+ }
38
+ else
39
+ this.terminal = update;
40
+ const waiter = this.waiters.shift();
41
+ if (waiter === undefined)
42
+ this.queue.push(update);
43
+ else
44
+ waiter(update);
45
+ if (this.terminal !== undefined)
46
+ for (const waiting of this.waiters.splice(0))
47
+ waiting(this.terminal);
48
+ }
49
+ next() {
50
+ const queued = this.queue.shift();
51
+ if (queued !== undefined)
52
+ return Promise.resolve(queued);
53
+ if (this.terminal !== undefined)
54
+ return Promise.resolve(this.terminal);
55
+ return new Promise((resolve) => { this.waiters.push(resolve); });
56
+ }
57
+ cancel() { return this.cancelling ??= this.unsubscribe(this); }
58
+ }
59
+ // One listener for every open session of this process, however many runs an
60
+ // API server keeps attached: the sessions it closes as the process ends.
61
+ const openSessions = new Set();
62
+ const closeOpenSessions = () => { for (const session of openSessions)
63
+ void session.close(); };
64
+ /**
65
+ * Keeps a run attached to the hub and serves its repository through a private
66
+ * socket. A lost hub ends every pending wait; this session never reconnects
67
+ * to a replacement whose in-memory leases cannot be the old ones.
68
+ * @example
69
+ * const session = await RackSession.open(client, { storage, repoPath, workspace: 'dev', label: 'run dev' });
70
+ * try { const lease = await session.createLease(request); await lease.next(); }
71
+ * finally { await session.close(); }
72
+ */
73
+ export class RackSession {
74
+ client;
75
+ sessionId;
76
+ repoAlias;
77
+ data;
78
+ lost = false;
79
+ closing;
80
+ abort = new AbortController();
81
+ handles = new Map();
82
+ early = new Map();
83
+ creating = 0;
84
+ snapshot = { racks: [], queued: 0n, mine: { pending: 0n, claimed: 0n } };
85
+ tee = new Map();
86
+ loop;
87
+ constructor(client, sessionId, repoAlias, data) {
88
+ this.client = client;
89
+ this.sessionId = sessionId;
90
+ this.repoAlias = repoAlias;
91
+ this.data = data;
92
+ }
93
+ /**
94
+ * Starts a private data server, registers the live process and starts polling.
95
+ * @param client - A connected, compatible hub
96
+ * @param options - Repository backend, workspace, label and exact release
97
+ * @returns The live session
98
+ * @throws {Error} On registration failure, after removing the data socket
99
+ */
100
+ static async open(client, options) {
101
+ const dataToken = randomBytes(32).toString('hex');
102
+ let session;
103
+ const data = await startSessionDataServer({ storage: options.storage, repoPath: await realpath(options.repoPath), home: client.home, dataToken,
104
+ onLog: (id, stream, text) => { for (const sink of session?.tee.get(id) ?? [])
105
+ sink(stream, text); } });
106
+ try {
107
+ const opened = await client.openSession({ protocol: BigInt(HUB_PROTOCOL), e3Version: options.e3Version ?? E3_RACK_VERSION,
108
+ owner: await processOwner(), repoPath: await realpath(options.repoPath), workspace: options.workspace, label: options.label,
109
+ dataSocket: data.socketPath, dataToken });
110
+ session = new RackSession(client, opened.sessionId, opened.repoAlias, data);
111
+ const initial = await client.events(opened.sessionId, 0n, 0);
112
+ session.snapshot = initial.capacity;
113
+ session.loop = session.poll();
114
+ if (openSessions.size === 0)
115
+ process.on('beforeExit', closeOpenSessions);
116
+ openSessions.add(session);
117
+ return session;
118
+ }
119
+ catch (err) {
120
+ if (session !== undefined)
121
+ await client.closeSession(session.sessionId).catch(() => { });
122
+ await data.close();
123
+ throw err;
124
+ }
125
+ }
126
+ /** Whether the original hub has been lost; later creates refuse immediately. */
127
+ get hubLost() { return this.lost; }
128
+ /** Returns the latest capacity snapshot without a network round trip. */
129
+ capacity() { return this.snapshot; }
130
+ /** Refreshes fleet capacity without disturbing the ordered event cursor. */
131
+ async refreshCapacity() {
132
+ if (this.lost || this.closing !== undefined)
133
+ return this.snapshot;
134
+ const status = await this.client.status();
135
+ this.snapshot = { ...status.capacity, mine: this.snapshot.mine };
136
+ return this.snapshot;
137
+ }
138
+ /**
139
+ * Submits work or attaches to an existing lease, handling events that race
140
+ * the create response. Attempt identity is assigned at claim, never locally.
141
+ * @param request - Current task/unit event and rack shape
142
+ * @returns An ordered subscription to the lease
143
+ */
144
+ async createLease(request) {
145
+ return (await this.subscribe((sessionId) => this.client.createLease(sessionId, request)));
146
+ }
147
+ /**
148
+ * Attaches to identical work already queued or held, which costs the racks
149
+ * no further capacity, and queues nothing otherwise: how a run spills work
150
+ * locally when the racks are full, without first offering it to them.
151
+ * @param request - Current task/unit event and rack shape
152
+ * @returns An ordered subscription, or null when no such work is queued
153
+ */
154
+ attachLease(request) {
155
+ return this.subscribe(async (sessionId) => {
156
+ const attached = await this.client.attachLease(sessionId, request);
157
+ return attached.type === 'some' ? attached.value : null;
158
+ });
159
+ }
160
+ async subscribe(ask) {
161
+ if (this.lost || this.closing !== undefined)
162
+ throw new RackHubUnavailableError('Rack session is closed or its hub is unreachable');
163
+ this.creating++;
164
+ try {
165
+ const created = await ask(this.sessionId);
166
+ if (created === null)
167
+ return null;
168
+ const handle = new Handle(created.leaseId, created.attached, created.attempt.type === 'some' ? created.attempt.value : null, (handle) => this.unsubscribe(handle));
169
+ const handles = this.handles.get(created.leaseId) ?? new Set();
170
+ handles.add(handle);
171
+ this.handles.set(created.leaseId, handles);
172
+ for (const update of this.early.get(created.leaseId) ?? [])
173
+ handle.push(update);
174
+ if (this.lost || this.closing !== undefined)
175
+ handle.push({ kind: 'lost', reason: 'rack hub unreachable' });
176
+ return handle;
177
+ }
178
+ finally {
179
+ this.creating--;
180
+ if (this.creating === 0)
181
+ this.early.clear();
182
+ }
183
+ }
184
+ async unsubscribe(handle) {
185
+ const handles = this.handles.get(handle.leaseId);
186
+ if (handles === undefined || !handles.has(handle))
187
+ return;
188
+ if (handles.size > 1) {
189
+ handles.delete(handle);
190
+ handle.push({ kind: 'cancelled' });
191
+ return;
192
+ }
193
+ try {
194
+ if (!this.lost && this.closing === undefined) {
195
+ const response = await this.client.cancelLease(this.sessionId, handle.leaseId);
196
+ // Completion won the hub's fence. Its event remains authoritative and
197
+ // may still be in the events response racing this cancellation reply.
198
+ if (response.status === 'completed')
199
+ return;
200
+ }
201
+ handle.push(this.lost ? { kind: 'lost', reason: 'rack hub unreachable' } : { kind: 'cancelled' });
202
+ handles.delete(handle);
203
+ if (handles.size === 0)
204
+ this.handles.delete(handle.leaseId);
205
+ }
206
+ finally {
207
+ // A completed handle can still receive the in-flight completion event.
208
+ if (this.lost || this.closing !== undefined)
209
+ this.handles.delete(handle.leaseId);
210
+ }
211
+ }
212
+ /** Registers a live log tee; persistence remains the data server's job. */
213
+ onLog(executionId, sink) {
214
+ const sinks = this.tee.get(executionId) ?? new Set();
215
+ sinks.add(sink);
216
+ this.tee.set(executionId, sinks);
217
+ return () => { sinks.delete(sink); if (sinks.size === 0)
218
+ this.tee.delete(executionId); };
219
+ }
220
+ update(event) {
221
+ switch (event.type) {
222
+ case 'claimed': return { kind: 'claimed', rackId: event.value.rackId, rackLabel: event.value.rackLabel, attempt: event.value.attempt };
223
+ case 'completed': return { kind: 'completed', result: JSON.parse(event.value.resultJson),
224
+ attempt: event.value.attempt.type === 'some' ? event.value.attempt.value : null, at: event.value.at };
225
+ case 'lost': return { kind: 'lost', reason: event.value.reason };
226
+ case 'cancelled': return { kind: 'cancelled' };
227
+ }
228
+ }
229
+ async poll() {
230
+ let after = 0n;
231
+ let failures = 0;
232
+ while (!this.abort.signal.aborted && !this.lost) {
233
+ try {
234
+ const response = await this.client.events(this.sessionId, after, 20000, this.abort.signal);
235
+ failures = 0;
236
+ this.snapshot = response.capacity;
237
+ for (const event of response.events) {
238
+ if (compareFor(IntegerType)(event.seq, after) <= 0)
239
+ continue;
240
+ after = event.seq;
241
+ const update = this.update(event.event);
242
+ const id = event.event.value.leaseId;
243
+ const handles = this.handles.get(id);
244
+ if (handles !== undefined)
245
+ for (const handle of handles)
246
+ handle.push(update);
247
+ if (update.kind !== 'claimed')
248
+ this.handles.delete(id);
249
+ if (this.creating > 0) {
250
+ const early = this.early.get(id) ?? [];
251
+ early.push(update);
252
+ this.early.set(id, early);
253
+ }
254
+ }
255
+ }
256
+ catch {
257
+ if (this.abort.signal.aborted)
258
+ break;
259
+ if (++failures >= 2) {
260
+ this.lost = true;
261
+ this.snapshot = { racks: [], queued: 0n, mine: { pending: 0n, claimed: 0n } };
262
+ for (const handles of this.handles.values())
263
+ for (const handle of handles)
264
+ handle.push({ kind: 'lost', reason: 'rack hub unreachable' });
265
+ break;
266
+ }
267
+ await delay(100, undefined, { signal: this.abort.signal }).catch(() => { });
268
+ }
269
+ }
270
+ }
271
+ /** Unregisters first, stops polling and removes the private data endpoint once. */
272
+ close() {
273
+ return this.closing ??= (async () => {
274
+ openSessions.delete(this);
275
+ if (openSessions.size === 0)
276
+ process.off('beforeExit', closeOpenSessions);
277
+ await this.client.closeSession(this.sessionId).catch(() => { });
278
+ this.abort.abort();
279
+ await this.loop;
280
+ for (const handles of this.handles.values())
281
+ for (const handle of handles)
282
+ handle.push({ kind: 'cancelled' });
283
+ this.handles.clear();
284
+ this.early.clear();
285
+ this.tee.clear();
286
+ await this.data.close();
287
+ })();
288
+ }
289
+ }
290
+ //# sourceMappingURL=session.js.map