@shipfox/api-runners 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (465) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +187 -0
  5. package/LICENSE +21 -0
  6. package/dist/config.d.ts +30 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +208 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/core/demand.d.ts +33 -0
  11. package/dist/core/demand.d.ts.map +1 -0
  12. package/dist/core/demand.js +169 -0
  13. package/dist/core/demand.js.map +1 -0
  14. package/dist/core/entities/ephemeral-registration-token.d.ts +14 -0
  15. package/dist/core/entities/ephemeral-registration-token.d.ts.map +1 -0
  16. package/dist/core/entities/ephemeral-registration-token.js +3 -0
  17. package/dist/core/entities/ephemeral-registration-token.js.map +1 -0
  18. package/dist/core/entities/manual-registration-token.d.ts +12 -0
  19. package/dist/core/entities/manual-registration-token.d.ts.map +1 -0
  20. package/dist/core/entities/manual-registration-token.js +3 -0
  21. package/dist/core/entities/manual-registration-token.js.map +1 -0
  22. package/dist/core/entities/provisioned-runner.d.ts +24 -0
  23. package/dist/core/entities/provisioned-runner.d.ts.map +1 -0
  24. package/dist/core/entities/provisioned-runner.js +3 -0
  25. package/dist/core/entities/provisioned-runner.js.map +1 -0
  26. package/dist/core/entities/provisioner-token.d.ts +18 -0
  27. package/dist/core/entities/provisioner-token.d.ts.map +1 -0
  28. package/dist/core/entities/provisioner-token.js +3 -0
  29. package/dist/core/entities/provisioner-token.js.map +1 -0
  30. package/dist/core/entities/reservation.d.ts +10 -0
  31. package/dist/core/entities/reservation.d.ts.map +1 -0
  32. package/dist/core/entities/reservation.js +3 -0
  33. package/dist/core/entities/reservation.js.map +1 -0
  34. package/dist/core/entities/runner-session.d.ts +18 -0
  35. package/dist/core/entities/runner-session.d.ts.map +1 -0
  36. package/dist/core/entities/runner-session.js +3 -0
  37. package/dist/core/entities/runner-session.js.map +1 -0
  38. package/dist/core/ephemeral-registration-tokens.d.ts +31 -0
  39. package/dist/core/ephemeral-registration-tokens.d.ts.map +1 -0
  40. package/dist/core/ephemeral-registration-tokens.js +61 -0
  41. package/dist/core/ephemeral-registration-tokens.js.map +1 -0
  42. package/dist/core/errors.d.ts +62 -0
  43. package/dist/core/errors.d.ts.map +1 -0
  44. package/dist/core/errors.js +92 -0
  45. package/dist/core/errors.js.map +1 -0
  46. package/dist/core/index.d.ts +14 -0
  47. package/dist/core/index.d.ts.map +1 -0
  48. package/dist/core/index.js +10 -0
  49. package/dist/core/index.js.map +1 -0
  50. package/dist/core/job-executions.d.ts +14 -0
  51. package/dist/core/job-executions.d.ts.map +1 -0
  52. package/dist/core/job-executions.js +37 -0
  53. package/dist/core/job-executions.js.map +1 -0
  54. package/dist/core/maintenance-policy.d.ts +2 -0
  55. package/dist/core/maintenance-policy.d.ts.map +1 -0
  56. package/dist/core/maintenance-policy.js +3 -0
  57. package/dist/core/maintenance-policy.js.map +1 -0
  58. package/dist/core/maintenance.d.ts +33 -0
  59. package/dist/core/maintenance.d.ts.map +1 -0
  60. package/dist/core/maintenance.js +55 -0
  61. package/dist/core/maintenance.js.map +1 -0
  62. package/dist/core/manual-registration-tokens.d.ts +17 -0
  63. package/dist/core/manual-registration-tokens.d.ts.map +1 -0
  64. package/dist/core/manual-registration-tokens.js +28 -0
  65. package/dist/core/manual-registration-tokens.js.map +1 -0
  66. package/dist/core/provisioned-runners.d.ts +67 -0
  67. package/dist/core/provisioned-runners.d.ts.map +1 -0
  68. package/dist/core/provisioned-runners.js +162 -0
  69. package/dist/core/provisioned-runners.js.map +1 -0
  70. package/dist/core/provisioner-tokens.d.ts +20 -0
  71. package/dist/core/provisioner-tokens.d.ts.map +1 -0
  72. package/dist/core/provisioner-tokens.js +36 -0
  73. package/dist/core/provisioner-tokens.js.map +1 -0
  74. package/dist/core/rate-limit.d.ts +35 -0
  75. package/dist/core/rate-limit.d.ts.map +1 -0
  76. package/dist/core/rate-limit.js +74 -0
  77. package/dist/core/rate-limit.js.map +1 -0
  78. package/dist/core/runner-sessions.d.ts +26 -0
  79. package/dist/core/runner-sessions.d.ts.map +1 -0
  80. package/dist/core/runner-sessions.js +41 -0
  81. package/dist/core/runner-sessions.js.map +1 -0
  82. package/dist/core/runner-tool-capabilities.d.ts +24 -0
  83. package/dist/core/runner-tool-capabilities.d.ts.map +1 -0
  84. package/dist/core/runner-tool-capabilities.js +43 -0
  85. package/dist/core/runner-tool-capabilities.js.map +1 -0
  86. package/dist/db/db.d.ts +4478 -0
  87. package/dist/db/db.d.ts.map +1 -0
  88. package/dist/db/db.js +36 -0
  89. package/dist/db/db.js.map +1 -0
  90. package/dist/db/ephemeral-registration-tokens.d.ts +47 -0
  91. package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -0
  92. package/dist/db/ephemeral-registration-tokens.js +170 -0
  93. package/dist/db/ephemeral-registration-tokens.js.map +1 -0
  94. package/dist/db/index.d.ts +19 -0
  95. package/dist/db/index.d.ts.map +1 -0
  96. package/dist/db/index.js +15 -0
  97. package/dist/db/index.js.map +1 -0
  98. package/dist/db/job-executions.d.ts +128 -0
  99. package/dist/db/job-executions.d.ts.map +1 -0
  100. package/dist/db/job-executions.js +354 -0
  101. package/dist/db/job-executions.js.map +1 -0
  102. package/dist/db/manual-registration-tokens.d.ts +16 -0
  103. package/dist/db/manual-registration-tokens.d.ts.map +1 -0
  104. package/dist/db/manual-registration-tokens.js +37 -0
  105. package/dist/db/manual-registration-tokens.js.map +1 -0
  106. package/dist/db/provisioned-runners.d.ts +79 -0
  107. package/dist/db/provisioned-runners.d.ts.map +1 -0
  108. package/dist/db/provisioned-runners.js +466 -0
  109. package/dist/db/provisioned-runners.js.map +1 -0
  110. package/dist/db/provisioner-tokens.d.ts +26 -0
  111. package/dist/db/provisioner-tokens.d.ts.map +1 -0
  112. package/dist/db/provisioner-tokens.js +54 -0
  113. package/dist/db/provisioner-tokens.js.map +1 -0
  114. package/dist/db/rate-limits.d.ts +18 -0
  115. package/dist/db/rate-limits.d.ts.map +1 -0
  116. package/dist/db/rate-limits.js +44 -0
  117. package/dist/db/rate-limits.js.map +1 -0
  118. package/dist/db/reservations.d.ts +48 -0
  119. package/dist/db/reservations.d.ts.map +1 -0
  120. package/dist/db/reservations.js +162 -0
  121. package/dist/db/reservations.js.map +1 -0
  122. package/dist/db/runner-sessions.d.ts +18 -0
  123. package/dist/db/runner-sessions.d.ts.map +1 -0
  124. package/dist/db/runner-sessions.js +38 -0
  125. package/dist/db/runner-sessions.js.map +1 -0
  126. package/dist/db/schema/common.d.ts +2 -0
  127. package/dist/db/schema/common.d.ts.map +1 -0
  128. package/dist/db/schema/common.js +4 -0
  129. package/dist/db/schema/common.js.map +1 -0
  130. package/dist/db/schema/ephemeral-registration-tokens.d.ts +199 -0
  131. package/dist/db/schema/ephemeral-registration-tokens.d.ts.map +1 -0
  132. package/dist/db/schema/ephemeral-registration-tokens.js +45 -0
  133. package/dist/db/schema/ephemeral-registration-tokens.js.map +1 -0
  134. package/dist/db/schema/manual-registration-tokens.d.ts +165 -0
  135. package/dist/db/schema/manual-registration-tokens.d.ts.map +1 -0
  136. package/dist/db/schema/manual-registration-tokens.js +40 -0
  137. package/dist/db/schema/manual-registration-tokens.js.map +1 -0
  138. package/dist/db/schema/outbox.d.ts +195 -0
  139. package/dist/db/schema/outbox.d.ts.map +1 -0
  140. package/dist/db/schema/outbox.js +5 -0
  141. package/dist/db/schema/outbox.js.map +1 -0
  142. package/dist/db/schema/pending-job-executions.d.ts +189 -0
  143. package/dist/db/schema/pending-job-executions.d.ts.map +1 -0
  144. package/dist/db/schema/pending-job-executions.js +21 -0
  145. package/dist/db/schema/pending-job-executions.js.map +1 -0
  146. package/dist/db/schema/provisioned-runners.d.ts +379 -0
  147. package/dist/db/schema/provisioned-runners.d.ts.map +1 -0
  148. package/dist/db/schema/provisioned-runners.js +83 -0
  149. package/dist/db/schema/provisioned-runners.js.map +1 -0
  150. package/dist/db/schema/provisioner-tokens.d.ts +216 -0
  151. package/dist/db/schema/provisioner-tokens.d.ts.map +1 -0
  152. package/dist/db/schema/provisioner-tokens.js +48 -0
  153. package/dist/db/schema/provisioner-tokens.js.map +1 -0
  154. package/dist/db/schema/rate-limits.d.ts +145 -0
  155. package/dist/db/schema/rate-limits.d.ts.map +1 -0
  156. package/dist/db/schema/rate-limits.js +25 -0
  157. package/dist/db/schema/rate-limits.js.map +1 -0
  158. package/dist/db/schema/reservations.d.ts +157 -0
  159. package/dist/db/schema/reservations.d.ts.map +1 -0
  160. package/dist/db/schema/reservations.js +34 -0
  161. package/dist/db/schema/reservations.js.map +1 -0
  162. package/dist/db/schema/runner-sessions.d.ts +298 -0
  163. package/dist/db/schema/runner-sessions.d.ts.map +1 -0
  164. package/dist/db/schema/runner-sessions.js +61 -0
  165. package/dist/db/schema/runner-sessions.js.map +1 -0
  166. package/dist/db/schema/running-job-executions.d.ts +334 -0
  167. package/dist/db/schema/running-job-executions.d.ts.map +1 -0
  168. package/dist/db/schema/running-job-executions.js +41 -0
  169. package/dist/db/schema/running-job-executions.js.map +1 -0
  170. package/dist/index.d.ts +6 -0
  171. package/dist/index.d.ts.map +1 -0
  172. package/dist/index.js +56 -0
  173. package/dist/index.js.map +1 -0
  174. package/dist/metrics/index.d.ts +3 -0
  175. package/dist/metrics/index.d.ts.map +1 -0
  176. package/dist/metrics/index.js +4 -0
  177. package/dist/metrics/index.js.map +1 -0
  178. package/dist/metrics/instance.d.ts +34 -0
  179. package/dist/metrics/instance.d.ts.map +1 -0
  180. package/dist/metrics/instance.js +60 -0
  181. package/dist/metrics/instance.js.map +1 -0
  182. package/dist/metrics/service.d.ts +2 -0
  183. package/dist/metrics/service.d.ts.map +1 -0
  184. package/dist/metrics/service.js +21 -0
  185. package/dist/metrics/service.js.map +1 -0
  186. package/dist/presentation/auth/index.d.ts +3 -0
  187. package/dist/presentation/auth/index.d.ts.map +1 -0
  188. package/dist/presentation/auth/index.js +4 -0
  189. package/dist/presentation/auth/index.js.map +1 -0
  190. package/dist/presentation/auth/provisioner-token-auth.d.ts +3 -0
  191. package/dist/presentation/auth/provisioner-token-auth.d.ts.map +1 -0
  192. package/dist/presentation/auth/provisioner-token-auth.js +81 -0
  193. package/dist/presentation/auth/provisioner-token-auth.js.map +1 -0
  194. package/dist/presentation/auth/runner-registration-token-auth.d.ts +17 -0
  195. package/dist/presentation/auth/runner-registration-token-auth.d.ts.map +1 -0
  196. package/dist/presentation/auth/runner-registration-token-auth.js +77 -0
  197. package/dist/presentation/auth/runner-registration-token-auth.js.map +1 -0
  198. package/dist/presentation/dto/active-provisioner.d.ts +8 -0
  199. package/dist/presentation/dto/active-provisioner.d.ts.map +1 -0
  200. package/dist/presentation/dto/active-provisioner.js +10 -0
  201. package/dist/presentation/dto/active-provisioner.js.map +1 -0
  202. package/dist/presentation/dto/index.d.ts +7 -0
  203. package/dist/presentation/dto/index.d.ts.map +1 -0
  204. package/dist/presentation/dto/index.js +8 -0
  205. package/dist/presentation/dto/index.js.map +1 -0
  206. package/dist/presentation/dto/manual-registration-token.d.ts +12 -0
  207. package/dist/presentation/dto/manual-registration-token.d.ts.map +1 -0
  208. package/dist/presentation/dto/manual-registration-token.js +14 -0
  209. package/dist/presentation/dto/manual-registration-token.js.map +1 -0
  210. package/dist/presentation/dto/mint-registration-tokens.d.ts +5 -0
  211. package/dist/presentation/dto/mint-registration-tokens.d.ts.map +1 -0
  212. package/dist/presentation/dto/mint-registration-tokens.js +19 -0
  213. package/dist/presentation/dto/mint-registration-tokens.js.map +1 -0
  214. package/dist/presentation/dto/poll-demand.d.ts +4 -0
  215. package/dist/presentation/dto/poll-demand.d.ts.map +1 -0
  216. package/dist/presentation/dto/poll-demand.js +19 -0
  217. package/dist/presentation/dto/poll-demand.js.map +1 -0
  218. package/dist/presentation/dto/provisioned-runners.d.ts +38 -0
  219. package/dist/presentation/dto/provisioned-runners.d.ts.map +1 -0
  220. package/dist/presentation/dto/provisioned-runners.js +44 -0
  221. package/dist/presentation/dto/provisioned-runners.js.map +1 -0
  222. package/dist/presentation/dto/provisioner-token.d.ts +15 -0
  223. package/dist/presentation/dto/provisioner-token.d.ts.map +1 -0
  224. package/dist/presentation/dto/provisioner-token.js +17 -0
  225. package/dist/presentation/dto/provisioner-token.js.map +1 -0
  226. package/dist/presentation/index.d.ts +4 -0
  227. package/dist/presentation/index.d.ts.map +1 -0
  228. package/dist/presentation/index.js +5 -0
  229. package/dist/presentation/index.js.map +1 -0
  230. package/dist/presentation/routes/create-manual-registration-token.d.ts +2 -0
  231. package/dist/presentation/routes/create-manual-registration-token.d.ts.map +1 -0
  232. package/dist/presentation/routes/create-manual-registration-token.js +44 -0
  233. package/dist/presentation/routes/create-manual-registration-token.js.map +1 -0
  234. package/dist/presentation/routes/create-provisioner-token.d.ts +2 -0
  235. package/dist/presentation/routes/create-provisioner-token.d.ts.map +1 -0
  236. package/dist/presentation/routes/create-provisioner-token.js +42 -0
  237. package/dist/presentation/routes/create-provisioner-token.js.map +1 -0
  238. package/dist/presentation/routes/get-provisioner-me.d.ts +2 -0
  239. package/dist/presentation/routes/get-provisioner-me.d.ts.map +1 -0
  240. package/dist/presentation/routes/get-provisioner-me.js +22 -0
  241. package/dist/presentation/routes/get-provisioner-me.js.map +1 -0
  242. package/dist/presentation/routes/heartbeat.d.ts +2 -0
  243. package/dist/presentation/routes/heartbeat.d.ts.map +1 -0
  244. package/dist/presentation/routes/heartbeat.js +64 -0
  245. package/dist/presentation/routes/heartbeat.js.map +1 -0
  246. package/dist/presentation/routes/index.d.ts +4 -0
  247. package/dist/presentation/routes/index.d.ts.map +1 -0
  248. package/dist/presentation/routes/index.js +97 -0
  249. package/dist/presentation/routes/index.js.map +1 -0
  250. package/dist/presentation/routes/list-active-provisioners.d.ts +2 -0
  251. package/dist/presentation/routes/list-active-provisioners.d.ts.map +1 -0
  252. package/dist/presentation/routes/list-active-provisioners.js +32 -0
  253. package/dist/presentation/routes/list-active-provisioners.js.map +1 -0
  254. package/dist/presentation/routes/list-active-runners.d.ts +2 -0
  255. package/dist/presentation/routes/list-active-runners.d.ts.map +1 -0
  256. package/dist/presentation/routes/list-active-runners.js +32 -0
  257. package/dist/presentation/routes/list-active-runners.js.map +1 -0
  258. package/dist/presentation/routes/list-manual-registration-tokens.d.ts +2 -0
  259. package/dist/presentation/routes/list-manual-registration-tokens.d.ts.map +1 -0
  260. package/dist/presentation/routes/list-manual-registration-tokens.js +32 -0
  261. package/dist/presentation/routes/list-manual-registration-tokens.js.map +1 -0
  262. package/dist/presentation/routes/list-provisioner-tokens.d.ts +2 -0
  263. package/dist/presentation/routes/list-provisioner-tokens.d.ts.map +1 -0
  264. package/dist/presentation/routes/list-provisioner-tokens.js +32 -0
  265. package/dist/presentation/routes/list-provisioner-tokens.js.map +1 -0
  266. package/dist/presentation/routes/mint-registration-tokens.d.ts +2 -0
  267. package/dist/presentation/routes/mint-registration-tokens.d.ts.map +1 -0
  268. package/dist/presentation/routes/mint-registration-tokens.js +74 -0
  269. package/dist/presentation/routes/mint-registration-tokens.js.map +1 -0
  270. package/dist/presentation/routes/poll-demand.d.ts +2 -0
  271. package/dist/presentation/routes/poll-demand.d.ts.map +1 -0
  272. package/dist/presentation/routes/poll-demand.js +53 -0
  273. package/dist/presentation/routes/poll-demand.js.map +1 -0
  274. package/dist/presentation/routes/rate-limit.d.ts +4 -0
  275. package/dist/presentation/routes/rate-limit.d.ts.map +1 -0
  276. package/dist/presentation/routes/rate-limit.js +94 -0
  277. package/dist/presentation/routes/rate-limit.js.map +1 -0
  278. package/dist/presentation/routes/reconcile-provisioned-runners.d.ts +2 -0
  279. package/dist/presentation/routes/reconcile-provisioned-runners.d.ts.map +1 -0
  280. package/dist/presentation/routes/reconcile-provisioned-runners.js +27 -0
  281. package/dist/presentation/routes/reconcile-provisioned-runners.js.map +1 -0
  282. package/dist/presentation/routes/register.d.ts +2 -0
  283. package/dist/presentation/routes/register.d.ts.map +1 -0
  284. package/dist/presentation/routes/register.js +63 -0
  285. package/dist/presentation/routes/register.js.map +1 -0
  286. package/dist/presentation/routes/report-provisioned-runners.d.ts +2 -0
  287. package/dist/presentation/routes/report-provisioned-runners.d.ts.map +1 -0
  288. package/dist/presentation/routes/report-provisioned-runners.js +37 -0
  289. package/dist/presentation/routes/report-provisioned-runners.js.map +1 -0
  290. package/dist/presentation/routes/request-job.d.ts +2 -0
  291. package/dist/presentation/routes/request-job.d.ts.map +1 -0
  292. package/dist/presentation/routes/request-job.js +45 -0
  293. package/dist/presentation/routes/request-job.js.map +1 -0
  294. package/dist/presentation/routes/revoke-manual-registration-token.d.ts +2 -0
  295. package/dist/presentation/routes/revoke-manual-registration-token.d.ts.map +1 -0
  296. package/dist/presentation/routes/revoke-manual-registration-token.js +42 -0
  297. package/dist/presentation/routes/revoke-manual-registration-token.js.map +1 -0
  298. package/dist/presentation/routes/revoke-provisioner-token.d.ts +2 -0
  299. package/dist/presentation/routes/revoke-provisioner-token.d.ts.map +1 -0
  300. package/dist/presentation/routes/revoke-provisioner-token.js +44 -0
  301. package/dist/presentation/routes/revoke-provisioner-token.js.map +1 -0
  302. package/dist/presentation/routes/workspace-membership.d.ts +6 -0
  303. package/dist/presentation/routes/workspace-membership.d.ts.map +1 -0
  304. package/dist/presentation/routes/workspace-membership.js +6 -0
  305. package/dist/presentation/routes/workspace-membership.js.map +1 -0
  306. package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.d.ts +3 -0
  307. package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.d.ts.map +1 -0
  308. package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.js +14 -0
  309. package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.js.map +1 -0
  310. package/dist/temporal/activities/index.d.ts +9 -0
  311. package/dist/temporal/activities/index.d.ts.map +1 -0
  312. package/dist/temporal/activities/index.js +12 -0
  313. package/dist/temporal/activities/index.js.map +1 -0
  314. package/dist/temporal/activities/maintenance-activities.d.ts +31 -0
  315. package/dist/temporal/activities/maintenance-activities.d.ts.map +1 -0
  316. package/dist/temporal/activities/maintenance-activities.js +18 -0
  317. package/dist/temporal/activities/maintenance-activities.js.map +1 -0
  318. package/dist/temporal/constants.d.ts +2 -0
  319. package/dist/temporal/constants.d.ts.map +1 -0
  320. package/dist/temporal/constants.js +3 -0
  321. package/dist/temporal/constants.js.map +1 -0
  322. package/dist/temporal/workflows/index.d.ts +2 -0
  323. package/dist/temporal/workflows/index.d.ts.map +1 -0
  324. package/dist/temporal/workflows/index.js +3 -0
  325. package/dist/temporal/workflows/index.js.map +1 -0
  326. package/dist/temporal/workflows/stuck-job-detector.d.ts +2 -0
  327. package/dist/temporal/workflows/stuck-job-detector.d.ts.map +1 -0
  328. package/dist/temporal/workflows/stuck-job-detector.js +61 -0
  329. package/dist/temporal/workflows/stuck-job-detector.js.map +1 -0
  330. package/dist/tsconfig.test.tsbuildinfo +1 -0
  331. package/drizzle/0000_initial.sql +185 -0
  332. package/drizzle/meta/0000_snapshot.json +1582 -0
  333. package/drizzle/meta/_journal.json +13 -0
  334. package/drizzle.config.ts +7 -0
  335. package/package.json +82 -0
  336. package/src/config.test.ts +374 -0
  337. package/src/config.ts +330 -0
  338. package/src/core/demand.test.ts +301 -0
  339. package/src/core/demand.ts +239 -0
  340. package/src/core/entities/ephemeral-registration-token.ts +13 -0
  341. package/src/core/entities/manual-registration-token.ts +11 -0
  342. package/src/core/entities/provisioned-runner.ts +30 -0
  343. package/src/core/entities/provisioner-token.ts +16 -0
  344. package/src/core/entities/reservation.ts +9 -0
  345. package/src/core/entities/runner-session.ts +18 -0
  346. package/src/core/ephemeral-registration-tokens.test.ts +108 -0
  347. package/src/core/ephemeral-registration-tokens.ts +112 -0
  348. package/src/core/errors.ts +128 -0
  349. package/src/core/index.ts +56 -0
  350. package/src/core/job-executions.ts +47 -0
  351. package/src/core/maintenance-policy.ts +1 -0
  352. package/src/core/maintenance.test.ts +251 -0
  353. package/src/core/maintenance.ts +74 -0
  354. package/src/core/manual-registration-tokens.test.ts +44 -0
  355. package/src/core/manual-registration-tokens.ts +51 -0
  356. package/src/core/provisioned-runners.test.ts +101 -0
  357. package/src/core/provisioned-runners.ts +301 -0
  358. package/src/core/provisioner-tokens.test.ts +61 -0
  359. package/src/core/provisioner-tokens.ts +61 -0
  360. package/src/core/rate-limit.test.ts +158 -0
  361. package/src/core/rate-limit.ts +121 -0
  362. package/src/core/runner-sessions.test.ts +209 -0
  363. package/src/core/runner-sessions.ts +66 -0
  364. package/src/core/runner-tool-capabilities.test.ts +141 -0
  365. package/src/core/runner-tool-capabilities.ts +75 -0
  366. package/src/db/db.ts +38 -0
  367. package/src/db/ephemeral-registration-tokens.test.ts +332 -0
  368. package/src/db/ephemeral-registration-tokens.ts +348 -0
  369. package/src/db/index.ts +90 -0
  370. package/src/db/job-executions.test.ts +1500 -0
  371. package/src/db/job-executions.ts +647 -0
  372. package/src/db/manual-registration-tokens.test.ts +99 -0
  373. package/src/db/manual-registration-tokens.ts +87 -0
  374. package/src/db/provisioned-runners.test.ts +1812 -0
  375. package/src/db/provisioned-runners.ts +886 -0
  376. package/src/db/provisioner-tokens.test.ts +133 -0
  377. package/src/db/provisioner-tokens.ts +142 -0
  378. package/src/db/rate-limits.test.ts +74 -0
  379. package/src/db/rate-limits.ts +69 -0
  380. package/src/db/reservations.test.ts +399 -0
  381. package/src/db/reservations.ts +296 -0
  382. package/src/db/runner-sessions.test.ts +153 -0
  383. package/src/db/runner-sessions.ts +97 -0
  384. package/src/db/schema/common.ts +3 -0
  385. package/src/db/schema/ephemeral-registration-tokens.ts +58 -0
  386. package/src/db/schema/manual-registration-tokens.ts +40 -0
  387. package/src/db/schema/outbox.ts +4 -0
  388. package/src/db/schema/pending-job-executions.ts +25 -0
  389. package/src/db/schema/provisioned-runners.ts +89 -0
  390. package/src/db/schema/provisioner-tokens.ts +50 -0
  391. package/src/db/schema/rate-limits.ts +27 -0
  392. package/src/db/schema/reservations.ts +38 -0
  393. package/src/db/schema/runner-sessions.ts +77 -0
  394. package/src/db/schema/running-job-executions.ts +50 -0
  395. package/src/index.ts +63 -0
  396. package/src/metrics/index.ts +13 -0
  397. package/src/metrics/instance.ts +115 -0
  398. package/src/metrics/service.ts +22 -0
  399. package/src/presentation/auth/index.ts +6 -0
  400. package/src/presentation/auth/provisioner-token-auth.ts +76 -0
  401. package/src/presentation/auth/runner-registration-token-auth.ts +105 -0
  402. package/src/presentation/dto/active-provisioner.ts +15 -0
  403. package/src/presentation/dto/index.ts +13 -0
  404. package/src/presentation/dto/manual-registration-token.ts +23 -0
  405. package/src/presentation/dto/mint-registration-tokens.ts +35 -0
  406. package/src/presentation/dto/poll-demand.ts +20 -0
  407. package/src/presentation/dto/provisioned-runners.ts +88 -0
  408. package/src/presentation/dto/provisioner-token.ts +29 -0
  409. package/src/presentation/index.ts +6 -0
  410. package/src/presentation/routes/create-manual-registration-token.ts +43 -0
  411. package/src/presentation/routes/create-provisioner-token.ts +41 -0
  412. package/src/presentation/routes/get-provisioner-me.ts +22 -0
  413. package/src/presentation/routes/heartbeat.test.ts +325 -0
  414. package/src/presentation/routes/heartbeat.ts +80 -0
  415. package/src/presentation/routes/index.ts +86 -0
  416. package/src/presentation/routes/list-active-provisioners.ts +25 -0
  417. package/src/presentation/routes/list-active-runners.test.ts +366 -0
  418. package/src/presentation/routes/list-active-runners.ts +25 -0
  419. package/src/presentation/routes/list-manual-registration-tokens.ts +25 -0
  420. package/src/presentation/routes/list-provisioner-tokens.ts +25 -0
  421. package/src/presentation/routes/manual-registration-tokens.test.ts +213 -0
  422. package/src/presentation/routes/mint-registration-tokens.test.ts +453 -0
  423. package/src/presentation/routes/mint-registration-tokens.ts +82 -0
  424. package/src/presentation/routes/poll-demand-release.test.ts +80 -0
  425. package/src/presentation/routes/poll-demand.test.ts +323 -0
  426. package/src/presentation/routes/poll-demand.ts +56 -0
  427. package/src/presentation/routes/provisioner-me.test.ts +172 -0
  428. package/src/presentation/routes/provisioner-tokens.test.ts +219 -0
  429. package/src/presentation/routes/rate-limit.ts +115 -0
  430. package/src/presentation/routes/reconcile-provisioned-runners.test.ts +380 -0
  431. package/src/presentation/routes/reconcile-provisioned-runners.ts +30 -0
  432. package/src/presentation/routes/register.test.ts +453 -0
  433. package/src/presentation/routes/register.ts +75 -0
  434. package/src/presentation/routes/report-provisioned-runners.test.ts +249 -0
  435. package/src/presentation/routes/report-provisioned-runners.ts +40 -0
  436. package/src/presentation/routes/request-job.test.ts +222 -0
  437. package/src/presentation/routes/request-job.ts +47 -0
  438. package/src/presentation/routes/revoke-manual-registration-token.ts +35 -0
  439. package/src/presentation/routes/revoke-provisioner-token.ts +37 -0
  440. package/src/presentation/routes/workspace-membership.ts +9 -0
  441. package/src/presentation/subscribers/on-workflows-job-execution-timed-out.test.ts +147 -0
  442. package/src/presentation/subscribers/on-workflows-job-execution-timed-out.ts +17 -0
  443. package/src/temporal/activities/index.ts +17 -0
  444. package/src/temporal/activities/maintenance-activities.test.ts +104 -0
  445. package/src/temporal/activities/maintenance-activities.ts +41 -0
  446. package/src/temporal/constants.ts +1 -0
  447. package/src/temporal/workflows/index.ts +1 -0
  448. package/src/temporal/workflows/stuck-job-detector.test.ts +93 -0
  449. package/src/temporal/workflows/stuck-job-detector.ts +67 -0
  450. package/test/env.ts +11 -0
  451. package/test/factories/ephemeral-registration-token.ts +41 -0
  452. package/test/factories/manual-registration-token.ts +37 -0
  453. package/test/factories/pending-job.ts +44 -0
  454. package/test/factories/provisioned-runner.ts +78 -0
  455. package/test/factories/provisioner-token.ts +41 -0
  456. package/test/factories/reservation.ts +33 -0
  457. package/test/factories/runner-session.ts +32 -0
  458. package/test/globalSetup.ts +23 -0
  459. package/test/index.ts +16 -0
  460. package/test/setup.ts +17 -0
  461. package/tsconfig.build.json +9 -0
  462. package/tsconfig.build.tsbuildinfo +1 -0
  463. package/tsconfig.json +3 -0
  464. package/tsconfig.test.json +8 -0
  465. package/vitest.config.ts +14 -0
@@ -0,0 +1,399 @@
1
+ import {and, eq, inArray, or, sql, sum} from 'drizzle-orm';
2
+ import {db} from '#db/db.js';
3
+ import {
4
+ deleteReservationsByIds,
5
+ pollDemandAndReserve,
6
+ releaseReservationUnits,
7
+ } from '#db/reservations.js';
8
+ import {reservations} from '#db/schema/reservations.js';
9
+ import {pendingJobFactory, reservationFactory} from '#test/index.js';
10
+
11
+ describe('pollDemandAndReserve', () => {
12
+ let workspaceId: string;
13
+ let provisionerId: string;
14
+
15
+ beforeEach(() => {
16
+ workspaceId = crypto.randomUUID();
17
+ provisionerId = crypto.randomUUID();
18
+ });
19
+
20
+ it('caps grants by unreserved demand, available slots, and max reservations', async () => {
21
+ await createPendingJobs(50, ['linux']);
22
+
23
+ const result = await pollDemandAndReserve({
24
+ workspaceId,
25
+ provisionerId,
26
+ maxReservations: 100,
27
+ ttlSeconds: 60,
28
+ templates: [template('linux', ['linux'], 20)],
29
+ });
30
+
31
+ expect(result.reservations).toHaveLength(1);
32
+ expect(result.reservations[0]?.count).toBe(20);
33
+ expect(result.stats[0]).toMatchObject({labels: ['linux'], queued: 50, reserved: 20});
34
+ });
35
+
36
+ it('allocates overlapping label sets most-specific-first', async () => {
37
+ await createPendingJobs(2, ['linux']);
38
+ await createPendingJobs(1, ['linux', 'gpu']);
39
+
40
+ const result = await pollDemandAndReserve({
41
+ workspaceId,
42
+ provisionerId,
43
+ maxReservations: 2,
44
+ ttlSeconds: 60,
45
+ templates: [template('linux', ['linux'], 1), template('linux-gpu', ['linux', 'gpu'], 1)],
46
+ });
47
+
48
+ expect(result.reservations).toEqual([
49
+ expect.objectContaining({labels: ['gpu', 'linux'], count: 1}),
50
+ expect.objectContaining({labels: ['linux'], count: 1}),
51
+ ]);
52
+ });
53
+
54
+ it('serializes multiple provisioners so total active reservations do not exceed queued demand', async () => {
55
+ await createPendingJobs(5, ['linux']);
56
+
57
+ await Promise.all([
58
+ pollDemandAndReserve({
59
+ workspaceId,
60
+ provisionerId: crypto.randomUUID(),
61
+ maxReservations: 5,
62
+ ttlSeconds: 60,
63
+ templates: [template('linux-a', ['linux'], 5)],
64
+ }),
65
+ pollDemandAndReserve({
66
+ workspaceId,
67
+ provisionerId: crypto.randomUUID(),
68
+ maxReservations: 5,
69
+ ttlSeconds: 60,
70
+ templates: [template('linux-b', ['linux'], 5)],
71
+ }),
72
+ ]);
73
+
74
+ const reserved = await activeReservedCount();
75
+ expect(reserved).toBeLessThanOrEqual(5);
76
+ });
77
+
78
+ it('does not count expired reservations against demand', async () => {
79
+ await createPendingJobs(1, ['linux']);
80
+ await reservationFactory.create({
81
+ workspaceId,
82
+ provisionerId,
83
+ requiredLabels: ['linux'],
84
+ count: 1,
85
+ expiresAt: new Date(Date.now() - 60_000),
86
+ });
87
+
88
+ const result = await pollDemandAndReserve({
89
+ workspaceId,
90
+ provisionerId,
91
+ maxReservations: 1,
92
+ ttlSeconds: 60,
93
+ templates: [template('linux', ['linux'], 1)],
94
+ });
95
+
96
+ expect(result.reservations).toHaveLength(1);
97
+ expect(result.reservations[0]?.count).toBe(1);
98
+ expect(result.stats[0]).toMatchObject({queued: 1, reserved: 1});
99
+ });
100
+
101
+ it('deducts this provisioner active reservations from advertised capacity', async () => {
102
+ await createPendingJobs(10, ['linux']);
103
+ await reservationFactory.create({
104
+ workspaceId,
105
+ provisionerId,
106
+ requiredLabels: ['linux'],
107
+ count: 5,
108
+ expiresAt: new Date(Date.now() + 60_000),
109
+ });
110
+
111
+ const result = await pollDemandAndReserve({
112
+ workspaceId,
113
+ provisionerId,
114
+ maxReservations: 5,
115
+ ttlSeconds: 60,
116
+ templates: [template('linux', ['linux'], 5)],
117
+ });
118
+
119
+ const reserved = await activeReservedCount();
120
+ expect(result.reservations).toEqual([]);
121
+ expect(result.stats[0]).toMatchObject({queued: 10, reserved: 5});
122
+ expect(reserved).toBe(5);
123
+ });
124
+
125
+ it('returns multiple reservation groups in one response', async () => {
126
+ await createPendingJobs(2, ['linux']);
127
+ await createPendingJobs(1, ['macos']);
128
+
129
+ const result = await pollDemandAndReserve({
130
+ workspaceId,
131
+ provisionerId,
132
+ maxReservations: 3,
133
+ ttlSeconds: 60,
134
+ templates: [template('linux', ['linux'], 2), template('macos', ['macos'], 1)],
135
+ });
136
+
137
+ expect(result.reservations).toEqual(
138
+ expect.arrayContaining([
139
+ expect.objectContaining({labels: ['linux'], count: 2}),
140
+ expect.objectContaining({labels: ['macos'], count: 1}),
141
+ ]),
142
+ );
143
+ });
144
+
145
+ it('splits max reservation budget across groups after the most-specific group', async () => {
146
+ await createPendingJobs(3, ['linux', 'gpu']);
147
+ await createPendingJobs(3, ['linux']);
148
+
149
+ const result = await pollDemandAndReserve({
150
+ workspaceId,
151
+ provisionerId,
152
+ maxReservations: 4,
153
+ ttlSeconds: 60,
154
+ templates: [template('linux', ['linux'], 10), template('linux-gpu', ['linux', 'gpu'], 10)],
155
+ });
156
+
157
+ expect(result.reservations).toEqual([
158
+ expect.objectContaining({labels: ['gpu', 'linux'], count: 3}),
159
+ expect.objectContaining({labels: ['linux'], count: 1}),
160
+ ]);
161
+ });
162
+
163
+ it('excludes demand that no template can satisfy from stats', async () => {
164
+ await createPendingJobs(1, ['windows']);
165
+
166
+ const result = await pollDemandAndReserve({
167
+ workspaceId,
168
+ provisionerId,
169
+ maxReservations: 1,
170
+ ttlSeconds: 60,
171
+ templates: [template('linux', ['linux'], 1)],
172
+ });
173
+
174
+ expect(result).toEqual({stats: [], reservations: []});
175
+ });
176
+
177
+ it('returns stats without writing rows when max reservations is zero', async () => {
178
+ await createPendingJobs(1, ['linux']);
179
+
180
+ const result = await pollDemandAndReserve({
181
+ workspaceId,
182
+ provisionerId,
183
+ maxReservations: 0,
184
+ ttlSeconds: 60,
185
+ templates: [template('linux', ['linux'], 1)],
186
+ });
187
+
188
+ const reserved = await activeReservedCount();
189
+ expect(result.stats).toEqual([
190
+ expect.objectContaining({labels: ['linux'], queued: 1, reserved: 0}),
191
+ ]);
192
+ expect(result.reservations).toEqual([]);
193
+ expect(reserved).toBe(0);
194
+ });
195
+
196
+ it('sets reservation expiry from database time', async () => {
197
+ await createPendingJobs(1, ['linux']);
198
+ const before = Date.now();
199
+
200
+ const result = await pollDemandAndReserve({
201
+ workspaceId,
202
+ provisionerId,
203
+ maxReservations: 1,
204
+ ttlSeconds: 60,
205
+ templates: [template('linux', ['linux'], 1)],
206
+ });
207
+
208
+ const expiresAt = result.reservations[0]?.expiresAt;
209
+ expect(expiresAt).toBeInstanceOf(Date);
210
+ expect(expiresAt?.getTime()).toBeGreaterThan(before + 50_000);
211
+ });
212
+
213
+ it('deletes reservations by id', async () => {
214
+ await reservationFactory.create({
215
+ workspaceId,
216
+ provisionerId,
217
+ requiredLabels: ['linux'],
218
+ count: 1,
219
+ expiresAt: new Date(Date.now() + 60_000),
220
+ });
221
+ await reservationFactory.create({
222
+ workspaceId,
223
+ provisionerId,
224
+ requiredLabels: ['macos'],
225
+ count: 1,
226
+ expiresAt: new Date(Date.now() + 60_000),
227
+ });
228
+ const beforeDelete = await reservationsForTest();
229
+ const deletedReservation = beforeDelete.find((reservation) =>
230
+ reservation.requiredLabels.includes('linux'),
231
+ );
232
+ if (!deletedReservation) throw new Error('Expected linux reservation');
233
+
234
+ const deleted = await deleteReservationsByIds([deletedReservation.id]);
235
+
236
+ const remaining = await reservationsForTest();
237
+ expect(deleted).toBe(1);
238
+ expect(remaining).toHaveLength(1);
239
+ expect(remaining[0]?.requiredLabels).toEqual(['macos']);
240
+ });
241
+
242
+ it('decrements reservation units inside a caller transaction', async () => {
243
+ await reservationFactory.create({
244
+ workspaceId,
245
+ provisionerId,
246
+ requiredLabels: ['linux'],
247
+ count: 3,
248
+ expiresAt: new Date(Date.now() + 60_000),
249
+ });
250
+ const [reservation] = await reservationsForTest();
251
+ if (!reservation) throw new Error('Expected reservation');
252
+
253
+ const released = await db().transaction((tx) =>
254
+ releaseReservationUnits(tx, {
255
+ workspaceId,
256
+ provisionerId,
257
+ releases: [{reservationId: reservation.id, count: 2}],
258
+ }),
259
+ );
260
+
261
+ const rows = await reservationsForTest();
262
+ expect(released).toBe(2);
263
+ expect(rows[0]?.count).toBe(1);
264
+ });
265
+
266
+ it('deletes reservations when releasing all remaining units', async () => {
267
+ await reservationFactory.create({
268
+ workspaceId,
269
+ provisionerId,
270
+ requiredLabels: ['linux'],
271
+ count: 1,
272
+ expiresAt: new Date(Date.now() + 60_000),
273
+ });
274
+ const [reservation] = await reservationsForTest();
275
+ if (!reservation) throw new Error('Expected reservation');
276
+
277
+ const released = await db().transaction((tx) =>
278
+ releaseReservationUnits(tx, {
279
+ workspaceId,
280
+ provisionerId,
281
+ releases: [{reservationId: reservation.id, count: 1}],
282
+ }),
283
+ );
284
+
285
+ const rows = await reservationsForTest();
286
+ expect(released).toBe(1);
287
+ expect(rows).toHaveLength(0);
288
+ });
289
+
290
+ it('does not release another workspace or provisioner reservation', async () => {
291
+ const otherWorkspace = await reservationFactory.create({
292
+ workspaceId: crypto.randomUUID(),
293
+ provisionerId,
294
+ requiredLabels: ['linux'],
295
+ count: 1,
296
+ expiresAt: new Date(Date.now() + 60_000),
297
+ });
298
+ const otherProvisioner = await reservationFactory.create({
299
+ workspaceId,
300
+ provisionerId: crypto.randomUUID(),
301
+ requiredLabels: ['linux'],
302
+ count: 1,
303
+ expiresAt: new Date(Date.now() + 60_000),
304
+ });
305
+ const rowsBefore = await db()
306
+ .select()
307
+ .from(reservations)
308
+ .where(
309
+ or(
310
+ and(
311
+ eq(reservations.workspaceId, otherWorkspace.workspaceId),
312
+ eq(reservations.provisionerId, otherWorkspace.provisionerId),
313
+ ),
314
+ and(
315
+ eq(reservations.workspaceId, otherProvisioner.workspaceId),
316
+ eq(reservations.provisionerId, otherProvisioner.provisionerId),
317
+ ),
318
+ ),
319
+ );
320
+
321
+ const released = await db().transaction((tx) =>
322
+ releaseReservationUnits(tx, {
323
+ workspaceId,
324
+ provisionerId,
325
+ releases: rowsBefore.map((reservation) => ({reservationId: reservation.id, count: 1})),
326
+ }),
327
+ );
328
+
329
+ const rowsAfter = await db()
330
+ .select()
331
+ .from(reservations)
332
+ .where(
333
+ inArray(
334
+ reservations.id,
335
+ rowsBefore.map((reservation) => reservation.id),
336
+ ),
337
+ );
338
+ expect(released).toBe(0);
339
+ expect(rowsAfter.map((reservation) => reservation.id).sort()).toEqual(
340
+ rowsBefore.map((reservation) => reservation.id).sort(),
341
+ );
342
+ });
343
+
344
+ it('credits only the deleted reservation count when release units exceed the row count', async () => {
345
+ await reservationFactory.create({
346
+ workspaceId,
347
+ provisionerId,
348
+ requiredLabels: ['linux'],
349
+ count: 1,
350
+ expiresAt: new Date(Date.now() + 60_000),
351
+ });
352
+ const [reservation] = await reservationsForTest();
353
+ if (!reservation) throw new Error('Expected reservation');
354
+
355
+ const released = await db().transaction((tx) =>
356
+ releaseReservationUnits(tx, {
357
+ workspaceId,
358
+ provisionerId,
359
+ releases: [{reservationId: reservation.id, count: 3}],
360
+ }),
361
+ );
362
+
363
+ const rows = await reservationsForTest();
364
+ expect(released).toBe(1);
365
+ expect(rows).toHaveLength(0);
366
+ });
367
+
368
+ async function createPendingJobs(count: number, requiredLabels: string[]): Promise<void> {
369
+ for (let index = 0; index < count; index++) {
370
+ await pendingJobFactory.create({workspaceId, requiredLabels});
371
+ }
372
+ }
373
+
374
+ async function activeReservedCount(): Promise<number> {
375
+ const [row] = await db()
376
+ .select({value: sum(reservations.count)})
377
+ .from(reservations)
378
+ .where(
379
+ sql`${reservations.workspaceId} = ${workspaceId} and ${reservations.expiresAt} > now()`,
380
+ );
381
+ return Number(row?.value ?? 0);
382
+ }
383
+
384
+ async function reservationsForTest() {
385
+ return await db()
386
+ .select()
387
+ .from(reservations)
388
+ .where(
389
+ and(
390
+ eq(reservations.workspaceId, workspaceId),
391
+ eq(reservations.provisionerId, provisionerId),
392
+ ),
393
+ );
394
+ }
395
+
396
+ function template(templateKey: string, labels: string[], availableSlots: number) {
397
+ return {templateKey, labels, availableSlots, starting: 0, running: 0};
398
+ }
399
+ });
@@ -0,0 +1,296 @@
1
+ import {canonicalizeLabels} from '@shipfox/runner-labels';
2
+ import {and, asc, eq, gt, inArray, lt, sql} from 'drizzle-orm';
3
+ import type {Tx} from './db.js';
4
+ import {db} from './db.js';
5
+ import {pendingJobExecutions} from './schema/pending-job-executions.js';
6
+ import {reservations} from './schema/reservations.js';
7
+
8
+ export interface ReservationTemplate {
9
+ templateKey: string;
10
+ labels: string[];
11
+ availableSlots: number;
12
+ starting: number;
13
+ running: number;
14
+ }
15
+
16
+ export interface DemandStat {
17
+ labels: string[];
18
+ queued: number;
19
+ reserved: number;
20
+ oldestQueuedAt: Date;
21
+ }
22
+
23
+ export interface ReservationGrant {
24
+ reservationId: string;
25
+ labels: string[];
26
+ count: number;
27
+ expiresAt: Date;
28
+ }
29
+
30
+ export interface PollDemandAndReserveParams {
31
+ workspaceId: string;
32
+ provisionerId: string;
33
+ maxReservations: number;
34
+ ttlSeconds: number;
35
+ templates: ReservationTemplate[];
36
+ }
37
+
38
+ interface NormalizedTemplate {
39
+ templateKey: string;
40
+ labels: string[];
41
+ remainingSlots: number;
42
+ }
43
+
44
+ interface DemandRow {
45
+ requiredLabels: string[];
46
+ queued: number;
47
+ oldestQueuedAt: Date;
48
+ }
49
+
50
+ export async function pollDemandAndReserve(
51
+ params: PollDemandAndReserveParams,
52
+ ): Promise<{stats: DemandStat[]; reservations: ReservationGrant[]}> {
53
+ return await db().transaction(async (tx) => {
54
+ return await pollDemandAndReserveTx(tx, params);
55
+ });
56
+ }
57
+
58
+ export async function pollDemandAndReserveTx(
59
+ tx: Tx,
60
+ params: PollDemandAndReserveParams,
61
+ ): Promise<{stats: DemandStat[]; reservations: ReservationGrant[]}> {
62
+ await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${params.workspaceId}))`);
63
+
64
+ const demandRows = (
65
+ await tx
66
+ .select({
67
+ requiredLabels: pendingJobExecutions.requiredLabels,
68
+ queued: sql<number>`count(*)::int`,
69
+ oldestQueuedAt: sql<Date | string>`min(${pendingJobExecutions.createdAt})`,
70
+ })
71
+ .from(pendingJobExecutions)
72
+ .where(eq(pendingJobExecutions.workspaceId, params.workspaceId))
73
+ .groupBy(pendingJobExecutions.requiredLabels)
74
+ ).map((row) => ({
75
+ ...row,
76
+ oldestQueuedAt: new Date(row.oldestQueuedAt),
77
+ }));
78
+
79
+ const activeReservationRows = await tx
80
+ .select({
81
+ requiredLabels: reservations.requiredLabels,
82
+ reserved: sql<number>`coalesce(sum(${reservations.count}), 0)::int`,
83
+ })
84
+ .from(reservations)
85
+ .where(
86
+ and(eq(reservations.workspaceId, params.workspaceId), gt(reservations.expiresAt, sql`now()`)),
87
+ )
88
+ .groupBy(reservations.requiredLabels);
89
+
90
+ const reservedByLabels = new Map(
91
+ activeReservationRows.map((row) => [labelKey(row.requiredLabels), row.reserved]),
92
+ );
93
+ const activeProvisionerReservationRows = await tx
94
+ .select({
95
+ requiredLabels: reservations.requiredLabels,
96
+ reserved: sql<number>`coalesce(sum(${reservations.count}), 0)::int`,
97
+ })
98
+ .from(reservations)
99
+ .where(
100
+ and(
101
+ eq(reservations.workspaceId, params.workspaceId),
102
+ eq(reservations.provisionerId, params.provisionerId),
103
+ gt(reservations.expiresAt, sql`now()`),
104
+ ),
105
+ )
106
+ .groupBy(reservations.requiredLabels);
107
+
108
+ const templates = params.templates.map((template) => ({
109
+ templateKey: template.templateKey,
110
+ labels: [...canonicalizeLabels(template.labels)],
111
+ remainingSlots: template.availableSlots,
112
+ }));
113
+ deductProvisionerReservations(templates, activeProvisionerReservationRows);
114
+ const stats: DemandStat[] = [];
115
+ const grants: ReservationGrant[] = [];
116
+ let remainingMaxReservations = params.maxReservations;
117
+
118
+ for (const demand of sortDemandRows(demandRows)) {
119
+ const satisfyingTemplates = templates
120
+ .filter((template) => isSubset(demand.requiredLabels, template.labels))
121
+ .sort(
122
+ (a, b) => a.labels.length - b.labels.length || a.templateKey.localeCompare(b.templateKey),
123
+ );
124
+
125
+ if (satisfyingTemplates.length === 0) continue;
126
+
127
+ const reserved = reservedByLabels.get(labelKey(demand.requiredLabels)) ?? 0;
128
+ const unreserved = Math.max(0, demand.queued - reserved);
129
+ const capacity = satisfyingTemplates.reduce(
130
+ (total, template) => total + template.remainingSlots,
131
+ 0,
132
+ );
133
+ const grant = Math.min(unreserved, capacity, remainingMaxReservations);
134
+ let reservedAfterGrant = reserved;
135
+
136
+ if (grant > 0 && params.maxReservations > 0) {
137
+ const [inserted] = await tx
138
+ .insert(reservations)
139
+ .values({
140
+ workspaceId: params.workspaceId,
141
+ provisionerId: params.provisionerId,
142
+ requiredLabels: demand.requiredLabels,
143
+ count: grant,
144
+ expiresAt: sql`now() + (${params.ttlSeconds} || ' seconds')::interval`,
145
+ })
146
+ .returning({id: reservations.id, expiresAt: reservations.expiresAt});
147
+
148
+ if (!inserted) throw new Error('Insert returned no rows');
149
+
150
+ remainingMaxReservations -= grant;
151
+ drawSlots(satisfyingTemplates, grant);
152
+ reservedAfterGrant += grant;
153
+ grants.push({
154
+ reservationId: inserted.id,
155
+ labels: demand.requiredLabels,
156
+ count: grant,
157
+ expiresAt: inserted.expiresAt,
158
+ });
159
+ }
160
+
161
+ stats.push({
162
+ labels: demand.requiredLabels,
163
+ queued: demand.queued,
164
+ reserved: reservedAfterGrant,
165
+ oldestQueuedAt: demand.oldestQueuedAt,
166
+ });
167
+ }
168
+
169
+ return {stats, reservations: grants};
170
+ }
171
+
172
+ export async function deleteExpiredReservations(params?: {limit?: number}): Promise<number> {
173
+ const expiredIds = db()
174
+ .select({id: reservations.id})
175
+ .from(reservations)
176
+ .where(lt(reservations.expiresAt, sql`now()`))
177
+ .orderBy(asc(reservations.expiresAt))
178
+ .limit(params?.limit ?? 1000);
179
+
180
+ const deleted = await db()
181
+ .delete(reservations)
182
+ .where(inArray(reservations.id, expiredIds))
183
+ .returning({id: reservations.id});
184
+
185
+ return deleted.length;
186
+ }
187
+
188
+ export async function deleteReservationsByIds(ids: string[]): Promise<number> {
189
+ if (ids.length === 0) return 0;
190
+
191
+ const deleted = await db()
192
+ .delete(reservations)
193
+ .where(inArray(reservations.id, ids))
194
+ .returning({id: reservations.id});
195
+
196
+ return deleted.length;
197
+ }
198
+
199
+ export async function releaseReservationUnits(
200
+ tx: Tx,
201
+ params: {
202
+ workspaceId: string;
203
+ provisionerId: string;
204
+ releases: Array<{reservationId: string; count: number}>;
205
+ },
206
+ ): Promise<number> {
207
+ let released = 0;
208
+
209
+ for (const release of params.releases) {
210
+ if (release.count <= 0) continue;
211
+
212
+ const decremented = await tx
213
+ .update(reservations)
214
+ .set({count: sql`${reservations.count} - ${release.count}`})
215
+ .where(
216
+ and(
217
+ eq(reservations.id, release.reservationId),
218
+ eq(reservations.workspaceId, params.workspaceId),
219
+ eq(reservations.provisionerId, params.provisionerId),
220
+ gt(reservations.count, release.count),
221
+ gt(reservations.expiresAt, sql`now()`),
222
+ ),
223
+ )
224
+ .returning({id: reservations.id});
225
+
226
+ if (decremented.length > 0) {
227
+ released += release.count;
228
+ continue;
229
+ }
230
+
231
+ const deleted = await tx
232
+ .delete(reservations)
233
+ .where(
234
+ and(
235
+ eq(reservations.id, release.reservationId),
236
+ eq(reservations.workspaceId, params.workspaceId),
237
+ eq(reservations.provisionerId, params.provisionerId),
238
+ sql`${reservations.count} <= ${release.count}`,
239
+ gt(reservations.expiresAt, sql`now()`),
240
+ ),
241
+ )
242
+ .returning({count: reservations.count});
243
+
244
+ released += deleted.reduce((total, row) => total + row.count, 0);
245
+ }
246
+
247
+ return released;
248
+ }
249
+
250
+ function deductProvisionerReservations(
251
+ templates: NormalizedTemplate[],
252
+ activeReservations: {requiredLabels: string[]; reserved: number}[],
253
+ ): void {
254
+ for (const reservation of sortReservationRows(activeReservations)) {
255
+ const satisfyingTemplates = templates
256
+ .filter((template) => isSubset(reservation.requiredLabels, template.labels))
257
+ .sort(
258
+ (a, b) => a.labels.length - b.labels.length || a.templateKey.localeCompare(b.templateKey),
259
+ );
260
+ drawSlots(satisfyingTemplates, reservation.reserved);
261
+ }
262
+ }
263
+
264
+ function sortDemandRows(rows: DemandRow[]): DemandRow[] {
265
+ return [...rows].sort((a, b) => {
266
+ const specificity = b.requiredLabels.length - a.requiredLabels.length;
267
+ if (specificity !== 0) return specificity;
268
+ return a.oldestQueuedAt.getTime() - b.oldestQueuedAt.getTime();
269
+ });
270
+ }
271
+
272
+ function sortReservationRows<T extends {requiredLabels: string[]}>(rows: T[]): T[] {
273
+ return [...rows].sort((a, b) => {
274
+ const specificity = b.requiredLabels.length - a.requiredLabels.length;
275
+ if (specificity !== 0) return specificity;
276
+ return labelKey(a.requiredLabels).localeCompare(labelKey(b.requiredLabels));
277
+ });
278
+ }
279
+
280
+ function isSubset(requiredLabels: string[], availableLabels: string[]): boolean {
281
+ return requiredLabels.every((label) => availableLabels.includes(label));
282
+ }
283
+
284
+ function drawSlots(templates: NormalizedTemplate[], count: number): void {
285
+ let remaining = count;
286
+ for (const template of templates) {
287
+ if (remaining === 0) return;
288
+ const used = Math.min(template.remainingSlots, remaining);
289
+ template.remainingSlots -= used;
290
+ remaining -= used;
291
+ }
292
+ }
293
+
294
+ function labelKey(labels: string[]): string {
295
+ return JSON.stringify(labels);
296
+ }