@elaraai/e3-rack 0.0.0-stage → 1.0.95

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 (307) hide show
  1. package/LICENSE.md +54 -0
  2. package/README.md +409 -2
  3. package/dist/src/client/client.spec.d.ts +6 -0
  4. package/dist/src/client/client.spec.d.ts.map +1 -0
  5. package/dist/src/client/client.spec.js +188 -0
  6. package/dist/src/client/client.spec.js.map +1 -0
  7. package/dist/src/client/connect.d.ts +54 -0
  8. package/dist/src/client/connect.d.ts.map +1 -0
  9. package/dist/src/client/connect.js +115 -0
  10. package/dist/src/client/connect.js.map +1 -0
  11. package/dist/src/client/hub-client.d.ts +57 -0
  12. package/dist/src/client/hub-client.d.ts.map +1 -0
  13. package/dist/src/client/hub-client.js +87 -0
  14. package/dist/src/client/hub-client.js.map +1 -0
  15. package/dist/src/client/local-http.d.ts +78 -0
  16. package/dist/src/client/local-http.d.ts.map +1 -0
  17. package/dist/src/client/local-http.js +149 -0
  18. package/dist/src/client/local-http.js.map +1 -0
  19. package/dist/src/client/session-attempts.d.ts +20 -0
  20. package/dist/src/client/session-attempts.d.ts.map +1 -0
  21. package/dist/src/client/session-attempts.js +88 -0
  22. package/dist/src/client/session-attempts.js.map +1 -0
  23. package/dist/src/client/session-data-server.d.ts +37 -0
  24. package/dist/src/client/session-data-server.d.ts.map +1 -0
  25. package/dist/src/client/session-data-server.js +280 -0
  26. package/dist/src/client/session-data-server.js.map +1 -0
  27. package/dist/src/client/session-data-server.spec.d.ts +6 -0
  28. package/dist/src/client/session-data-server.spec.d.ts.map +1 -0
  29. package/dist/src/client/session-data-server.spec.js +192 -0
  30. package/dist/src/client/session-data-server.spec.js.map +1 -0
  31. package/dist/src/client/session.d.ts +119 -0
  32. package/dist/src/client/session.d.ts.map +1 -0
  33. package/dist/src/client/session.js +290 -0
  34. package/dist/src/client/session.js.map +1 -0
  35. package/dist/src/hub/config.d.ts +74 -0
  36. package/dist/src/hub/config.d.ts.map +1 -0
  37. package/dist/src/hub/config.js +159 -0
  38. package/dist/src/hub/config.js.map +1 -0
  39. package/dist/src/hub/fair-lease-store.d.ts +36 -0
  40. package/dist/src/hub/fair-lease-store.d.ts.map +1 -0
  41. package/dist/src/hub/fair-lease-store.js +73 -0
  42. package/dist/src/hub/fair-lease-store.js.map +1 -0
  43. package/dist/src/hub/hub.d.ts +91 -0
  44. package/dist/src/hub/hub.d.ts.map +1 -0
  45. package/dist/src/hub/hub.js +641 -0
  46. package/dist/src/hub/hub.js.map +1 -0
  47. package/dist/src/hub/hub.spec.d.ts +6 -0
  48. package/dist/src/hub/hub.spec.d.ts.map +1 -0
  49. package/dist/src/hub/hub.spec.js +363 -0
  50. package/dist/src/hub/hub.spec.js.map +1 -0
  51. package/dist/src/hub/infrastructure.spec.d.ts +6 -0
  52. package/dist/src/hub/infrastructure.spec.d.ts.map +1 -0
  53. package/dist/src/hub/infrastructure.spec.js +211 -0
  54. package/dist/src/hub/infrastructure.spec.js.map +1 -0
  55. package/dist/src/hub/lock.d.ts +62 -0
  56. package/dist/src/hub/lock.d.ts.map +1 -0
  57. package/dist/src/hub/lock.js +165 -0
  58. package/dist/src/hub/lock.js.map +1 -0
  59. package/dist/src/hub/main.d.ts +16 -0
  60. package/dist/src/hub/main.d.ts.map +1 -0
  61. package/dist/src/hub/main.js +56 -0
  62. package/dist/src/hub/main.js.map +1 -0
  63. package/dist/src/hub/session-bridge.d.ts +116 -0
  64. package/dist/src/hub/session-bridge.d.ts.map +1 -0
  65. package/dist/src/hub/session-bridge.js +336 -0
  66. package/dist/src/hub/session-bridge.js.map +1 -0
  67. package/dist/src/identity/file-identity-store.d.ts +18 -0
  68. package/dist/src/identity/file-identity-store.d.ts.map +1 -0
  69. package/dist/src/identity/file-identity-store.js +34 -0
  70. package/dist/src/identity/file-identity-store.js.map +1 -0
  71. package/dist/src/identity/file-identity-store.spec.d.ts +6 -0
  72. package/dist/src/identity/file-identity-store.spec.d.ts.map +1 -0
  73. package/dist/src/identity/file-identity-store.spec.js +59 -0
  74. package/dist/src/identity/file-identity-store.spec.js.map +1 -0
  75. package/dist/src/identity/in-memory-identity-store.d.ts +48 -0
  76. package/dist/src/identity/in-memory-identity-store.d.ts.map +1 -0
  77. package/dist/src/identity/in-memory-identity-store.js +102 -0
  78. package/dist/src/identity/in-memory-identity-store.js.map +1 -0
  79. package/dist/src/identity/machine-identity.d.ts +134 -0
  80. package/dist/src/identity/machine-identity.d.ts.map +1 -0
  81. package/dist/src/identity/machine-identity.js +65 -0
  82. package/dist/src/identity/machine-identity.js.map +1 -0
  83. package/dist/src/index.d.ts +34 -0
  84. package/dist/src/index.d.ts.map +1 -0
  85. package/dist/src/index.js +34 -0
  86. package/dist/src/index.js.map +1 -0
  87. package/dist/src/lease/closure.d.ts +96 -0
  88. package/dist/src/lease/closure.d.ts.map +1 -0
  89. package/dist/src/lease/closure.js +192 -0
  90. package/dist/src/lease/closure.js.map +1 -0
  91. package/dist/src/lease/closure.spec.d.ts +6 -0
  92. package/dist/src/lease/closure.spec.d.ts.map +1 -0
  93. package/dist/src/lease/closure.spec.js +35 -0
  94. package/dist/src/lease/closure.spec.js.map +1 -0
  95. package/dist/src/lease/coordinator.d.ts +34 -0
  96. package/dist/src/lease/coordinator.d.ts.map +1 -0
  97. package/dist/src/lease/coordinator.js +42 -0
  98. package/dist/src/lease/coordinator.js.map +1 -0
  99. package/dist/src/lease/in-memory-lease-store.d.ts +58 -0
  100. package/dist/src/lease/in-memory-lease-store.d.ts.map +1 -0
  101. package/dist/src/lease/in-memory-lease-store.js +132 -0
  102. package/dist/src/lease/in-memory-lease-store.js.map +1 -0
  103. package/dist/src/lease/lease-store.d.ts +169 -0
  104. package/dist/src/lease/lease-store.d.ts.map +1 -0
  105. package/dist/src/lease/lease-store.js +94 -0
  106. package/dist/src/lease/lease-store.js.map +1 -0
  107. package/dist/src/lease/lease-store.spec.d.ts +6 -0
  108. package/dist/src/lease/lease-store.spec.d.ts.map +1 -0
  109. package/dist/src/lease/lease-store.spec.js +8 -0
  110. package/dist/src/lease/lease-store.spec.js.map +1 -0
  111. package/dist/src/lease/rack-dispatch.d.ts +105 -0
  112. package/dist/src/lease/rack-dispatch.d.ts.map +1 -0
  113. package/dist/src/lease/rack-dispatch.js +6 -0
  114. package/dist/src/lease/rack-dispatch.js.map +1 -0
  115. package/dist/src/mutation-queue.d.ts +13 -0
  116. package/dist/src/mutation-queue.d.ts.map +1 -0
  117. package/dist/src/mutation-queue.js +17 -0
  118. package/dist/src/mutation-queue.js.map +1 -0
  119. package/dist/src/paths.d.ts +56 -0
  120. package/dist/src/paths.d.ts.map +1 -0
  121. package/dist/src/paths.js +82 -0
  122. package/dist/src/paths.js.map +1 -0
  123. package/dist/src/protocol/control.d.ts +295 -0
  124. package/dist/src/protocol/control.d.ts.map +1 -0
  125. package/dist/src/protocol/control.js +74 -0
  126. package/dist/src/protocol/control.js.map +1 -0
  127. package/dist/src/protocol/index.d.ts +9 -0
  128. package/dist/src/protocol/index.d.ts.map +1 -0
  129. package/dist/src/protocol/index.js +9 -0
  130. package/dist/src/protocol/index.js.map +1 -0
  131. package/dist/src/protocol/rack-wire.d.ts +357 -0
  132. package/dist/src/protocol/rack-wire.d.ts.map +1 -0
  133. package/dist/src/protocol/rack-wire.js +262 -0
  134. package/dist/src/protocol/rack-wire.js.map +1 -0
  135. package/dist/src/protocol/rack-wire.spec.d.ts +6 -0
  136. package/dist/src/protocol/rack-wire.spec.d.ts.map +1 -0
  137. package/dist/src/protocol/rack-wire.spec.js +14 -0
  138. package/dist/src/protocol/rack-wire.spec.js.map +1 -0
  139. package/dist/src/protocol/registration.d.ts +70 -0
  140. package/dist/src/protocol/registration.d.ts.map +1 -0
  141. package/dist/src/protocol/registration.js +67 -0
  142. package/dist/src/protocol/registration.js.map +1 -0
  143. package/dist/src/protocol/session-data.d.ts +69 -0
  144. package/dist/src/protocol/session-data.d.ts.map +1 -0
  145. package/dist/src/protocol/session-data.js +26 -0
  146. package/dist/src/protocol/session-data.js.map +1 -0
  147. package/dist/src/protocol/task-envelope.d.ts +198 -0
  148. package/dist/src/protocol/task-envelope.d.ts.map +1 -0
  149. package/dist/src/protocol/task-envelope.js +93 -0
  150. package/dist/src/protocol/task-envelope.js.map +1 -0
  151. package/dist/src/protocol/wire-golden.d.ts +8 -0
  152. package/dist/src/protocol/wire-golden.d.ts.map +1 -0
  153. package/dist/src/protocol/wire-golden.js +34 -0
  154. package/dist/src/protocol/wire-golden.js.map +1 -0
  155. package/dist/src/protocol/wire-samples.d.ts +11 -0
  156. package/dist/src/protocol/wire-samples.d.ts.map +1 -0
  157. package/dist/src/protocol/wire-samples.js +52 -0
  158. package/dist/src/protocol/wire-samples.js.map +1 -0
  159. package/dist/src/registry/file-rack-registry.d.ts +68 -0
  160. package/dist/src/registry/file-rack-registry.d.ts.map +1 -0
  161. package/dist/src/registry/file-rack-registry.js +138 -0
  162. package/dist/src/registry/file-rack-registry.js.map +1 -0
  163. package/dist/src/registry/file-rack-registry.spec.d.ts +6 -0
  164. package/dist/src/registry/file-rack-registry.spec.d.ts.map +1 -0
  165. package/dist/src/registry/file-rack-registry.spec.js +50 -0
  166. package/dist/src/registry/file-rack-registry.spec.js.map +1 -0
  167. package/dist/src/registry/rack-registry.d.ts +64 -0
  168. package/dist/src/registry/rack-registry.d.ts.map +1 -0
  169. package/dist/src/registry/rack-registry.js +65 -0
  170. package/dist/src/registry/rack-registry.js.map +1 -0
  171. package/dist/src/registry/rack-registry.spec.d.ts +6 -0
  172. package/dist/src/registry/rack-registry.spec.d.ts.map +1 -0
  173. package/dist/src/registry/rack-registry.spec.js +8 -0
  174. package/dist/src/registry/rack-registry.spec.js.map +1 -0
  175. package/dist/src/routing/eligibility.d.ts +38 -0
  176. package/dist/src/routing/eligibility.d.ts.map +1 -0
  177. package/dist/src/routing/eligibility.js +78 -0
  178. package/dist/src/routing/eligibility.js.map +1 -0
  179. package/dist/src/routing/host-coupling.d.ts +26 -0
  180. package/dist/src/routing/host-coupling.d.ts.map +1 -0
  181. package/dist/src/routing/host-coupling.js +62 -0
  182. package/dist/src/routing/host-coupling.js.map +1 -0
  183. package/dist/src/routing/policy.d.ts +188 -0
  184. package/dist/src/routing/policy.d.ts.map +1 -0
  185. package/dist/src/routing/policy.js +212 -0
  186. package/dist/src/routing/policy.js.map +1 -0
  187. package/dist/src/routing/routing.spec.d.ts +6 -0
  188. package/dist/src/routing/routing.spec.d.ts.map +1 -0
  189. package/dist/src/routing/routing.spec.js +100 -0
  190. package/dist/src/routing/routing.spec.js.map +1 -0
  191. package/dist/src/routing/tiers.d.ts +22 -0
  192. package/dist/src/routing/tiers.d.ts.map +1 -0
  193. package/dist/src/routing/tiers.js +26 -0
  194. package/dist/src/routing/tiers.js.map +1 -0
  195. package/dist/src/runner/api-server.d.ts +45 -0
  196. package/dist/src/runner/api-server.d.ts.map +1 -0
  197. package/dist/src/runner/api-server.js +21 -0
  198. package/dist/src/runner/api-server.js.map +1 -0
  199. package/dist/src/runner/capacity.d.ts +13 -0
  200. package/dist/src/runner/capacity.d.ts.map +1 -0
  201. package/dist/src/runner/capacity.js +18 -0
  202. package/dist/src/runner/capacity.js.map +1 -0
  203. package/dist/src/runner/rack-body.d.ts +43 -0
  204. package/dist/src/runner/rack-body.d.ts.map +1 -0
  205. package/dist/src/runner/rack-body.js +208 -0
  206. package/dist/src/runner/rack-body.js.map +1 -0
  207. package/dist/src/runner/rack-runner.d.ts +62 -0
  208. package/dist/src/runner/rack-runner.d.ts.map +1 -0
  209. package/dist/src/runner/rack-runner.js +75 -0
  210. package/dist/src/runner/rack-runner.js.map +1 -0
  211. package/dist/src/runner/rack-runner.spec.d.ts +6 -0
  212. package/dist/src/runner/rack-runner.spec.d.ts.map +1 -0
  213. package/dist/src/runner/rack-runner.spec.js +302 -0
  214. package/dist/src/runner/rack-runner.spec.js.map +1 -0
  215. package/dist/src/runner/split.spec.d.ts +6 -0
  216. package/dist/src/runner/split.spec.d.ts.map +1 -0
  217. package/dist/src/runner/split.spec.js +188 -0
  218. package/dist/src/runner/split.spec.js.map +1 -0
  219. package/dist/src/runner-event.d.ts +261 -0
  220. package/dist/src/runner-event.d.ts.map +1 -0
  221. package/dist/src/runner-event.js +332 -0
  222. package/dist/src/runner-event.js.map +1 -0
  223. package/dist/src/server/beast2.d.ts +19 -0
  224. package/dist/src/server/beast2.d.ts.map +1 -0
  225. package/dist/src/server/beast2.js +18 -0
  226. package/dist/src/server/beast2.js.map +1 -0
  227. package/dist/src/server/body-limit.d.ts +16 -0
  228. package/dist/src/server/body-limit.d.ts.map +1 -0
  229. package/dist/src/server/body-limit.js +53 -0
  230. package/dist/src/server/body-limit.js.map +1 -0
  231. package/dist/src/server/body-limit.spec.d.ts +2 -0
  232. package/dist/src/server/body-limit.spec.d.ts.map +1 -0
  233. package/dist/src/server/body-limit.spec.js +32 -0
  234. package/dist/src/server/body-limit.spec.js.map +1 -0
  235. package/dist/src/server/bounded.d.ts +16 -0
  236. package/dist/src/server/bounded.d.ts.map +1 -0
  237. package/dist/src/server/bounded.js +39 -0
  238. package/dist/src/server/bounded.js.map +1 -0
  239. package/dist/src/server/dispatch.d.ts +137 -0
  240. package/dist/src/server/dispatch.d.ts.map +1 -0
  241. package/dist/src/server/dispatch.js +267 -0
  242. package/dist/src/server/dispatch.js.map +1 -0
  243. package/dist/src/server/env-publication.d.ts +45 -0
  244. package/dist/src/server/env-publication.d.ts.map +1 -0
  245. package/dist/src/server/env-publication.js +10 -0
  246. package/dist/src/server/env-publication.js.map +1 -0
  247. package/dist/src/server/index.d.ts +16 -0
  248. package/dist/src/server/index.d.ts.map +1 -0
  249. package/dist/src/server/index.js +17 -0
  250. package/dist/src/server/index.js.map +1 -0
  251. package/dist/src/server/protocol.spec.d.ts +6 -0
  252. package/dist/src/server/protocol.spec.d.ts.map +1 -0
  253. package/dist/src/server/protocol.spec.js +78 -0
  254. package/dist/src/server/protocol.spec.js.map +1 -0
  255. package/dist/src/server/rack-routes.d.ts +175 -0
  256. package/dist/src/server/rack-routes.d.ts.map +1 -0
  257. package/dist/src/server/rack-routes.js +755 -0
  258. package/dist/src/server/rack-routes.js.map +1 -0
  259. package/dist/src/server/rack-routes.spec.d.ts +6 -0
  260. package/dist/src/server/rack-routes.spec.d.ts.map +1 -0
  261. package/dist/src/server/rack-routes.spec.js +477 -0
  262. package/dist/src/server/rack-routes.spec.js.map +1 -0
  263. package/dist/src/server/storage-bridge.d.ts +140 -0
  264. package/dist/src/server/storage-bridge.d.ts.map +1 -0
  265. package/dist/src/server/storage-bridge.js +6 -0
  266. package/dist/src/server/storage-bridge.js.map +1 -0
  267. package/dist/src/state-file.d.ts +10 -0
  268. package/dist/src/state-file.d.ts.map +1 -0
  269. package/dist/src/state-file.js +38 -0
  270. package/dist/src/state-file.js.map +1 -0
  271. package/dist/src/testing/in-memory.d.ts +116 -0
  272. package/dist/src/testing/in-memory.d.ts.map +1 -0
  273. package/dist/src/testing/in-memory.js +193 -0
  274. package/dist/src/testing/in-memory.js.map +1 -0
  275. package/dist/src/testing/index.d.ts +14 -0
  276. package/dist/src/testing/index.d.ts.map +1 -0
  277. package/dist/src/testing/index.js +14 -0
  278. package/dist/src/testing/index.js.map +1 -0
  279. package/dist/src/testing/lease-store-contract.d.ts +14 -0
  280. package/dist/src/testing/lease-store-contract.d.ts.map +1 -0
  281. package/dist/src/testing/lease-store-contract.js +78 -0
  282. package/dist/src/testing/lease-store-contract.js.map +1 -0
  283. package/dist/src/testing/protocol-suite.d.ts +38 -0
  284. package/dist/src/testing/protocol-suite.d.ts.map +1 -0
  285. package/dist/src/testing/protocol-suite.js +207 -0
  286. package/dist/src/testing/protocol-suite.js.map +1 -0
  287. package/dist/src/testing/rack-registry-contract.d.ts +15 -0
  288. package/dist/src/testing/rack-registry-contract.d.ts.map +1 -0
  289. package/dist/src/testing/rack-registry-contract.js +106 -0
  290. package/dist/src/testing/rack-registry-contract.js.map +1 -0
  291. package/dist/src/testing/test-rack-agent.d.ts +121 -0
  292. package/dist/src/testing/test-rack-agent.d.ts.map +1 -0
  293. package/dist/src/testing/test-rack-agent.js +316 -0
  294. package/dist/src/testing/test-rack-agent.js.map +1 -0
  295. package/dist/src/tls/tls.d.ts +67 -0
  296. package/dist/src/tls/tls.d.ts.map +1 -0
  297. package/dist/src/tls/tls.js +200 -0
  298. package/dist/src/tls/tls.js.map +1 -0
  299. package/dist/src/tls/tls.spec.d.ts +6 -0
  300. package/dist/src/tls/tls.spec.d.ts.map +1 -0
  301. package/dist/src/tls/tls.spec.js +101 -0
  302. package/dist/src/tls/tls.spec.js.map +1 -0
  303. package/dist/src/version.d.ts +9 -0
  304. package/dist/src/version.d.ts.map +1 -0
  305. package/dist/src/version.js +10 -0
  306. package/dist/src/version.js.map +1 -0
  307. package/package.json +72 -3
@@ -0,0 +1,641 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ import { createHash, randomUUID } from 'node:crypto';
6
+ import { unlink } from 'node:fs/promises';
7
+ import { Server } from 'node:http';
8
+ import { Server as HttpsServer, createServer as createHttpsServer } from 'node:https';
9
+ import { hostname } from 'node:os';
10
+ import { isAbsolute } from 'node:path';
11
+ import { Hono } from 'hono';
12
+ import { createAdaptorServer } from '@hono/node-server';
13
+ import { getConnInfo } from '@hono/node-server/conninfo';
14
+ import { IntegerType, StringType, compareFor, equalFor, none, some, variant } from '@elaraai/east';
15
+ import { inputsHash, isObjectHash, isProcessAlive } from '@elaraai/e3-core';
16
+ import { decodeBody, sendSuccess } from '../server/beast2.js';
17
+ import { FileMachineIdentityStore } from '../identity/file-identity-store.js';
18
+ import { generateEnrollmentToken } from '../identity/machine-identity.js';
19
+ import { FileRackRegistry } from '../registry/file-rack-registry.js';
20
+ import { InMemoryRackLeaseStore } from '../lease/in-memory-lease-store.js';
21
+ import { StoreRackDispatch, bootPendingApproval, bundledE3Version, agentVersionAtLeast, DEFAULT_HEALTHY_WINDOW_MS } from '../server/dispatch.js';
22
+ import { createRackRoutes } from '../server/rack-routes.js';
23
+ import { RackRegistrationType, RackSecurityPolicyType } from '../protocol/registration.js';
24
+ import { isLeaseEventV2, leaseInputs, leaseTaskHash } from '../protocol/task-envelope.js';
25
+ import { decodeRunnerEvent } from '../runner-event.js';
26
+ import { ApproveRackType, AttachLeaseResultType, CancelLeaseResultType, CreateLeaseResultType, CreateLeaseType, EmptyType, HubConfigType, HubEnrollResultType, HubEnrollType, HubHelloType, HubStatusType, OpenSessionResultType, OpenSessionType, RemoveRackResultType, SessionPollResultType, SessionPollType } from '../protocol/control.js';
27
+ import { closeSocketServer, RackHubError, REQUEST_BODY_TIMEOUT_MS, serveOnSocket } from '../client/local-http.js';
28
+ import { rackHome, hubSocketPath } from '../paths.js';
29
+ import { E3_RACK_VERSION, HUB_PROTOCOL } from '../version.js';
30
+ import { certificateSha256 } from '../tls/tls.js';
31
+ import { MutationQueue } from '../state-file.js';
32
+ import { listenerProblem, loadHubCertificate, loadHubConfig, saveHubConfig, wildcardHost } from './config.js';
33
+ import { acquireHubLock } from './lock.js';
34
+ import { FairLeaseStore } from './fair-lease-store.js';
35
+ import { SessionLeaseCoordinator, SessionProxyStorageBridge } from './session-bridge.js';
36
+ const equalString = equalFor(StringType);
37
+ const compareInteger = compareFor(IntegerType);
38
+ const nonterminal = (lease) => lease.status === 'pending' || lease.status === 'claimed';
39
+ const normalizedAddress = (address) => address.replace(/^::ffff:/, '');
40
+ /**
41
+ * Hosts one user's rack queue, fleet and repository-owning sessions.
42
+ * The hub interprets the rack/control protocol only. A live session of the
43
+ * same repository and exact e3 release performs every repository operation.
44
+ * @example
45
+ * const hub = new RackHub({ foreground: true });
46
+ * if (await hub.start()) await hub.closed;
47
+ */
48
+ export class RackHub {
49
+ options;
50
+ /** Resolves after listeners, file stores and ownership have been closed. */
51
+ closed;
52
+ resolveClosed;
53
+ home;
54
+ now;
55
+ sessions = new Map();
56
+ subscriptions = new Map();
57
+ dedupe = new Map();
58
+ mutations = new MutationQueue();
59
+ coordinator = new SessionLeaseCoordinator();
60
+ backend;
61
+ leases;
62
+ abort = new AbortController();
63
+ lock;
64
+ identity;
65
+ registry;
66
+ config;
67
+ bridge;
68
+ dispatch;
69
+ control;
70
+ rack;
71
+ listener = variant('disabled', null);
72
+ retry;
73
+ sweeper;
74
+ stopping;
75
+ draining = false;
76
+ idleSince;
77
+ /** @param options - State home, idle policy, diagnostics and lifecycle clocks */
78
+ constructor(options = {}) {
79
+ this.options = options;
80
+ this.home = options.home ?? rackHome();
81
+ this.now = options.now ?? Date.now;
82
+ this.idleSince = this.now();
83
+ this.backend = new InMemoryRackLeaseStore(this.now);
84
+ this.leases = new FairLeaseStore(this.backend, (lease) => this.subscriptions.get(lease.leaseId)?.owner ?? '');
85
+ this.closed = new Promise((resolve) => { this.resolveClosed = resolve; });
86
+ }
87
+ /**
88
+ * Acquires ownership, loads state, and starts both listeners and sweepers.
89
+ * @returns False when another live hub owns this home
90
+ * @throws {Error} When private state or the control socket cannot be opened
91
+ */
92
+ async start() {
93
+ if (this.lock !== undefined || this.stopping !== undefined)
94
+ throw new Error('Rack hub has already started or stopped');
95
+ const lock = await acquireHubLock(this.home);
96
+ if (lock === null) {
97
+ this.options.log?.('rack hub already running');
98
+ this.resolveClosed();
99
+ return false;
100
+ }
101
+ this.lock = lock;
102
+ try {
103
+ this.config = await loadHubConfig(this.home);
104
+ this.identity = new FileMachineIdentityStore(this.home);
105
+ this.registry = new FileRackRegistry(this.home, this.now);
106
+ await Promise.all([this.identity.getRackToken('startup'), this.registry.listRacks()]);
107
+ this.bridge = new SessionProxyStorageBridge({ leases: this.leases, coordinator: this.coordinator,
108
+ session: (repo, id) => this.endpoint(repo, id), owner: lock.record, now: this.now,
109
+ subscribers: (repo, id) => {
110
+ const sub = this.subscriptions.get(id);
111
+ if (sub?.repo !== repo)
112
+ return [];
113
+ return [...sub.subscribers].flatMap((id) => {
114
+ const session = this.sessions.get(id);
115
+ return session !== undefined && !session.dead ? [session.registration] : [];
116
+ });
117
+ },
118
+ advertiseOrigin: () => this.config.advertiseUrl.type === 'some' ? this.config.advertiseUrl.value : undefined });
119
+ this.dispatch = new StoreRackDispatch(this.leases, this.registry, { coordinator: this.coordinator, now: this.now,
120
+ visibilityMs: this.options.visibilityMs, endings: { stopped: this.bridge.attempts.stopped, writes: this.bridge } });
121
+ // The rack listener binds before the control socket answers: a client
122
+ // that connects to a hub starting in the background, and enrolls at
123
+ // once, finds the listener up or down with its reason, never still
124
+ // binding. A bind that fails leaves the listener down and the control
125
+ // socket up, so e3 rack config can still change it.
126
+ await this.bindRack();
127
+ const socket = hubSocketPath(this.home);
128
+ if (process.platform !== 'win32')
129
+ await unlink(socket).catch((err) => { if (err.code !== 'ENOENT')
130
+ throw err; });
131
+ this.control = await serveOnSocket(this.controlApp(), socket);
132
+ this.sweeper = setInterval(() => { void this.sweep().catch((err) => this.options.log?.(`rack sweep: ${messageOf(err)}`)); }, this.options.sweepMs ?? 5000);
133
+ this.sweeper.unref();
134
+ this.options.log?.(`rack hub started (pid ${process.pid}, e3 ${E3_RACK_VERSION})`);
135
+ return true;
136
+ }
137
+ catch (err) {
138
+ await this.stop();
139
+ throw err;
140
+ }
141
+ }
142
+ hello() {
143
+ return { version: E3_RACK_VERSION, protocol: BigInt(HUB_PROTOCOL), pid: BigInt(process.pid), draining: this.draining };
144
+ }
145
+ endpoint(repo, id) {
146
+ const sub = this.subscriptions.get(id);
147
+ if (sub === undefined || sub.repo !== repo)
148
+ throw new RackHubError('no_session', 'No live session for repository');
149
+ const usable = (s) => s !== undefined && !s.dead && s.alias === repo && equalString(s.registration.e3Version, sub.version);
150
+ const owner = this.sessions.get(sub.owner);
151
+ const session = usable(owner) ? owner : [...this.sessions.values()].find(usable);
152
+ if (session === undefined)
153
+ throw new RackHubError('no_session', 'No live session for repository and e3 version');
154
+ return session.registration;
155
+ }
156
+ session(id) {
157
+ const session = this.sessions.get(id);
158
+ if (session === undefined || session.dead)
159
+ throw new RackHubError('unknown_session', 'Rack session no longer exists');
160
+ return session;
161
+ }
162
+ emit(session, event) {
163
+ session.events.push({ seq: ++session.seq, event });
164
+ // A non-polling client is disconnected by the sweeper. Bound its replay
165
+ // queue too; a cursor gap is explicit, never a silently dropped outcome.
166
+ if (session.events.length > 32768)
167
+ session.events.splice(0, session.events.length - 32768);
168
+ for (const wake of session.waiters)
169
+ wake();
170
+ }
171
+ publish(sub, event) {
172
+ for (const id of sub.subscribers) {
173
+ const session = this.sessions.get(id);
174
+ if (session !== undefined)
175
+ this.emit(session, event);
176
+ }
177
+ }
178
+ async claimedEvent(lease) {
179
+ if (lease.attempt === undefined || lease.claimedBy === undefined)
180
+ throw new Error('Claim has no attempt');
181
+ const rack = await this.registry.getRack(lease.claimedBy);
182
+ return variant('claimed', { leaseId: lease.leaseId, rackId: lease.claimedBy, rackLabel: rack?.label ?? lease.claimedBy,
183
+ attempt: { executionId: lease.attempt.executionId, startedAt: new Date(lease.attempt.startedAtMs) } });
184
+ }
185
+ async finish(sub) {
186
+ sub.endedAt ??= this.now();
187
+ if (this.dedupe.get(sub.key) === sub.id)
188
+ this.dedupe.delete(sub.key);
189
+ await this.bridge.discardWrites(sub.repo, sub.id, []).catch((error) => this.options.log?.(`rack cleanup: ${messageOf(error)}`));
190
+ }
191
+ async signal(signal) {
192
+ const sub = this.subscriptions.get(signal.leaseId);
193
+ if (sub === undefined)
194
+ return;
195
+ const lease = await this.leases.get(signal.repo, signal.leaseId);
196
+ if (lease === null)
197
+ return;
198
+ if (signal.kind === 'claimed') {
199
+ sub.claimants.add(lease.claimedBy);
200
+ // Reclaim invalidates every prior-attempt data URL before its new grant.
201
+ await this.bridge.discardWrites(lease.repo, lease.leaseId, lease.targets ?? []);
202
+ const event = await this.claimedEvent(lease);
203
+ if (event.type === 'claimed') {
204
+ // A line of the attempt's log, best effort: the claim stands, and its
205
+ // grant is answered, whether or not the session stored the line.
206
+ try {
207
+ await this.bridge.attempts.logs.append(lease.repo, lease.taskHash, lease.inputsHash, lease.attempt.executionId, 'stdout', `e3: running on rack ${event.value.rackLabel} (lease ${lease.leaseId})\n`);
208
+ await this.bridge.attempts.logs.flush?.(lease.repo, lease.taskHash, lease.inputsHash, lease.attempt.executionId);
209
+ }
210
+ catch (error) {
211
+ this.options.log?.(`rack lease ${lease.leaseId}: its first log line was not stored: ${messageOf(error)}`);
212
+ }
213
+ }
214
+ this.publish(sub, event);
215
+ }
216
+ else if (signal.kind === 'completed' && signal.disposition === 'recorded') {
217
+ await this.finish(sub);
218
+ this.publish(sub, variant('completed', { leaseId: lease.leaseId,
219
+ attempt: lease.attempt === undefined ? none : some({ executionId: lease.attempt.executionId, startedAt: new Date(lease.attempt.startedAtMs) }),
220
+ resultJson: JSON.stringify(lease.result), at: new Date(lease.completedAtMs) }));
221
+ }
222
+ }
223
+ /** Attaches the session to identical work already queued or held, or queues the work; attach-only queues nothing. */
224
+ async submit(session, request, attachOnly) {
225
+ if (this.draining)
226
+ throw new RackHubError('draining', 'Rack hub is draining');
227
+ const value = JSON.parse(request.eventJson);
228
+ if (!isLeaseEventV2(value))
229
+ throw new Error('Expected a v2 rack lease');
230
+ const runner = decodeRunnerEvent(value.runnerEvent);
231
+ if (runner.mode !== 'task' && runner.mode !== 'unit')
232
+ throw new Error('Local rack delegation supports tasks and units only');
233
+ if (value.environment !== undefined)
234
+ throw new Error('Local rack environments are not supported');
235
+ if (!['node', 'py', 'py-datascience', 'c', 'full'].includes(request.tier) || !['serverless', 'small', 'medium', 'large', 'xlarge'].includes(request.computeSize))
236
+ throw new Error('Invalid rack tier or compute size');
237
+ const closure = value.closure;
238
+ if ('hashes' in closure) {
239
+ if (!Array.isArray(closure.hashes) || closure.hashes.length > 512 || !closure.hashes.every((hash) => typeof hash === 'string' && isObjectHash(hash)))
240
+ throw new Error('Invalid inline closure');
241
+ }
242
+ else if (!isObjectHash(closure.object) || !Number.isSafeInteger(closure.count) || closure.count < 0)
243
+ throw new Error('Invalid closure object');
244
+ const event = { ...value, runnerEvent: { ...runner, repo: session.alias },
245
+ workspace: session.registration.workspace, e3Version: session.registration.e3Version };
246
+ const key = JSON.stringify([session.alias, leaseTaskHash(event), inputsHash(leaseInputs(event)), session.registration.e3Version]);
247
+ const existingId = this.dedupe.get(key);
248
+ const existing = existingId === undefined ? null : await this.leases.get(session.alias, existingId);
249
+ if (existing !== null && nonterminal(existing)) {
250
+ const sub = this.subscriptions.get(existing.leaseId);
251
+ sub.subscribers.add(session.id);
252
+ if (existing.status === 'claimed')
253
+ this.emit(session, await this.claimedEvent(existing));
254
+ return { leaseId: existing.leaseId, attached: true,
255
+ attempt: existing.attempt === undefined ? none : some({ executionId: existing.attempt.executionId, startedAt: new Date(existing.attempt.startedAtMs) }) };
256
+ }
257
+ if (attachOnly)
258
+ return null;
259
+ if (this.subscriptions.size >= 16384)
260
+ throw new RackHubError('capacity', 'Rack hub queue is full');
261
+ const lease = await this.dispatch.createLease(event, request.tier, request.computeSize);
262
+ this.subscriptions.set(lease.leaseId, { repo: session.alias, id: lease.leaseId, key, version: session.registration.e3Version, owner: session.id, subscribers: new Set([session.id]), claimants: new Set() });
263
+ this.dedupe.set(key, lease.leaseId);
264
+ return { leaseId: lease.leaseId, attached: false, attempt: none };
265
+ }
266
+ async unsubscribe(session, id) {
267
+ const sub = this.subscriptions.get(id);
268
+ if (sub === undefined || sub.repo !== session.alias || !sub.subscribers.has(session.id))
269
+ return 'unknown';
270
+ if (sub.subscribers.size === 1 && sub.endedAt === undefined) {
271
+ const lease = await this.dispatch.cancel(sub.repo, id);
272
+ if (lease?.status === 'cancelled') {
273
+ await this.finish(sub);
274
+ this.publish(sub, variant('cancelled', { leaseId: id }));
275
+ }
276
+ }
277
+ sub.subscribers.delete(session.id);
278
+ if (sub.owner === session.id && sub.subscribers.size > 0) {
279
+ await this.coordinator.run(sub.repo, id, () => {
280
+ this.bridge.interrupt(sub.repo, id);
281
+ sub.owner = sub.subscribers.values().next().value;
282
+ return Promise.resolve();
283
+ });
284
+ }
285
+ return (await this.leases.get(sub.repo, id))?.status ?? 'unknown';
286
+ }
287
+ async endSession(session) {
288
+ for (const sub of this.subscriptions.values())
289
+ if (sub.subscribers.has(session.id))
290
+ await this.unsubscribe(session, sub.id);
291
+ this.sessions.delete(session.id);
292
+ this.idleSince = this.now();
293
+ for (const wake of session.waiters)
294
+ wake();
295
+ }
296
+ async capacity(sessionId) {
297
+ const active = [];
298
+ let pending = 0;
299
+ let claimed = 0;
300
+ for (const sub of this.subscriptions.values()) {
301
+ if (sub.endedAt !== undefined)
302
+ continue;
303
+ // Read in place: a copy of every live lease on every poll reply would
304
+ // cost the hub its time.
305
+ const lease = this.backend.peek(sub.repo, sub.id);
306
+ if (lease === null || !nonterminal(lease))
307
+ continue;
308
+ active.push(lease);
309
+ if (sessionId !== undefined && sub.subscribers.has(sessionId)) {
310
+ if (lease.status === 'pending')
311
+ pending++;
312
+ else
313
+ claimed++;
314
+ }
315
+ }
316
+ const healthy = new Set((await this.dispatch.listHealthyRacks()).map((rack) => rack.rackId));
317
+ const approval = (await this.registry.getRackSecurityPolicy())?.requireStartApproval ?? false;
318
+ return { racks: (await this.registry.listRacks()).map((rack) => {
319
+ const bundled = bundledE3Version(rack.agentVersion);
320
+ return { rackId: rack.rackId, label: rack.label, tiers: rack.tiers, capacity: rack.capacity,
321
+ busy: BigInt(active.filter((lease) => lease.claimedBy === rack.rackId && lease.status === 'claimed').length),
322
+ healthy: healthy.has(rack.rackId), bundledE3: bundled === null ? none : some(bundled), pendingApproval: approval && bootPendingApproval(rack) };
323
+ }), queued: BigInt(active.filter((lease) => lease.status === 'pending').length), mine: { pending: BigInt(pending), claimed: BigInt(claimed) } };
324
+ }
325
+ controlApp() {
326
+ const app = new Hono();
327
+ app.onError((err, c) => c.text(messageOf(err), 500));
328
+ app.post('/v1/hello', () => sendSuccess(HubHelloType, this.hello()));
329
+ app.post('/v1/sessions', async (c) => {
330
+ const registration = await decodeBody(c, OpenSessionType);
331
+ return this.mutations.run(async () => {
332
+ if (this.draining)
333
+ return c.text('Rack hub is draining', 409);
334
+ if (compareInteger(registration.protocol, BigInt(HUB_PROTOCOL)) !== 0)
335
+ return c.text('Rack session protocol must match the hub exactly', 409);
336
+ if (!isAbsolute(registration.repoPath) || !registration.dataSocket || registration.dataToken.length < 16 || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(registration.e3Version))
337
+ throw new Error('Invalid session registration');
338
+ if (this.sessions.size >= 1024)
339
+ throw new Error('Rack hub session limit reached');
340
+ const owner = registration.owner;
341
+ if (!await isProcessAlive(Number(owner.pid), Number(owner.pidStartTime), owner.bootId))
342
+ throw new Error('Rack session owner is not alive');
343
+ const id = randomUUID();
344
+ const alias = `r-${createHash('sha256').update(`${hostname()}\0${registration.repoPath}`).digest('hex').slice(0, 16)}`;
345
+ this.sessions.set(id, { id, alias, registration, lastSeen: this.now(), dead: false, seq: 0n, events: [], waiters: new Set() });
346
+ this.idleSince = this.now();
347
+ return sendSuccess(OpenSessionResultType, { sessionId: id, repoAlias: alias });
348
+ });
349
+ });
350
+ app.delete('/v1/sessions/:id', (c) => this.mutations.run(async () => {
351
+ const session = this.sessions.get(c.req.param('id'));
352
+ if (session !== undefined)
353
+ await this.endSession(session);
354
+ return sendSuccess(EmptyType, {});
355
+ }));
356
+ app.post('/v1/sessions/:id/leases', async (c) => {
357
+ const request = await decodeBody(c, CreateLeaseType);
358
+ return this.mutations.run(async () => sendSuccess(CreateLeaseResultType, (await this.submit(this.session(c.req.param('id')), request, false))));
359
+ });
360
+ // Attaches to identical work already queued or held, which costs the racks
361
+ // no further capacity, and queues nothing otherwise: how a full fleet is
362
+ // spilled without first offering it the work.
363
+ app.post('/v1/sessions/:id/leases/attach', async (c) => {
364
+ const request = await decodeBody(c, CreateLeaseType);
365
+ return this.mutations.run(async () => {
366
+ const attached = await this.submit(this.session(c.req.param('id')), request, true);
367
+ return sendSuccess(AttachLeaseResultType, attached === null ? none : some(attached));
368
+ });
369
+ });
370
+ app.post('/v1/sessions/:id/leases/:leaseId/cancel', (c) => this.mutations.run(async () => sendSuccess(CancelLeaseResultType, {
371
+ status: await this.unsubscribe(this.session(c.req.param('id')), c.req.param('leaseId')),
372
+ })));
373
+ app.post('/v1/sessions/:id/events', async (c) => {
374
+ const session = this.session(c.req.param('id'));
375
+ const request = await decodeBody(c, SessionPollType);
376
+ if (compareInteger(request.after, 0n) < 0 || compareInteger(request.after, session.seq) > 0)
377
+ throw new Error('Invalid rack event cursor');
378
+ const oldest = session.events[0]?.seq;
379
+ if (oldest !== undefined && compareInteger(request.after, oldest - 1n) < 0)
380
+ throw new Error('Rack session event replay expired');
381
+ session.lastSeen = this.now();
382
+ session.events = session.events.filter((event) => compareInteger(event.seq, request.after) > 0);
383
+ const wait = Math.min(20_000, Math.max(0, Number(request.waitMs)));
384
+ if (session.events.length === 0 && wait > 0)
385
+ await new Promise((resolve) => {
386
+ const done = () => { clearTimeout(timer); session.waiters.delete(done); c.req.raw.signal.removeEventListener('abort', done); resolve(); };
387
+ const timer = setTimeout(done, wait);
388
+ timer.unref();
389
+ session.waiters.add(done);
390
+ c.req.raw.signal.addEventListener('abort', done, { once: true });
391
+ if (c.req.raw.signal.aborted)
392
+ done();
393
+ });
394
+ this.session(session.id);
395
+ session.lastSeen = this.now();
396
+ return sendSuccess(SessionPollResultType, { events: [...session.events], capacity: await this.capacity(session.id) });
397
+ });
398
+ app.get('/v1/status', async () => sendSuccess(HubStatusType, { hello: this.hello(), rackListener: this.listener,
399
+ racks: await this.registry.listRacks(), sessions: await Promise.all([...this.sessions.values()].map(async (session) => ({ sessionId: session.id, repoAlias: session.alias,
400
+ workspace: session.registration.workspace, label: session.registration.label, e3Version: session.registration.e3Version,
401
+ ...(await this.capacity(session.id)).mine }))), capacity: await this.capacity() }));
402
+ app.get('/v1/config', () => sendSuccess(HubConfigType, this.config));
403
+ app.post('/v1/config', async (c) => {
404
+ const config = await decodeBody(c, HubConfigType);
405
+ return this.mutations.run(async () => {
406
+ await saveHubConfig(config, this.home);
407
+ const changed = !equalFor(HubConfigType)({ ...this.config, allow: config.allow, advertiseUrl: config.advertiseUrl, idleExitMinutes: config.idleExitMinutes }, config);
408
+ this.config = config;
409
+ // A listener that is not up binds again on any change: an --allow it
410
+ // lacked, or a port that has since come free.
411
+ if (changed || this.listener.type !== 'up') {
412
+ await this.closeRack();
413
+ await this.bindRack();
414
+ }
415
+ return sendSuccess(HubConfigType, this.config);
416
+ });
417
+ });
418
+ // Whether a rack's boots after its first need approval before it is leased
419
+ // work: e3 rack config --require-approval, and e3 rack approve.
420
+ app.get('/v1/security', async () => sendSuccess(RackSecurityPolicyType, await this.registry.getRackSecurityPolicy() ?? { requireStartApproval: false }));
421
+ app.post('/v1/security', async (c) => {
422
+ const policy = await decodeBody(c, RackSecurityPolicyType);
423
+ return this.mutations.run(async () => {
424
+ await this.registry.putRackSecurityPolicy(policy);
425
+ return sendSuccess(RackSecurityPolicyType, policy);
426
+ });
427
+ });
428
+ app.post('/v1/enroll', async (c) => {
429
+ const request = await decodeBody(c, HubEnrollType);
430
+ if (this.listener.type !== 'up' || this.config.listen.type === 'none')
431
+ throw new Error('Configure a reachable rack listener before enrollment');
432
+ // A wildcard bind's origin names no address a rack could reach.
433
+ if (this.config.advertiseUrl.type === 'none' && wildcardHost(this.config.listen.value.host)) {
434
+ throw new Error('The rack listener binds every address; set --advertise-url to the origin the rack reaches');
435
+ }
436
+ const ttl = request.ttlMinutes.type === 'some' ? Number(request.ttlMinutes.value) : 15;
437
+ if (!Number.isSafeInteger(ttl) || ttl < 1 || ttl > 1440)
438
+ throw new Error('Enrollment TTL must be 1–1440 minutes');
439
+ const { token, tokenHash } = generateEnrollmentToken();
440
+ const expiresAt = new Date(this.now() + ttl * 60000);
441
+ await this.identity.putEnrollmentToken(tokenHash, expiresAt);
442
+ return sendSuccess(HubEnrollResultType, { enrollmentToken: token, apiUrl: this.config.advertiseUrl.type === 'some' ? this.config.advertiseUrl.value : this.listener.value.origin,
443
+ expiresAt, e3Version: E3_RACK_VERSION, certificateSha256: this.listener.value.certificateSha256 });
444
+ });
445
+ app.delete('/v1/racks/:id', (c) => this.mutations.run(async () => {
446
+ const id = c.req.param('id');
447
+ const removed = await this.coordinator.run('@rack', id, () => this.registry.deleteRack(id));
448
+ await this.identity.deleteRackTokens(id);
449
+ for (const sub of this.subscriptions.values()) {
450
+ const lease = this.backend.peek(sub.repo, sub.id);
451
+ if (lease?.status === 'claimed' && lease.claimedBy === id)
452
+ await this.lose(sub, 'rack deregistered');
453
+ }
454
+ return sendSuccess(RemoveRackResultType, { removed });
455
+ }));
456
+ app.post('/v1/racks/:id/approve', async (c) => {
457
+ const request = await decodeBody(c, ApproveRackType);
458
+ return this.coordinator.run('@rack', c.req.param('id'), async () => {
459
+ const rack = await this.registry.getRack(c.req.param('id'));
460
+ if (rack === null)
461
+ return c.notFound();
462
+ if (rack.lastBootId.type !== 'some' || !equalString(rack.lastBootId.value, request.bootId))
463
+ return c.text('Rack rebooted; refresh before approving', 409);
464
+ const approved = { ...rack, approvedBootId: some(request.bootId) };
465
+ await this.registry.putRack(approved);
466
+ return sendSuccess(RackRegistrationType, approved);
467
+ });
468
+ });
469
+ app.post('/v1/drain', () => { this.draining = true; return sendSuccess(EmptyType, {}); });
470
+ app.post('/v1/stop', () => {
471
+ this.draining = true;
472
+ setImmediate(() => { void this.stop().catch((err) => this.options.log?.(messageOf(err))); });
473
+ return sendSuccess(EmptyType, {});
474
+ });
475
+ return app;
476
+ }
477
+ async bindRack() {
478
+ if (this.stopping !== undefined || this.config.listen.type === 'none') {
479
+ this.listener = variant('disabled', null);
480
+ return;
481
+ }
482
+ const listen = this.config.listen.value;
483
+ // A saved listener that may no longer be served stays down, with its
484
+ // reason in the status, until e3 rack config changes it.
485
+ const refused = listenerProblem(this.config);
486
+ if (refused !== undefined) {
487
+ this.listener = variant('down', { reason: refused });
488
+ this.options.log?.(`rack listener refused: ${refused}`);
489
+ return;
490
+ }
491
+ const app = new Hono();
492
+ app.use('/api/rack/*', async (c, next) => {
493
+ const address = normalizedAddress(getConnInfo(c).remote.address ?? '');
494
+ if (this.config.allow.length > 0 && !this.config.allow.some((allowed) => equalString(normalizedAddress(allowed), address)))
495
+ return c.text('Rack address is not allowed', 403);
496
+ await this.bridge.origin.run(new URL(c.req.url).origin, next);
497
+ });
498
+ app.route('/', this.bridge.dataRoutes());
499
+ app.route('/', createRackRoutes(this.identity, this.registry, this.leases, this.bridge, this.bridge.attempts, {
500
+ coordinator: this.coordinator, now: this.now, signal: this.abort.signal, visibilityMs: this.options.visibilityMs,
501
+ onLeaseSignal: (signal) => this.signal(signal),
502
+ canClaim: (rack, lease) => {
503
+ const sub = this.subscriptions.get(lease.leaseId);
504
+ if (sub === undefined || sub.endedAt !== undefined || sub.subscribers.size === 0)
505
+ return false;
506
+ // v2 completion does not carry an attempt id. A previous claimant
507
+ // must never reclaim this lease: its delayed reply is ambiguous.
508
+ if (sub.claimants.has(rack.rackId))
509
+ return false;
510
+ if (!rack.healthy || this.now() - rack.lastSeenAt.getTime() > DEFAULT_HEALTHY_WINDOW_MS)
511
+ return false;
512
+ const bundled = bundledE3Version(rack.agentVersion);
513
+ return bundled !== null && agentVersionAtLeast(bundled, sub.version);
514
+ },
515
+ }));
516
+ let server;
517
+ try {
518
+ let pin;
519
+ let created;
520
+ if (this.config.tls) {
521
+ const tls = await loadHubCertificate(this.home);
522
+ pin = certificateSha256(tls.certificatePem);
523
+ created = createAdaptorServer({ fetch: app.fetch, createServer: createHttpsServer,
524
+ serverOptions: { cert: tls.certificatePem, key: tls.privateKeyPem, requestTimeout: REQUEST_BODY_TIMEOUT_MS } });
525
+ }
526
+ else
527
+ created = createAdaptorServer({ fetch: app.fetch, serverOptions: { requestTimeout: REQUEST_BODY_TIMEOUT_MS } });
528
+ if (!(created instanceof Server) && !(created instanceof HttpsServer))
529
+ throw new Error('Rack listener requires HTTP/1');
530
+ const listening = server = created;
531
+ await new Promise((resolve, reject) => {
532
+ listening.once('error', reject);
533
+ listening.listen(Number(listen.port), listen.host, () => { listening.off('error', reject); resolve(); });
534
+ });
535
+ this.rack = listening;
536
+ const address = listening.address();
537
+ if (address === null || typeof address === 'string')
538
+ throw new Error('Rack listener has no network address');
539
+ const host = listen.host.includes(':') ? `[${listen.host}]` : listen.host;
540
+ this.listener = variant('up', { origin: `${pin === undefined ? 'http' : 'https'}://${host}:${address.port}`, certificateSha256: pin === undefined ? none : some(pin) });
541
+ }
542
+ catch (err) {
543
+ server?.close();
544
+ this.listener = variant('down', { reason: messageOf(err) });
545
+ this.options.log?.(`rack listener unavailable: ${messageOf(err)}`);
546
+ this.retry = setTimeout(() => { this.retry = undefined; void this.mutations.run(() => this.bindRack()).catch((error) => this.options.log?.(messageOf(error))); }, this.options.listenerRetryMs ?? 10000);
547
+ this.retry.unref();
548
+ }
549
+ }
550
+ async closeRack() {
551
+ if (this.retry !== undefined) {
552
+ clearTimeout(this.retry);
553
+ this.retry = undefined;
554
+ }
555
+ if (this.rack !== undefined) {
556
+ const server = this.rack;
557
+ this.rack = undefined;
558
+ await closeSocketServer(server);
559
+ }
560
+ }
561
+ async lose(sub, reason) {
562
+ const lease = await this.dispatch.supersede(sub.repo, sub.id);
563
+ if (lease?.status === 'completed')
564
+ return;
565
+ await this.finish(sub);
566
+ this.publish(sub, variant('lost', { leaseId: sub.id, reason }));
567
+ }
568
+ /** Sweeps dead sessions, lost attempts and expired replay; deterministic in tests. */
569
+ async sweep() {
570
+ await this.mutations.run(async () => {
571
+ if (this.stopping !== undefined)
572
+ return;
573
+ for (const session of this.sessions.values()) {
574
+ const { owner } = session.registration;
575
+ const alive = await isProcessAlive(Number(owner.pid), Number(owner.pidStartTime), owner.bootId);
576
+ if (!alive || this.now() - session.lastSeen > 45000) {
577
+ session.dead = !alive;
578
+ await this.endSession(session);
579
+ }
580
+ }
581
+ for (const sub of this.subscriptions.values()) {
582
+ if (sub.endedAt !== undefined) {
583
+ if (this.now() - sub.endedAt > 15 * 60000) {
584
+ this.backend.forget(sub.repo, sub.id);
585
+ this.subscriptions.delete(sub.id);
586
+ }
587
+ }
588
+ else if (await this.dispatch.getLease(sub.repo, sub.id) === null)
589
+ await this.lose(sub, 'lease lost on the rack');
590
+ }
591
+ const active = [...this.subscriptions.values()].some((sub) => sub.endedAt === undefined);
592
+ if (this.sessions.size > 0 || active)
593
+ this.idleSince = this.now();
594
+ else {
595
+ const idle = this.options.idleExitMinutes ?? (this.options.foreground ? 0 : Number(this.config.idleExitMinutes));
596
+ if (this.draining || (idle > 0 && this.now() - this.idleSince >= idle * 60000)) {
597
+ setImmediate(() => { void this.stop().catch((err) => this.options.log?.(messageOf(err))); });
598
+ }
599
+ }
600
+ });
601
+ }
602
+ /** Cancels outstanding work, flushes state and releases the hub lock once. */
603
+ stop() {
604
+ return this.stopping ??= (async () => {
605
+ this.draining = true;
606
+ this.abort.abort();
607
+ if (this.sweeper !== undefined)
608
+ clearInterval(this.sweeper);
609
+ const failures = [];
610
+ try {
611
+ await this.mutations.run(async () => {
612
+ for (const session of this.sessions.values()) {
613
+ try {
614
+ await this.endSession(session);
615
+ }
616
+ catch (error) {
617
+ failures.push(error);
618
+ }
619
+ }
620
+ });
621
+ this.bridge?.close();
622
+ const results = await Promise.allSettled([this.closeRack(), this.control === undefined ? Promise.resolve() : closeSocketServer(this.control, hubSocketPath(this.home)), this.registry?.flush()]);
623
+ for (const result of results)
624
+ if (result.status === 'rejected')
625
+ failures.push(result.reason);
626
+ }
627
+ finally {
628
+ try {
629
+ await this.lock?.release();
630
+ }
631
+ finally {
632
+ this.resolveClosed();
633
+ }
634
+ }
635
+ if (failures.length > 0)
636
+ throw new AggregateError(failures, 'Rack hub shutdown failed');
637
+ })();
638
+ }
639
+ }
640
+ function messageOf(err) { return err instanceof Error ? err.message : String(err); }
641
+ //# sourceMappingURL=hub.js.map