@storyshelf/core 0.6.2 → 0.7.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 (628) hide show
  1. package/README.md +49 -32
  2. package/dist/adapter/capture-queue.d.mts +44 -2
  3. package/dist/adapter/capture-runner.d.mts +127 -10
  4. package/dist/adapter/capture-runner.mjs +5 -12
  5. package/dist/adapter/database.d.mts +27 -8
  6. package/dist/adapter/database.mjs.map +1 -1
  7. package/dist/adapter/design.d.mts +85 -0
  8. package/dist/adapter/design.mjs +28 -0
  9. package/dist/adapter/design.mjs.map +1 -0
  10. package/dist/adapter/metadata.d.mts +3 -10
  11. package/dist/adapter/notifier.d.mts +6 -3
  12. package/dist/adapter/notifier.mjs +5 -1
  13. package/dist/adapter/oauth.d.mts +52 -0
  14. package/dist/adapter/setup.d.mts +4 -5
  15. package/dist/adapter/setup.mjs +2 -1
  16. package/dist/adapter/setup.mjs.map +1 -1
  17. package/dist/adapter/webhook-events.mjs +1 -1
  18. package/dist/adapters/capture-target-offers.d.mts +28 -0
  19. package/dist/adapters/capture-target-offers.mjs +58 -0
  20. package/dist/adapters/capture-target-offers.mjs.map +1 -0
  21. package/dist/adapters/capture-target.d.mts +71 -0
  22. package/dist/adapters/capture-target.mjs +76 -0
  23. package/dist/adapters/capture-target.mjs.map +1 -0
  24. package/dist/adapters/notifier/build-comment.d.mts +24 -0
  25. package/dist/adapters/notifier/build-comment.mjs +24 -0
  26. package/dist/adapters/notifier/build-comment.mjs.map +1 -0
  27. package/dist/adapters/notifier/build-state.d.mts +99 -0
  28. package/dist/adapters/notifier/build-state.mjs +218 -0
  29. package/dist/adapters/notifier/build-state.mjs.map +1 -0
  30. package/dist/adapters/notifier/events.d.mts +4 -2
  31. package/dist/adapters/notifier/events.mjs +31 -18
  32. package/dist/adapters/notifier/events.mjs.map +1 -1
  33. package/dist/adapters/notifier/provider.d.mts +53 -7
  34. package/dist/adapters/notifier/provider.mjs +12 -0
  35. package/dist/adapters/notifier/provider.mjs.map +1 -0
  36. package/dist/adapters/notifier/state.d.mts +24 -0
  37. package/dist/adapters/notifier/state.mjs +66 -0
  38. package/dist/adapters/notifier/state.mjs.map +1 -0
  39. package/dist/adapters/notifier/types.d.mts +51 -2
  40. package/dist/adapters/remote-browser.d.mts +87 -0
  41. package/dist/adapters/remote-browser.mjs +103 -0
  42. package/dist/adapters/remote-browser.mjs.map +1 -0
  43. package/dist/adapters/render-runs.d.mts +25 -0
  44. package/dist/adapters/render-runs.mjs +41 -0
  45. package/dist/adapters/render-runs.mjs.map +1 -0
  46. package/dist/adapters/setup-runner.mjs +2 -4
  47. package/dist/adapters/setup-runner.mjs.map +1 -1
  48. package/dist/adapters/sources.d.mts +12 -0
  49. package/dist/adapters/sources.mjs +20 -0
  50. package/dist/adapters/sources.mjs.map +1 -0
  51. package/dist/adapters/validate.mjs +25 -18
  52. package/dist/adapters/validate.mjs.map +1 -1
  53. package/dist/ai.d.mts +105 -0
  54. package/dist/ai.mjs +19 -0
  55. package/dist/ai.mjs.map +1 -0
  56. package/dist/capture/a11y/axe-source.d.mts +18 -0
  57. package/dist/capture/a11y/axe-source.mjs +37 -0
  58. package/dist/capture/a11y/axe-source.mjs.map +1 -0
  59. package/dist/capture/a11y/in-page.d.mts +27 -0
  60. package/dist/capture/a11y/in-page.mjs +60 -0
  61. package/dist/capture/a11y/in-page.mjs.map +1 -0
  62. package/dist/capture/a11y/map.d.mts +24 -0
  63. package/dist/capture/a11y/map.mjs +48 -0
  64. package/dist/capture/a11y/map.mjs.map +1 -0
  65. package/dist/capture/a11y/options.d.mts +23 -0
  66. package/dist/capture/a11y/options.mjs +75 -0
  67. package/dist/capture/a11y/options.mjs.map +1 -0
  68. package/dist/capture/adapter.d.mts +41 -3
  69. package/dist/capture/adapter.mjs.map +1 -1
  70. package/dist/capture/affected.d.mts +2 -0
  71. package/dist/capture/affected.mjs +13 -4
  72. package/dist/capture/affected.mjs.map +1 -1
  73. package/dist/capture/attempt-logger.mjs +84 -0
  74. package/dist/capture/attempt-logger.mjs.map +1 -0
  75. package/dist/capture/browsers.d.mts +35 -0
  76. package/dist/capture/browsers.mjs +67 -0
  77. package/dist/capture/browsers.mjs.map +1 -0
  78. package/dist/capture/build-state/deliver.d.mts +32 -0
  79. package/dist/capture/build-state/deliver.mjs +118 -0
  80. package/dist/capture/build-state/deliver.mjs.map +1 -0
  81. package/dist/capture/build-state/index.d.mts +8 -0
  82. package/dist/capture/build-state/notify-job.d.mts +56 -0
  83. package/dist/capture/build-state/notify-job.mjs +133 -0
  84. package/dist/capture/build-state/notify-job.mjs.map +1 -0
  85. package/dist/capture/build-state/publisher.d.mts +23 -0
  86. package/dist/capture/build-state/publisher.mjs +107 -0
  87. package/dist/capture/build-state/publisher.mjs.map +1 -0
  88. package/dist/capture/build-state/resume.d.mts +9 -0
  89. package/dist/capture/build-state/resume.mjs +47 -0
  90. package/dist/capture/build-state/resume.mjs.map +1 -0
  91. package/dist/capture/build-state/types.d.mts +49 -0
  92. package/dist/capture/catalog/catalogue.d.mts +22 -0
  93. package/dist/capture/catalog/catalogue.mjs +53 -0
  94. package/dist/capture/catalog/catalogue.mjs.map +1 -0
  95. package/dist/capture/catalog/components.d.mts +23 -0
  96. package/dist/capture/catalog/components.mjs +55 -0
  97. package/dist/capture/catalog/components.mjs.map +1 -0
  98. package/dist/capture/catalog/history.d.mts +30 -0
  99. package/dist/capture/catalog/history.mjs +51 -0
  100. package/dist/capture/catalog/history.mjs.map +1 -0
  101. package/dist/capture/catalog/hook.d.mts +14 -0
  102. package/dist/capture/catalog/hook.mjs +19 -0
  103. package/dist/capture/catalog/hook.mjs.map +1 -0
  104. package/dist/capture/catalog/index.d.mts +11 -0
  105. package/dist/capture/catalog/load.d.mts +26 -0
  106. package/dist/capture/catalog/load.mjs +49 -0
  107. package/dist/capture/catalog/load.mjs.map +1 -0
  108. package/dist/capture/catalog/parse.d.mts +15 -0
  109. package/dist/capture/catalog/parse.mjs +56 -0
  110. package/dist/capture/catalog/parse.mjs.map +1 -0
  111. package/dist/capture/catalog/read.d.mts +12 -0
  112. package/dist/capture/catalog/read.mjs +33 -0
  113. package/dist/capture/catalog/read.mjs.map +1 -0
  114. package/dist/capture/catalog/types.d.mts +33 -0
  115. package/dist/capture/consumer/capture-handler.d.mts +58 -0
  116. package/dist/capture/consumer/capture-handler.mjs +118 -0
  117. package/dist/capture/consumer/capture-handler.mjs.map +1 -0
  118. package/dist/capture/consumer/idle.d.mts +17 -0
  119. package/dist/capture/consumer/idle.mjs +25 -0
  120. package/dist/capture/consumer/idle.mjs.map +1 -0
  121. package/dist/capture/consumer/index.d.mts +13 -0
  122. package/dist/capture/consumer/index.mjs +139 -0
  123. package/dist/capture/consumer/index.mjs.map +1 -0
  124. package/dist/capture/consumer/loop.mjs +110 -0
  125. package/dist/capture/consumer/loop.mjs.map +1 -0
  126. package/dist/capture/consumer/metrics.d.mts +41 -0
  127. package/dist/capture/consumer/metrics.mjs +38 -0
  128. package/dist/capture/consumer/metrics.mjs.map +1 -0
  129. package/dist/capture/consumer/retry.d.mts +33 -0
  130. package/dist/capture/consumer/retry.mjs +34 -0
  131. package/dist/capture/consumer/retry.mjs.map +1 -0
  132. package/dist/capture/consumer/run-job.mjs +195 -0
  133. package/dist/capture/consumer/run-job.mjs.map +1 -0
  134. package/dist/capture/consumer/startup.mjs +38 -0
  135. package/dist/capture/consumer/startup.mjs.map +1 -0
  136. package/dist/capture/consumer/types.d.mts +97 -0
  137. package/dist/capture/crash-failure.d.mts +25 -0
  138. package/dist/capture/crash-failure.mjs +70 -0
  139. package/dist/capture/crash-failure.mjs.map +1 -0
  140. package/dist/capture/db-queue/cadence.d.mts +32 -0
  141. package/dist/capture/db-queue/cadence.mjs +38 -0
  142. package/dist/capture/db-queue/cadence.mjs.map +1 -0
  143. package/dist/capture/db-queue/claim.d.mts +9 -0
  144. package/dist/capture/db-queue/claim.mjs +132 -0
  145. package/dist/capture/db-queue/claim.mjs.map +1 -0
  146. package/dist/capture/db-queue/clock.mjs +12 -0
  147. package/dist/capture/db-queue/clock.mjs.map +1 -0
  148. package/dist/capture/db-queue/codec.mjs +76 -0
  149. package/dist/capture/db-queue/codec.mjs.map +1 -0
  150. package/dist/capture/db-queue/index.d.mts +20 -0
  151. package/dist/capture/db-queue/index.mjs +89 -0
  152. package/dist/capture/db-queue/index.mjs.map +1 -0
  153. package/dist/capture/db-queue/keeper.d.mts +36 -0
  154. package/dist/capture/db-queue/keeper.mjs +92 -0
  155. package/dist/capture/db-queue/keeper.mjs.map +1 -0
  156. package/dist/capture/db-queue/lease.mjs +46 -0
  157. package/dist/capture/db-queue/lease.mjs.map +1 -0
  158. package/dist/capture/db-queue/lifecycle.mjs +36 -0
  159. package/dist/capture/db-queue/lifecycle.mjs.map +1 -0
  160. package/dist/capture/db-queue/operations.mjs +94 -0
  161. package/dist/capture/db-queue/operations.mjs.map +1 -0
  162. package/dist/capture/db-queue/poll-loop.d.mts +30 -0
  163. package/dist/capture/db-queue/poll-loop.mjs +89 -0
  164. package/dist/capture/db-queue/poll-loop.mjs.map +1 -0
  165. package/dist/capture/db-queue/poll.d.mts +10 -0
  166. package/dist/capture/db-queue/poll.mjs +69 -0
  167. package/dist/capture/db-queue/poll.mjs.map +1 -0
  168. package/dist/capture/db-queue/timers.d.mts +12 -0
  169. package/dist/capture/db-queue/timers.mjs +37 -0
  170. package/dist/capture/db-queue/timers.mjs.map +1 -0
  171. package/dist/capture/db-queue/types.d.mts +83 -0
  172. package/dist/capture/db-queue/types.mjs +12 -0
  173. package/dist/capture/db-queue/types.mjs.map +1 -0
  174. package/dist/capture/db-queue/wake.mjs +48 -0
  175. package/dist/capture/db-queue/wake.mjs.map +1 -0
  176. package/dist/capture/design.mjs +39 -0
  177. package/dist/capture/design.mjs.map +1 -0
  178. package/dist/capture/diff-record.mjs +23 -3
  179. package/dist/capture/diff-record.mjs.map +1 -1
  180. package/dist/capture/dispatch.d.mts +12 -7
  181. package/dist/capture/dispatch.mjs +86 -107
  182. package/dist/capture/dispatch.mjs.map +1 -1
  183. package/dist/capture/enrich.mjs +60 -0
  184. package/dist/capture/enrich.mjs.map +1 -0
  185. package/dist/capture/failure-sets.mjs +22 -0
  186. package/dist/capture/failure-sets.mjs.map +1 -0
  187. package/dist/capture/fingerprint.d.mts +42 -0
  188. package/dist/capture/fingerprint.mjs +83 -0
  189. package/dist/capture/fingerprint.mjs.map +1 -0
  190. package/dist/capture/forked/channel.d.mts +25 -0
  191. package/dist/capture/forked/child.d.mts +34 -0
  192. package/dist/capture/forked/child.mjs +202 -0
  193. package/dist/capture/forked/child.mjs.map +1 -0
  194. package/dist/capture/forked/crash-tracker.mjs +23 -0
  195. package/dist/capture/forked/crash-tracker.mjs.map +1 -0
  196. package/dist/capture/forked/decision.d.mts +26 -0
  197. package/dist/capture/forked/decision.mjs +27 -0
  198. package/dist/capture/forked/decision.mjs.map +1 -0
  199. package/dist/capture/forked/exit.mjs +12 -0
  200. package/dist/capture/forked/exit.mjs.map +1 -0
  201. package/dist/capture/forked/fork.d.mts +31 -0
  202. package/dist/capture/forked/fork.mjs +93 -0
  203. package/dist/capture/forked/fork.mjs.map +1 -0
  204. package/dist/capture/forked/forked-capture.d.mts +40 -0
  205. package/dist/capture/forked/forked-capture.mjs +240 -0
  206. package/dist/capture/forked/forked-capture.mjs.map +1 -0
  207. package/dist/capture/forked/lease-book.mjs +46 -0
  208. package/dist/capture/forked/lease-book.mjs.map +1 -0
  209. package/dist/capture/forked/protocol.d.mts +38 -0
  210. package/dist/capture/forked/protocol.mjs +25 -0
  211. package/dist/capture/forked/protocol.mjs.map +1 -0
  212. package/dist/capture/forked/runs.mjs +75 -0
  213. package/dist/capture/forked/runs.mjs.map +1 -0
  214. package/dist/capture/isolate.mjs +141 -0
  215. package/dist/capture/isolate.mjs.map +1 -0
  216. package/dist/capture/job-lease.d.mts +28 -0
  217. package/dist/capture/job-lease.mjs +57 -0
  218. package/dist/capture/job-lease.mjs.map +1 -0
  219. package/dist/capture/matrix.mjs +298 -0
  220. package/dist/capture/matrix.mjs.map +1 -0
  221. package/dist/capture/modes.d.mts +25 -0
  222. package/dist/capture/modes.mjs +107 -0
  223. package/dist/capture/modes.mjs.map +1 -0
  224. package/dist/capture/orchestrator.d.mts +37 -5
  225. package/dist/capture/orchestrator.mjs +118 -107
  226. package/dist/capture/orchestrator.mjs.map +1 -1
  227. package/dist/capture/pipeline.d.mts +9 -2
  228. package/dist/capture/pipeline.mjs +250 -45
  229. package/dist/capture/pipeline.mjs.map +1 -1
  230. package/dist/capture/progress.d.mts +11 -0
  231. package/dist/capture/progress.mjs +28 -0
  232. package/dist/capture/progress.mjs.map +1 -0
  233. package/dist/capture/publish/accept.d.mts +39 -0
  234. package/dist/capture/publish/accept.mjs +28 -0
  235. package/dist/capture/publish/accept.mjs.map +1 -0
  236. package/dist/capture/publish/complete.d.mts +46 -0
  237. package/dist/capture/publish/complete.mjs +64 -0
  238. package/dist/capture/publish/complete.mjs.map +1 -0
  239. package/dist/capture/publish/extract.d.mts +20 -0
  240. package/dist/capture/publish/extract.mjs +119 -0
  241. package/dist/capture/publish/extract.mjs.map +1 -0
  242. package/dist/capture/publish/handler.d.mts +28 -0
  243. package/dist/capture/publish/handler.mjs +62 -0
  244. package/dist/capture/publish/handler.mjs.map +1 -0
  245. package/dist/capture/publish/index.d.mts +10 -0
  246. package/dist/capture/publish/limits.d.mts +37 -0
  247. package/dist/capture/publish/limits.mjs +39 -0
  248. package/dist/capture/publish/limits.mjs.map +1 -0
  249. package/dist/capture/publish/published.d.mts +15 -0
  250. package/dist/capture/publish/published.mjs +18 -0
  251. package/dist/capture/publish/published.mjs.map +1 -0
  252. package/dist/capture/publish/run.d.mts +33 -0
  253. package/dist/capture/publish/run.mjs +30 -0
  254. package/dist/capture/publish/run.mjs.map +1 -0
  255. package/dist/capture/rediff.d.mts +1 -1
  256. package/dist/capture/rediff.mjs +4 -3
  257. package/dist/capture/rediff.mjs.map +1 -1
  258. package/dist/capture/render.mjs +53 -0
  259. package/dist/capture/render.mjs.map +1 -0
  260. package/dist/capture/scratch.d.mts +44 -0
  261. package/dist/capture/scratch.mjs +138 -0
  262. package/dist/capture/scratch.mjs.map +1 -0
  263. package/dist/capture/shard-plan.mjs +128 -0
  264. package/dist/capture/shard-plan.mjs.map +1 -0
  265. package/dist/capture/shards/context.mjs +51 -0
  266. package/dist/capture/shards/context.mjs.map +1 -0
  267. package/dist/capture/shards/execute.mjs +174 -0
  268. package/dist/capture/shards/execute.mjs.map +1 -0
  269. package/dist/capture/shards/fan-out.mjs +126 -0
  270. package/dist/capture/shards/fan-out.mjs.map +1 -0
  271. package/dist/capture/shards/history.mjs +35 -0
  272. package/dist/capture/shards/history.mjs.map +1 -0
  273. package/dist/capture/shards/reduce.mjs +97 -0
  274. package/dist/capture/shards/reduce.mjs.map +1 -0
  275. package/dist/capture/sizing.mjs +1 -11
  276. package/dist/capture/sizing.mjs.map +1 -1
  277. package/dist/capture/skip-checks.mjs +2 -65
  278. package/dist/capture/skip-checks.mjs.map +1 -1
  279. package/dist/capture/statics.d.mts +7 -8
  280. package/dist/capture/statics.mjs +25 -71
  281. package/dist/capture/statics.mjs.map +1 -1
  282. package/dist/capture/storybook-urls.d.mts +40 -0
  283. package/dist/capture/storybook-urls.mjs +41 -0
  284. package/dist/capture/storybook-urls.mjs.map +1 -0
  285. package/dist/capture/storybook.d.mts +12 -4
  286. package/dist/capture/storybook.mjs +56 -16
  287. package/dist/capture/storybook.mjs.map +1 -1
  288. package/dist/capture/target.mjs +21 -0
  289. package/dist/capture/target.mjs.map +1 -0
  290. package/dist/capture/targets.d.mts +25 -0
  291. package/dist/capture/targets.mjs +32 -0
  292. package/dist/capture/targets.mjs.map +1 -0
  293. package/dist/capture/tests.d.mts +37 -0
  294. package/dist/capture/tests.mjs +126 -0
  295. package/dist/capture/tests.mjs.map +1 -0
  296. package/dist/capture/token-chunks.mjs +74 -0
  297. package/dist/capture/token-chunks.mjs.map +1 -0
  298. package/dist/capture.d.mts +60 -7
  299. package/dist/capture.mjs +48 -3
  300. package/dist/config.d.mts +121 -12
  301. package/dist/config.mjs +9 -6
  302. package/dist/config.mjs.map +1 -1
  303. package/dist/diff.d.mts +9 -3
  304. package/dist/diff.mjs +16 -3
  305. package/dist/diff.mjs.map +1 -0
  306. package/dist/index.d.mts +1 -1
  307. package/dist/insights/budget.d.mts +20 -0
  308. package/dist/insights/budget.mjs +33 -0
  309. package/dist/insights/budget.mjs.map +1 -0
  310. package/dist/insights/envelope.d.mts +59 -0
  311. package/dist/insights/envelope.mjs +60 -0
  312. package/dist/insights/envelope.mjs.map +1 -0
  313. package/dist/insights/evidence-health.d.mts +32 -0
  314. package/dist/insights/evidence-health.mjs +28 -0
  315. package/dist/insights/evidence-health.mjs.map +1 -0
  316. package/dist/insights/evidence.d.mts +55 -0
  317. package/dist/insights/evidence.mjs +64 -0
  318. package/dist/insights/evidence.mjs.map +1 -0
  319. package/dist/insights/hash.d.mts +18 -0
  320. package/dist/insights/hash.mjs +21 -0
  321. package/dist/insights/hash.mjs.map +1 -0
  322. package/dist/insights/limits.d.mts +24 -0
  323. package/dist/insights/limits.mjs +24 -0
  324. package/dist/insights/limits.mjs.map +1 -0
  325. package/dist/insights/prompts/v1.d.mts +13 -0
  326. package/dist/insights/prompts/v1.mjs +34 -0
  327. package/dist/insights/prompts/v1.mjs.map +1 -0
  328. package/dist/insights/redact.d.mts +9 -0
  329. package/dist/insights/redact.mjs +24 -0
  330. package/dist/insights/redact.mjs.map +1 -0
  331. package/dist/insights/text.mjs +44 -0
  332. package/dist/insights/text.mjs.map +1 -0
  333. package/dist/insights.d.mts +11 -0
  334. package/dist/insights.mjs +12 -0
  335. package/dist/logger.mjs +17 -1
  336. package/dist/logger.mjs.map +1 -1
  337. package/dist/models/ai-budget-alert.d.mts +23 -0
  338. package/dist/models/ai-budget-alert.mjs +38 -0
  339. package/dist/models/ai-budget-alert.mjs.map +1 -0
  340. package/dist/models/ai-usage.d.mts +40 -0
  341. package/dist/models/ai-usage.mjs +61 -0
  342. package/dist/models/ai-usage.mjs.map +1 -0
  343. package/dist/models/baseline-archive.d.mts +84 -0
  344. package/dist/models/baseline-archive.mjs +106 -0
  345. package/dist/models/baseline-archive.mjs.map +1 -0
  346. package/dist/models/baseline-export.d.mts +19 -0
  347. package/dist/models/baseline-export.mjs +136 -0
  348. package/dist/models/baseline-export.mjs.map +1 -0
  349. package/dist/models/baseline-import.mjs +118 -0
  350. package/dist/models/baseline-import.mjs.map +1 -0
  351. package/dist/models/baseline-transfer.d.mts +49 -0
  352. package/dist/models/baseline-transfer.mjs +121 -0
  353. package/dist/models/baseline-transfer.mjs.map +1 -0
  354. package/dist/models/baseline.d.mts +32 -5
  355. package/dist/models/baseline.mjs +65 -20
  356. package/dist/models/baseline.mjs.map +1 -1
  357. package/dist/models/build-component.d.mts +28 -0
  358. package/dist/models/build-component.mjs +55 -0
  359. package/dist/models/build-component.mjs.map +1 -0
  360. package/dist/models/build-review-status.d.mts +29 -0
  361. package/dist/models/build-review-status.mjs +37 -0
  362. package/dist/models/build-review-status.mjs.map +1 -0
  363. package/dist/models/build-review.d.mts +28 -0
  364. package/dist/models/build-review.mjs +54 -0
  365. package/dist/models/build-review.mjs.map +1 -0
  366. package/dist/models/build.d.mts +25 -3
  367. package/dist/models/build.mjs +50 -11
  368. package/dist/models/build.mjs.map +1 -1
  369. package/dist/models/capture-attempt.d.mts +7 -1
  370. package/dist/models/capture-attempt.mjs +29 -4
  371. package/dist/models/capture-attempt.mjs.map +1 -1
  372. package/dist/models/capture-job.d.mts +106 -0
  373. package/dist/models/capture-job.mjs +288 -0
  374. package/dist/models/capture-job.mjs.map +1 -0
  375. package/dist/models/capture-log.d.mts +1 -1
  376. package/dist/models/capture-log.mjs +4 -3
  377. package/dist/models/capture-log.mjs.map +1 -1
  378. package/dist/models/capture-shard.d.mts +62 -0
  379. package/dist/models/capture-shard.mjs +190 -0
  380. package/dist/models/capture-shard.mjs.map +1 -0
  381. package/dist/models/changeset.d.mts +48 -0
  382. package/dist/models/changeset.mjs +81 -0
  383. package/dist/models/changeset.mjs.map +1 -0
  384. package/dist/models/columns.mjs +19 -0
  385. package/dist/models/columns.mjs.map +1 -0
  386. package/dist/models/comment.d.mts +4 -1
  387. package/dist/models/comment.mjs +7 -4
  388. package/dist/models/comment.mjs.map +1 -1
  389. package/dist/models/component.d.mts +43 -0
  390. package/dist/models/component.mjs +60 -0
  391. package/dist/models/component.mjs.map +1 -0
  392. package/dist/models/connections.d.mts +41 -0
  393. package/dist/models/connections.mjs +112 -0
  394. package/dist/models/connections.mjs.map +1 -0
  395. package/dist/models/content-ref.d.mts +1 -1
  396. package/dist/models/content-ref.mjs +1 -1
  397. package/dist/models/content-ref.mjs.map +1 -1
  398. package/dist/models/credentials.mjs +12 -2
  399. package/dist/models/credentials.mjs.map +1 -1
  400. package/dist/models/design-credentials.d.mts +41 -0
  401. package/dist/models/design-credentials.mjs +86 -0
  402. package/dist/models/design-credentials.mjs.map +1 -0
  403. package/dist/models/design-health.d.mts +16 -0
  404. package/dist/models/design-health.mjs +25 -0
  405. package/dist/models/design-health.mjs.map +1 -0
  406. package/dist/models/design-nodes.d.mts +83 -0
  407. package/dist/models/design-nodes.mjs +222 -0
  408. package/dist/models/design-nodes.mjs.map +1 -0
  409. package/dist/models/design-renders.mjs +74 -0
  410. package/dist/models/design-renders.mjs.map +1 -0
  411. package/dist/models/design-resolve.d.mts +29 -0
  412. package/dist/models/design-resolve.mjs +41 -0
  413. package/dist/models/design-resolve.mjs.map +1 -0
  414. package/dist/models/design.d.mts +39 -0
  415. package/dist/models/design.mjs +90 -0
  416. package/dist/models/design.mjs.map +1 -0
  417. package/dist/models/inherited-ratio.d.mts +21 -0
  418. package/dist/models/inherited-ratio.mjs +22 -0
  419. package/dist/models/inherited-ratio.mjs.map +1 -0
  420. package/dist/models/insight.d.mts +70 -0
  421. package/dist/models/insight.mjs +162 -0
  422. package/dist/models/insight.mjs.map +1 -0
  423. package/dist/models/label.d.mts +4 -2
  424. package/dist/models/label.mjs +25 -10
  425. package/dist/models/label.mjs.map +1 -1
  426. package/dist/models/member.d.mts +1 -1
  427. package/dist/models/member.mjs +5 -4
  428. package/dist/models/member.mjs.map +1 -1
  429. package/dist/models/notification-channel.d.mts +37 -3
  430. package/dist/models/notification-channel.mjs +88 -9
  431. package/dist/models/notification-channel.mjs.map +1 -1
  432. package/dist/models/notification-subscription.d.mts +1 -2
  433. package/dist/models/notification-subscription.mjs +7 -9
  434. package/dist/models/notification-subscription.mjs.map +1 -1
  435. package/dist/models/project-group-mapping.d.mts +6 -1
  436. package/dist/models/project-group-mapping.mjs +14 -4
  437. package/dist/models/project-group-mapping.mjs.map +1 -1
  438. package/dist/models/project.d.mts +9 -1
  439. package/dist/models/project.mjs +9 -4
  440. package/dist/models/project.mjs.map +1 -1
  441. package/dist/models/public-origin.d.mts +63 -0
  442. package/dist/models/public-origin.mjs +114 -0
  443. package/dist/models/public-origin.mjs.map +1 -0
  444. package/dist/models/review-switch-off.d.mts +14 -0
  445. package/dist/models/review-switch-off.mjs +26 -0
  446. package/dist/models/review-switch-off.mjs.map +1 -0
  447. package/dist/models/site-settings.d.mts +23 -0
  448. package/dist/models/site-settings.mjs +48 -0
  449. package/dist/models/site-settings.mjs.map +1 -0
  450. package/dist/models/snapshot.d.mts +36 -1
  451. package/dist/models/snapshot.mjs +50 -3
  452. package/dist/models/snapshot.mjs.map +1 -1
  453. package/dist/models/token.d.mts +13 -1
  454. package/dist/models/token.mjs +23 -5
  455. package/dist/models/token.mjs.map +1 -1
  456. package/dist/models/usage.d.mts +76 -0
  457. package/dist/models/usage.mjs +149 -0
  458. package/dist/models/usage.mjs.map +1 -0
  459. package/dist/models/user.d.mts +1 -1
  460. package/dist/models/user.mjs +4 -3
  461. package/dist/models/user.mjs.map +1 -1
  462. package/dist/models/webhook.d.mts +15 -1
  463. package/dist/models/webhook.mjs +33 -4
  464. package/dist/models/webhook.mjs.map +1 -1
  465. package/dist/models.d.mts +26 -3
  466. package/dist/models.mjs +27 -5
  467. package/dist/options.d.mts +18 -0
  468. package/dist/options.mjs +90 -0
  469. package/dist/options.mjs.map +1 -0
  470. package/dist/paths.d.mts +19 -4
  471. package/dist/paths.mjs +51 -10
  472. package/dist/paths.mjs.map +1 -1
  473. package/dist/retention/ai.d.mts +18 -0
  474. package/dist/retention/ai.mjs +23 -0
  475. package/dist/retention/ai.mjs.map +1 -0
  476. package/dist/retention/pending.mjs +47 -0
  477. package/dist/retention/pending.mjs.map +1 -0
  478. package/dist/retention/screenshot-sweep.d.mts +51 -0
  479. package/dist/retention/screenshot-sweep.mjs +128 -0
  480. package/dist/retention/screenshot-sweep.mjs.map +1 -0
  481. package/dist/retention.d.mts +16 -2
  482. package/dist/retention.mjs +71 -14
  483. package/dist/retention.mjs.map +1 -1
  484. package/dist/schema/baseline.d.mts +6 -0
  485. package/dist/schema/build-component.d.mts +25 -0
  486. package/dist/schema/build-review.d.mts +17 -0
  487. package/dist/schema/build.d.mts +7 -0
  488. package/dist/schema/capture-job.d.mts +60 -0
  489. package/dist/schema/capture-shard.d.mts +35 -0
  490. package/dist/schema/comment.d.mts +2 -0
  491. package/dist/schema/connection.d.mts +24 -0
  492. package/dist/schema/design.d.mts +58 -0
  493. package/dist/schema/insight.d.mts +54 -0
  494. package/dist/schema/notification.d.mts +4 -0
  495. package/dist/schema/project.d.mts +21 -0
  496. package/dist/schema/site-setting.d.mts +21 -0
  497. package/dist/schema/snapshot.d.mts +12 -0
  498. package/dist/schema/usage.d.mts +30 -0
  499. package/dist/schema.d.mts +11 -3
  500. package/dist/serverless.d.mts +21 -0
  501. package/dist/serverless.mjs +37 -0
  502. package/dist/serverless.mjs.map +1 -0
  503. package/dist/test-helpers/create-project.d.mts +1 -1
  504. package/dist/test-helpers/deferred.d.mts +39 -0
  505. package/dist/test-helpers/deferred.mjs +61 -0
  506. package/dist/test-helpers/deferred.mjs.map +1 -0
  507. package/dist/test-helpers/design-contract.d.mts +21 -0
  508. package/dist/test-helpers/design-contract.mjs +40 -0
  509. package/dist/test-helpers/design-contract.mjs.map +1 -0
  510. package/dist/test-helpers/fake-database.mjs +14 -2
  511. package/dist/test-helpers/fake-database.mjs.map +1 -1
  512. package/dist/test-helpers/fake-design.d.mts +43 -0
  513. package/dist/test-helpers/fake-design.mjs +114 -0
  514. package/dist/test-helpers/fake-design.mjs.map +1 -0
  515. package/dist/test-helpers/fake-notifier.d.mts +21 -0
  516. package/dist/test-helpers/fake-notifier.mjs +44 -0
  517. package/dist/test-helpers/fake-notifier.mjs.map +1 -0
  518. package/dist/test-helpers/fake-tables.mjs +255 -67
  519. package/dist/test-helpers/fake-tables.mjs.map +1 -1
  520. package/dist/test-helpers/manual-clock.d.mts +27 -0
  521. package/dist/test-helpers/manual-clock.mjs +56 -0
  522. package/dist/test-helpers/manual-clock.mjs.map +1 -0
  523. package/dist/test-helpers/notifier-contract.d.mts +7 -1
  524. package/dist/test-helpers/notifier-contract.mjs +32 -3
  525. package/dist/test-helpers/notifier-contract.mjs.map +1 -1
  526. package/dist/test-helpers/queue-contract.d.mts +16 -2
  527. package/dist/test-helpers/queue-contract.mjs +195 -1
  528. package/dist/test-helpers/queue-contract.mjs.map +1 -1
  529. package/dist/test-helpers/queue-multi-instance.d.mts +20 -0
  530. package/dist/test-helpers/queue-multi-instance.mjs +206 -0
  531. package/dist/test-helpers/queue-multi-instance.mjs.map +1 -0
  532. package/dist/test-helpers/runner-contract.mjs +11 -0
  533. package/dist/test-helpers/runner-contract.mjs.map +1 -1
  534. package/dist/test-helpers/sql-chunks.mjs +23 -0
  535. package/dist/test-helpers/sql-chunks.mjs.map +1 -1
  536. package/dist/test-helpers/storage-contract.mjs +1 -1
  537. package/dist/test-helpers.d.mts +9 -4
  538. package/dist/test-helpers.mjs +8 -3
  539. package/dist/types.d.mts +14 -3
  540. package/dist/types.mjs +21 -4
  541. package/dist/types.mjs.map +1 -1
  542. package/dist/urls.d.mts +4 -0
  543. package/dist/urls.mjs +11 -2
  544. package/dist/urls.mjs.map +1 -1
  545. package/dist/utils/adaptive-concurrency.d.mts +66 -0
  546. package/dist/utils/adaptive-concurrency.mjs +149 -0
  547. package/dist/utils/adaptive-concurrency.mjs.map +1 -0
  548. package/dist/utils/connect-retry.d.mts +26 -0
  549. package/dist/utils/connect-retry.mjs +43 -0
  550. package/dist/utils/connect-retry.mjs.map +1 -0
  551. package/dist/utils/environment.d.mts +19 -0
  552. package/dist/utils/environment.mjs +59 -0
  553. package/dist/utils/environment.mjs.map +1 -0
  554. package/dist/utils/http.d.mts +17 -1
  555. package/dist/utils/http.mjs +22 -3
  556. package/dist/utils/http.mjs.map +1 -1
  557. package/dist/utils/map-adaptive.d.mts +15 -0
  558. package/dist/utils/map-adaptive.mjs +72 -0
  559. package/dist/utils/map-adaptive.mjs.map +1 -0
  560. package/dist/utils/map-limit.d.mts +10 -0
  561. package/dist/utils/map-limit.mjs +32 -0
  562. package/dist/utils/map-limit.mjs.map +1 -0
  563. package/dist/utils/page-limiter.d.mts +37 -0
  564. package/dist/utils/page-limiter.mjs +56 -0
  565. package/dist/utils/page-limiter.mjs.map +1 -0
  566. package/dist/utils/public-origin.d.mts +20 -0
  567. package/dist/utils/public-origin.mjs +75 -0
  568. package/dist/utils/public-origin.mjs.map +1 -0
  569. package/dist/utils/redact.mjs +3 -0
  570. package/dist/utils/redact.mjs.map +1 -1
  571. package/dist/utils/remote-environment.d.mts +39 -0
  572. package/dist/utils/remote-environment.mjs +114 -0
  573. package/dist/utils/remote-environment.mjs.map +1 -0
  574. package/dist/utils/resources.d.mts +64 -0
  575. package/dist/utils/resources.mjs +122 -0
  576. package/dist/utils/resources.mjs.map +1 -0
  577. package/dist/utils/screenshot-store.d.mts +15 -0
  578. package/dist/utils/screenshot-store.mjs +41 -0
  579. package/dist/utils/screenshot-store.mjs.map +1 -0
  580. package/dist/utils/session-gate.d.mts +22 -0
  581. package/dist/utils/session-gate.mjs +53 -0
  582. package/dist/utils/session-gate.mjs.map +1 -0
  583. package/dist/utils/storybook-token.d.mts +81 -0
  584. package/dist/utils/storybook-token.mjs +134 -0
  585. package/dist/utils/storybook-token.mjs.map +1 -0
  586. package/dist/utils/storybook-url.d.mts +15 -0
  587. package/dist/utils/storybook-url.mjs +21 -0
  588. package/dist/utils/storybook-url.mjs.map +1 -0
  589. package/dist/utils/timing.mjs +1 -1
  590. package/dist/utils/timing.mjs.map +1 -1
  591. package/dist/utils/zip-read.mjs +129 -0
  592. package/dist/utils/zip-read.mjs.map +1 -0
  593. package/dist/utils/zip-write.d.mts +18 -0
  594. package/dist/utils/zip-write.mjs +188 -0
  595. package/dist/utils/zip-write.mjs.map +1 -0
  596. package/dist/utils.d.mts +17 -3
  597. package/dist/utils.mjs +18 -4
  598. package/package.json +32 -23
  599. package/dist/adapter/capture-runner.mjs.map +0 -1
  600. package/dist/adapter/git-host/comments.d.mts +0 -41
  601. package/dist/adapter/git-host/comments.mjs +0 -54
  602. package/dist/adapter/git-host/comments.mjs.map +0 -1
  603. package/dist/adapter/git-host/helpers.d.mts +0 -13
  604. package/dist/adapter/git-host/helpers.mjs +0 -30
  605. package/dist/adapter/git-host/helpers.mjs.map +0 -1
  606. package/dist/adapter/git-host.d.mts +0 -42
  607. package/dist/capture/queue.d.mts +0 -38
  608. package/dist/capture/queue.mjs +0 -98
  609. package/dist/capture/queue.mjs.map +0 -1
  610. package/dist/capture/recovery.d.mts +0 -21
  611. package/dist/capture/recovery.mjs +0 -83
  612. package/dist/capture/recovery.mjs.map +0 -1
  613. package/dist/capture/status-fanout.mjs +0 -72
  614. package/dist/capture/status-fanout.mjs.map +0 -1
  615. package/dist/diff/engine.d.mts +0 -16
  616. package/dist/diff/engine.mjs +0 -87
  617. package/dist/diff/engine.mjs.map +0 -1
  618. package/dist/diff/options.d.mts +0 -42
  619. package/dist/diff/options.mjs +0 -15
  620. package/dist/diff/options.mjs.map +0 -1
  621. package/dist/models/status-config.d.mts +0 -38
  622. package/dist/models/status-config.mjs +0 -63
  623. package/dist/models/status-config.mjs.map +0 -1
  624. package/dist/schema/status-config.d.mts +0 -16
  625. package/dist/test-helpers/git-contract.d.mts +0 -11
  626. package/dist/test-helpers/git-contract.mjs +0 -34
  627. package/dist/test-helpers/git-contract.mjs.map +0 -1
  628. /package/dist/adapter/{git-host.mjs → oauth.mjs} +0 -0
@@ -0,0 +1,288 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { col } from "./columns.mjs";
5
+ import { and, asc, desc, eq, inArray, lt, lte } from "drizzle-orm";
6
+ //#region src/models/capture-job.ts
7
+ /** Rows of the database queue (`capture_jobs`): job rows, per-attempt claim rows, and their views. */
8
+ /** Job statuses that end a job: nothing claims it again. */
9
+ const TERMINAL_JOB_STATUSES = [
10
+ "done",
11
+ "failed",
12
+ "cancelled"
13
+ ];
14
+ /** Statuses of rows the poll may claim: a queued job, or a running attempt whose lease ran out. */
15
+ const CANDIDATE_STATUSES = ["queued", "running"];
16
+ /**
17
+ * Data operations of the database queue. Every write is an unconditional update by id or an
18
+ * insert; the only atomic primitive is the unique-key insert of an attempt row ({@link claim}),
19
+ * which is what fences concurrent claimers on every supported database.
20
+ */
21
+ var CaptureJobModel = class {
22
+ #db;
23
+ #table;
24
+ #c;
25
+ constructor(db, tables) {
26
+ this.#db = db;
27
+ this.#table = (tables ?? { captureJobs: db.tables.captureJobs }).captureJobs;
28
+ this.#c = jobColumns(this.#table);
29
+ }
30
+ async get(id) {
31
+ return await this.#db.get(this.#table, id);
32
+ }
33
+ /** Insert a queued job row. */
34
+ async create(input, now) {
35
+ return await this.#db.insert(this.#table, jobRow(input, now));
36
+ }
37
+ /**
38
+ * The poll probe: the oldest row of `kinds` that may be claimed at `now`. One indexed range query
39
+ * on `(kind, visible_at, status)`, limit 1; only candidates have a `visibleAt`, so an empty queue
40
+ * reads no rows and a busy one reads one.
41
+ */
42
+ async nextCandidate(kinds, now) {
43
+ return (await this.#db.list(this.#table, {
44
+ where: and(kindFilter(this.#c, kinds), lte(this.#c.visibleAt, now), inArray(this.#c.status, CANDIDATE_STATUSES)),
45
+ orderBy: asc(this.#c.visibleAt),
46
+ limit: 1
47
+ }))[0] ?? null;
48
+ }
49
+ /**
50
+ * Claim attempt `attempt` of a job by inserting its row `<jobId>:<attempt>`. Exactly one caller
51
+ * succeeds on every database; the others hit the primary key, which this reports as `false`
52
+ * (told apart from real failures by re-reading the row, as `CaptureShardModel.claimFinalize`).
53
+ */
54
+ async claim(input, now) {
55
+ const id = attemptRowId(input.job.jobId, input.attempt);
56
+ try {
57
+ await this.#db.insert(this.#table, attemptRow(input, now));
58
+ return true;
59
+ } catch (error) {
60
+ if (await this.get(id) === null) throw error;
61
+ return false;
62
+ }
63
+ }
64
+ /** The claim won: the job row records the running attempt and leaves the poll's index range. */
65
+ async markRunning(jobId, attempt, now) {
66
+ await this.#db.update(this.#table, jobId, {
67
+ status: "running",
68
+ attempt,
69
+ visibleAt: null,
70
+ startedAt: now,
71
+ updatedAt: now
72
+ });
73
+ }
74
+ /** Push an attempt's lease out. Returns the row (null if gone) so the caller sees cancel or expiry. */
75
+ async renew(attemptId, leaseUntil, now) {
76
+ return await this.#db.update(this.#table, attemptId, {
77
+ visibleAt: leaseUntil,
78
+ updatedAt: now
79
+ }) ?? null;
80
+ }
81
+ /** The attempt row `attemptId` if it is running with a lease that passed by `now`, else null. */
82
+ async expiredAttempt(attemptId, now) {
83
+ return (await this.#db.list(this.#table, {
84
+ where: and(eq(this.#c.id, attemptId), eq(this.#c.status, "running"), lte(this.#c.visibleAt, now)),
85
+ limit: 1
86
+ }))[0] ?? null;
87
+ }
88
+ /** Move a row's visibility time (and nothing else). */
89
+ async defer(id, visibleAt, now) {
90
+ await this.#db.update(this.#table, id, {
91
+ visibleAt,
92
+ updatedAt: now
93
+ });
94
+ }
95
+ /** Take a row out of the poll's index range without changing its status. */
96
+ async clearVisibility(id, now) {
97
+ await this.#db.update(this.#table, id, {
98
+ visibleAt: null,
99
+ updatedAt: now
100
+ });
101
+ }
102
+ /** End an attempt row: `done`, `failed` (with its error) or `expired` (taken over). */
103
+ async finishAttempt(attemptId, status, now, error = null) {
104
+ await this.#db.update(this.#table, attemptId, {
105
+ status,
106
+ visibleAt: null,
107
+ error,
108
+ finishedAt: now,
109
+ updatedAt: now
110
+ });
111
+ }
112
+ /** End a job row for good. */
113
+ async finishJob(jobId, status, now, error = null) {
114
+ const patch = {
115
+ status,
116
+ visibleAt: null,
117
+ finishedAt: now,
118
+ updatedAt: now
119
+ };
120
+ await this.#db.update(this.#table, jobId, error === null ? patch : {
121
+ ...patch,
122
+ error
123
+ });
124
+ }
125
+ /** Put a job back on the queue, claimable from `visibleAt`, remembering why the attempt failed. */
126
+ async requeue(jobId, visibleAt, error, now) {
127
+ await this.#db.update(this.#table, jobId, {
128
+ status: "queued",
129
+ visibleAt,
130
+ error,
131
+ updatedAt: now
132
+ });
133
+ }
134
+ /** Flag a row as cancel-requested; the flag is never cleared. */
135
+ async flagCancel(id, now) {
136
+ await this.#db.update(this.#table, id, {
137
+ cancelRequestedAt: now,
138
+ updatedAt: now
139
+ });
140
+ }
141
+ /** Job rows of a build (its own job and its shard jobs), oldest first. */
142
+ async listForBuild(buildId) {
143
+ return (await this.#db.list(this.#table, { where: and(eq(this.#c.buildId, buildId), jobEntries(this.#c)) })).toSorted(byCreation);
144
+ }
145
+ /** The build's own (non-shard) job of `kinds` most recently queued, or null. */
146
+ async latestForBuild(buildId, kinds) {
147
+ return (await this.listForBuild(buildId)).filter((row) => row.shardId === null && kinds.includes(row.kind)).at(-1) ?? null;
148
+ }
149
+ /** Attempt rows of a job, by attempt number. */
150
+ async attempts(jobId) {
151
+ return (await this.#db.list(this.#table, { where: and(eq(this.#c.jobId, jobId), eq(this.#c.entry, "attempt")) })).toSorted((a, b) => a.attempt - b.attempt);
152
+ }
153
+ /** Job rows of `kinds` that are queued or running, oldest first. */
154
+ async listActive(kinds) {
155
+ return (await this.#db.list(this.#table, { where: and(jobEntries(this.#c), kindFilter(this.#c, kinds), inArray(this.#c.status, CANDIDATE_STATUSES)) })).toSorted(byCreation);
156
+ }
157
+ /** The newest job rows of `kinds`, newest first. */
158
+ async listRecent(kinds, limit) {
159
+ return (await this.#db.list(this.#table, {
160
+ where: and(jobEntries(this.#c), kindFilter(this.#c, kinds)),
161
+ orderBy: desc(this.#c.createdAt),
162
+ limit
163
+ })).toSorted((a, b) => byCreation(b, a));
164
+ }
165
+ /**
166
+ * Retention: delete finished jobs created before `before`, with their attempt rows. Returns the
167
+ * number of jobs removed. Running and queued jobs are never touched.
168
+ */
169
+ async purgeFinished(before) {
170
+ const rows = await this.#db.list(this.#table, { where: and(jobEntries(this.#c), lt(this.#c.createdAt, before), inArray(this.#c.status, [...TERMINAL_JOB_STATUSES])) });
171
+ await Promise.all(rows.map(async (row) => {
172
+ await this.#removeJob(row.id);
173
+ }));
174
+ return rows.length;
175
+ }
176
+ /**
177
+ * A build was removed: delete every `capture_jobs` row that points at it (job and attempt rows,
178
+ * whatever their status). There is no foreign key, so retention does this by hand. Returns the
179
+ * number of rows removed.
180
+ */
181
+ async removeForBuild(buildId) {
182
+ const rows = await this.#db.list(this.#table, { where: eq(this.#c.buildId, buildId) });
183
+ await Promise.all(rows.map(async (row) => {
184
+ await this.#db.remove(this.#table, row.id);
185
+ }));
186
+ return rows.length;
187
+ }
188
+ async #removeJob(jobId) {
189
+ const attempts = await this.attempts(jobId);
190
+ await Promise.all(attempts.map(async (row) => {
191
+ await this.#db.remove(this.#table, row.id);
192
+ }));
193
+ await this.#db.remove(this.#table, jobId);
194
+ }
195
+ };
196
+ /** Primary key of attempt `attempt` of a job. */
197
+ function attemptRowId(jobId, attempt) {
198
+ return `${jobId}:${String(attempt)}`;
199
+ }
200
+ /** Split an attempt row id back into its job id and number; null when it is not one. */
201
+ function parseAttemptRowId(id) {
202
+ const cut = id.lastIndexOf(":");
203
+ const attempt = Number(id.slice(cut + 1));
204
+ if (cut <= 0 || !Number.isInteger(attempt) || attempt < 1) return null;
205
+ return {
206
+ jobId: id.slice(0, cut),
207
+ attempt
208
+ };
209
+ }
210
+ /** Whether a job row has ended. */
211
+ function isTerminalJob(row) {
212
+ return TERMINAL_JOB_STATUSES.includes(row.status);
213
+ }
214
+ function jobColumns(table) {
215
+ return {
216
+ id: col(table, "id"),
217
+ entry: col(table, "entry"),
218
+ jobId: col(table, "jobId"),
219
+ kind: col(table, "kind"),
220
+ buildId: col(table, "buildId"),
221
+ status: col(table, "status"),
222
+ visibleAt: col(table, "visibleAt"),
223
+ createdAt: col(table, "createdAt")
224
+ };
225
+ }
226
+ function kindFilter(c, kinds) {
227
+ return kinds.length === 1 ? eq(c.kind, kinds[0]) : inArray(c.kind, [...kinds]);
228
+ }
229
+ function jobEntries(c) {
230
+ return eq(c.entry, "job");
231
+ }
232
+ function byCreation(a, b) {
233
+ return a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
234
+ }
235
+ function jobRow(input, now) {
236
+ return {
237
+ ...blankRow(now),
238
+ id: input.id,
239
+ entry: "job",
240
+ jobId: input.id,
241
+ kind: input.kind,
242
+ buildId: input.buildId ?? null,
243
+ shardId: input.shardId ?? null,
244
+ maxAttempts: input.maxAttempts,
245
+ status: "queued",
246
+ visibleAt: input.visibleAt ?? now,
247
+ payload: input.payload ?? null
248
+ };
249
+ }
250
+ function attemptRow(input, now) {
251
+ const { job } = input;
252
+ return {
253
+ ...blankRow(now),
254
+ id: attemptRowId(job.jobId, input.attempt),
255
+ entry: "attempt",
256
+ jobId: job.jobId,
257
+ kind: job.kind,
258
+ buildId: job.buildId,
259
+ shardId: job.shardId,
260
+ attempt: input.attempt,
261
+ maxAttempts: job.maxAttempts,
262
+ status: "running",
263
+ visibleAt: input.leaseUntil,
264
+ owner: input.owner,
265
+ cancelRequestedAt: job.cancelRequestedAt,
266
+ startedAt: now
267
+ };
268
+ }
269
+ function blankRow(now) {
270
+ return {
271
+ buildId: null,
272
+ shardId: null,
273
+ attempt: 0,
274
+ visibleAt: null,
275
+ owner: null,
276
+ payload: null,
277
+ cancelRequestedAt: null,
278
+ error: null,
279
+ startedAt: null,
280
+ finishedAt: null,
281
+ createdAt: now,
282
+ updatedAt: now
283
+ };
284
+ }
285
+ //#endregion
286
+ export { CaptureJobModel, TERMINAL_JOB_STATUSES, attemptRowId, isTerminalJob, parseAttemptRowId };
287
+
288
+ //# sourceMappingURL=capture-job.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-job.mjs","names":[],"sources":["../../src/models/capture-job.ts"],"sourcesContent":["/** Rows of the database queue (`capture_jobs`): job rows, per-attempt claim rows, and their views. */\nimport { and, asc, desc, eq, inArray, lt, lte } from \"drizzle-orm\";\nimport type { Column, SQL, Table } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type {\n CaptureAttemptStatus,\n CaptureJobKind,\n CaptureJobRow,\n CaptureJobStatus,\n} from \"../schema/capture-job.ts\";\nimport { col } from \"./columns.ts\";\n\n/** Tables required by {@link CaptureJobModel}. */\nexport interface CaptureJobTables {\n captureJobs: Table;\n}\n\n/** A job to put on the queue. */\nexport interface NewCaptureJob {\n id: string;\n kind: CaptureJobKind;\n buildId?: string | null;\n shardId?: string | null;\n maxAttempts: number;\n /** JSON payload (request id, traceparent, kind-specific fields). */\n payload?: string | null;\n /** When the job may first be claimed; defaults to `now`. */\n visibleAt?: string;\n}\n\n/** The claim an instance makes on a job: attempt `attempt`, held by `owner` until `leaseUntil`. */\nexport interface AttemptClaim {\n job: CaptureJobRow;\n attempt: number;\n owner: string;\n leaseUntil: string;\n}\n\n/** Job statuses that end a job: nothing claims it again. */\nexport const TERMINAL_JOB_STATUSES: readonly CaptureJobStatus[] = [\"done\", \"failed\", \"cancelled\"];\n\n/** Statuses of rows the poll may claim: a queued job, or a running attempt whose lease ran out. */\nconst CANDIDATE_STATUSES = [\"queued\", \"running\"];\n\n/**\n * Data operations of the database queue. Every write is an unconditional update by id or an\n * insert; the only atomic primitive is the unique-key insert of an attempt row ({@link claim}),\n * which is what fences concurrent claimers on every supported database.\n */\nexport class CaptureJobModel {\n readonly #db: DatabaseAdapter;\n readonly #table: Table;\n readonly #c: JobColumns;\n\n constructor(db: DatabaseAdapter, tables?: CaptureJobTables) {\n this.#db = db;\n this.#table = (tables ?? { captureJobs: db.tables.captureJobs }).captureJobs;\n this.#c = jobColumns(this.#table);\n }\n\n async get(id: string): Promise<CaptureJobRow | null> {\n return (await this.#db.get(this.#table, id)) as unknown as CaptureJobRow | null;\n }\n\n /** Insert a queued job row. */\n async create(input: NewCaptureJob, now: string): Promise<CaptureJobRow> {\n return (await this.#db.insert(this.#table, jobRow(input, now))) as unknown as CaptureJobRow;\n }\n\n /**\n * The poll probe: the oldest row of `kinds` that may be claimed at `now`. One indexed range query\n * on `(kind, visible_at, status)`, limit 1; only candidates have a `visibleAt`, so an empty queue\n * reads no rows and a busy one reads one.\n */\n async nextCandidate(\n kinds: readonly CaptureJobKind[],\n now: string,\n ): Promise<CaptureJobRow | null> {\n const rows = (await this.#db.list(this.#table, {\n where: and(\n kindFilter(this.#c, kinds),\n lte(this.#c.visibleAt, now),\n inArray(this.#c.status, CANDIDATE_STATUSES),\n ),\n orderBy: asc(this.#c.visibleAt),\n limit: 1,\n })) as unknown as CaptureJobRow[];\n return rows[0] ?? null;\n }\n\n /**\n * Claim attempt `attempt` of a job by inserting its row `<jobId>:<attempt>`. Exactly one caller\n * succeeds on every database; the others hit the primary key, which this reports as `false`\n * (told apart from real failures by re-reading the row, as `CaptureShardModel.claimFinalize`).\n */\n async claim(input: AttemptClaim, now: string): Promise<boolean> {\n const id = attemptRowId(input.job.jobId, input.attempt);\n try {\n await this.#db.insert(this.#table, attemptRow(input, now));\n return true;\n } catch (error) {\n if ((await this.get(id)) === null) {\n throw error;\n }\n return false;\n }\n }\n\n /** The claim won: the job row records the running attempt and leaves the poll's index range. */\n async markRunning(jobId: string, attempt: number, now: string): Promise<void> {\n await this.#db.update(this.#table, jobId, {\n status: \"running\",\n attempt,\n visibleAt: null,\n startedAt: now,\n updatedAt: now,\n });\n }\n\n /** Push an attempt's lease out. Returns the row (null if gone) so the caller sees cancel or expiry. */\n async renew(attemptId: string, leaseUntil: string, now: string): Promise<CaptureJobRow | null> {\n const row = await this.#db.update(this.#table, attemptId, {\n visibleAt: leaseUntil,\n updatedAt: now,\n });\n return (row ?? null) as unknown as CaptureJobRow | null;\n }\n\n /** The attempt row `attemptId` if it is running with a lease that passed by `now`, else null. */\n async expiredAttempt(attemptId: string, now: string): Promise<CaptureJobRow | null> {\n const rows = (await this.#db.list(this.#table, {\n where: and(\n eq(this.#c.id, attemptId),\n eq(this.#c.status, \"running\"),\n lte(this.#c.visibleAt, now),\n ),\n limit: 1,\n })) as unknown as CaptureJobRow[];\n return rows[0] ?? null;\n }\n\n /** Move a row's visibility time (and nothing else). */\n async defer(id: string, visibleAt: string, now: string): Promise<void> {\n await this.#db.update(this.#table, id, { visibleAt, updatedAt: now });\n }\n\n /** Take a row out of the poll's index range without changing its status. */\n async clearVisibility(id: string, now: string): Promise<void> {\n await this.#db.update(this.#table, id, { visibleAt: null, updatedAt: now });\n }\n\n /** End an attempt row: `done`, `failed` (with its error) or `expired` (taken over). */\n async finishAttempt(\n attemptId: string,\n status: Exclude<CaptureAttemptStatus, \"running\">,\n now: string,\n error: string | null = null,\n ): Promise<void> {\n await this.#db.update(this.#table, attemptId, {\n status,\n visibleAt: null,\n error,\n finishedAt: now,\n updatedAt: now,\n });\n }\n\n /** End a job row for good. */\n async finishJob(\n jobId: string,\n status: Exclude<CaptureJobStatus, \"queued\" | \"running\">,\n now: string,\n error: string | null = null,\n ): Promise<void> {\n const patch = { status, visibleAt: null, finishedAt: now, updatedAt: now };\n await this.#db.update(this.#table, jobId, error === null ? patch : { ...patch, error });\n }\n\n /** Put a job back on the queue, claimable from `visibleAt`, remembering why the attempt failed. */\n async requeue(\n jobId: string,\n visibleAt: string,\n error: string | null,\n now: string,\n ): Promise<void> {\n await this.#db.update(this.#table, jobId, {\n status: \"queued\",\n visibleAt,\n error,\n updatedAt: now,\n });\n }\n\n /** Flag a row as cancel-requested; the flag is never cleared. */\n async flagCancel(id: string, now: string): Promise<void> {\n await this.#db.update(this.#table, id, { cancelRequestedAt: now, updatedAt: now });\n }\n\n /** Job rows of a build (its own job and its shard jobs), oldest first. */\n async listForBuild(buildId: string): Promise<CaptureJobRow[]> {\n const rows = (await this.#db.list(this.#table, {\n where: and(eq(this.#c.buildId, buildId), jobEntries(this.#c)),\n })) as unknown as CaptureJobRow[];\n return rows.toSorted(byCreation);\n }\n\n /** The build's own (non-shard) job of `kinds` most recently queued, or null. */\n async latestForBuild(\n buildId: string,\n kinds: readonly CaptureJobKind[],\n ): Promise<CaptureJobRow | null> {\n const own = (await this.listForBuild(buildId)).filter(\n (row) => row.shardId === null && kinds.includes(row.kind),\n );\n return own.at(-1) ?? null;\n }\n\n /** Attempt rows of a job, by attempt number. */\n async attempts(jobId: string): Promise<CaptureJobRow[]> {\n const rows = (await this.#db.list(this.#table, {\n where: and(eq(this.#c.jobId, jobId), eq(this.#c.entry, \"attempt\")),\n })) as unknown as CaptureJobRow[];\n return rows.toSorted((a, b) => a.attempt - b.attempt);\n }\n\n /** Job rows of `kinds` that are queued or running, oldest first. */\n async listActive(kinds: readonly CaptureJobKind[]): Promise<CaptureJobRow[]> {\n const rows = (await this.#db.list(this.#table, {\n where: and(\n jobEntries(this.#c),\n kindFilter(this.#c, kinds),\n inArray(this.#c.status, CANDIDATE_STATUSES),\n ),\n })) as unknown as CaptureJobRow[];\n return rows.toSorted(byCreation);\n }\n\n /** The newest job rows of `kinds`, newest first. */\n async listRecent(kinds: readonly CaptureJobKind[], limit: number): Promise<CaptureJobRow[]> {\n const rows = (await this.#db.list(this.#table, {\n where: and(jobEntries(this.#c), kindFilter(this.#c, kinds)),\n orderBy: desc(this.#c.createdAt),\n limit,\n })) as unknown as CaptureJobRow[];\n return rows.toSorted((a, b) => byCreation(b, a));\n }\n\n /**\n * Retention: delete finished jobs created before `before`, with their attempt rows. Returns the\n * number of jobs removed. Running and queued jobs are never touched.\n */\n async purgeFinished(before: string): Promise<number> {\n const rows = (await this.#db.list(this.#table, {\n where: and(\n jobEntries(this.#c),\n lt(this.#c.createdAt, before),\n inArray(this.#c.status, [...TERMINAL_JOB_STATUSES]),\n ),\n })) as unknown as CaptureJobRow[];\n await Promise.all(\n rows.map(async (row) => {\n await this.#removeJob(row.id);\n }),\n );\n return rows.length;\n }\n\n /**\n * A build was removed: delete every `capture_jobs` row that points at it (job and attempt rows,\n * whatever their status). There is no foreign key, so retention does this by hand. Returns the\n * number of rows removed.\n */\n async removeForBuild(buildId: string): Promise<number> {\n const rows = (await this.#db.list(this.#table, {\n where: eq(this.#c.buildId, buildId),\n })) as unknown as CaptureJobRow[];\n await Promise.all(\n rows.map(async (row) => {\n await this.#db.remove(this.#table, row.id);\n }),\n );\n return rows.length;\n }\n\n async #removeJob(jobId: string): Promise<void> {\n const attempts = await this.attempts(jobId);\n await Promise.all(\n attempts.map(async (row) => {\n await this.#db.remove(this.#table, row.id);\n }),\n );\n await this.#db.remove(this.#table, jobId);\n }\n}\n\n/** Primary key of attempt `attempt` of a job. */\nexport function attemptRowId(jobId: string, attempt: number): string {\n return `${jobId}:${String(attempt)}`;\n}\n\n/** Split an attempt row id back into its job id and number; null when it is not one. */\nexport function parseAttemptRowId(id: string): { jobId: string; attempt: number } | null {\n const cut = id.lastIndexOf(\":\");\n const attempt = Number(id.slice(cut + 1));\n if (cut <= 0 || !Number.isInteger(attempt) || attempt < 1) {\n return null;\n }\n return { jobId: id.slice(0, cut), attempt };\n}\n\n/** Whether a job row has ended. */\nexport function isTerminalJob(row: CaptureJobRow): boolean {\n return (TERMINAL_JOB_STATUSES as readonly string[]).includes(row.status);\n}\n\n/** Column handles the queries filter and order on. */\ninterface JobColumns {\n id: Column;\n entry: Column;\n jobId: Column;\n kind: Column;\n buildId: Column;\n status: Column;\n visibleAt: Column;\n createdAt: Column;\n}\n\nfunction jobColumns(table: Table): JobColumns {\n return {\n id: col(table, \"id\"),\n entry: col(table, \"entry\"),\n jobId: col(table, \"jobId\"),\n kind: col(table, \"kind\"),\n buildId: col(table, \"buildId\"),\n status: col(table, \"status\"),\n visibleAt: col(table, \"visibleAt\"),\n createdAt: col(table, \"createdAt\"),\n };\n}\n\nfunction kindFilter(c: JobColumns, kinds: readonly CaptureJobKind[]): SQL {\n return kinds.length === 1 ? eq(c.kind, kinds[0]) : inArray(c.kind, [...kinds]);\n}\n\nfunction jobEntries(c: JobColumns): SQL {\n return eq(c.entry, \"job\");\n}\n\nfunction byCreation(a: CaptureJobRow, b: CaptureJobRow): number {\n return a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);\n}\n\nfunction jobRow(input: NewCaptureJob, now: string): CaptureJobRow {\n return {\n ...blankRow(now),\n id: input.id,\n entry: \"job\",\n jobId: input.id,\n kind: input.kind,\n buildId: input.buildId ?? null,\n shardId: input.shardId ?? null,\n maxAttempts: input.maxAttempts,\n status: \"queued\",\n visibleAt: input.visibleAt ?? now,\n payload: input.payload ?? null,\n };\n}\n\nfunction attemptRow(input: AttemptClaim, now: string): CaptureJobRow {\n const { job } = input;\n return {\n ...blankRow(now),\n id: attemptRowId(job.jobId, input.attempt),\n entry: \"attempt\",\n jobId: job.jobId,\n kind: job.kind,\n buildId: job.buildId,\n shardId: job.shardId,\n attempt: input.attempt,\n maxAttempts: job.maxAttempts,\n status: \"running\",\n visibleAt: input.leaseUntil,\n owner: input.owner,\n cancelRequestedAt: job.cancelRequestedAt,\n startedAt: now,\n };\n}\n\ntype BlankRow = Omit<CaptureJobRow, \"id\" | \"entry\" | \"jobId\" | \"kind\" | \"status\" | \"maxAttempts\">;\n\nfunction blankRow(now: string): BlankRow {\n return {\n buildId: null,\n shardId: null,\n attempt: 0,\n visibleAt: null,\n owner: null,\n payload: null,\n cancelRequestedAt: null,\n error: null,\n startedAt: null,\n finishedAt: null,\n createdAt: now,\n updatedAt: now,\n };\n}\n"],"mappings":"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"}
@@ -10,8 +10,8 @@ interface CaptureLogTables {
10
10
  }
11
11
  /** Data operations for capture log lines (every line of an attempt). */
12
12
  declare class CaptureLogModel {
13
- private readonly db;
14
13
  private readonly tables;
14
+ private readonly db;
15
15
  constructor(db: DatabaseAdapter, tables?: CaptureLogTables);
16
16
  listByAttempt(attemptId: string, limit?: number): Promise<CaptureLog[]>;
17
17
  append(projectId: string, buildId: string, attemptId: string, level: CaptureLogLevel, message: string, fields?: unknown): Promise<CaptureLog>;
@@ -2,13 +2,14 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { ulid } from "../utils/ulid.mjs";
5
- import { asc, eq, getTableColumns } from "drizzle-orm";
5
+ import { col } from "./columns.mjs";
6
+ import { asc, eq } from "drizzle-orm";
6
7
  //#region src/models/capture-log.ts
7
8
  /** Capture log lines: every stored line of a capture attempt. */
8
9
  /** Data operations for capture log lines (every line of an attempt). */
9
10
  var CaptureLogModel = class {
10
- db;
11
11
  tables;
12
+ db;
12
13
  constructor(db, tables) {
13
14
  this.db = db;
14
15
  this.tables = tables ?? { captureLogs: db.tables.captureLogs };
@@ -36,7 +37,7 @@ var CaptureLogModel = class {
36
37
  }
37
38
  };
38
39
  function columnsOf(table, column) {
39
- return getTableColumns(table)[column];
40
+ return col(table, column);
40
41
  }
41
42
  //#endregion
42
43
  export { CaptureLogModel };
@@ -1 +1 @@
1
- {"version":3,"file":"capture-log.mjs","names":[],"sources":["../../src/models/capture-log.ts"],"sourcesContent":["/** Capture log lines: every stored line of a capture attempt. */\nimport { asc, eq, getTableColumns } from \"drizzle-orm\";\nimport type { SQLWrapper, Table } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { CaptureLog, CaptureLogLevel } from \"../schema/capture-attempt.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Tables required by {@link CaptureLogModel}. */\nexport interface CaptureLogTables {\n captureLogs: Table;\n}\n\n/** Data operations for capture log lines (every line of an attempt). */\nexport class CaptureLogModel {\n private readonly tables: CaptureLogTables;\n constructor(\n private readonly db: DatabaseAdapter,\n tables?: CaptureLogTables,\n ) {\n this.tables = tables ?? { captureLogs: db.tables.captureLogs };\n }\n\n async listByAttempt(attemptId: string, limit?: number): Promise<CaptureLog[]> {\n return (await this.db.list(this.tables.captureLogs, {\n where: eq(columnsOf(this.tables.captureLogs, \"attemptId\"), attemptId),\n orderBy: asc(columnsOf(this.tables.captureLogs, \"seq\")),\n ...(limit === undefined ? {} : { limit }),\n })) as unknown as CaptureLog[];\n }\n\n async append(\n projectId: string,\n buildId: string,\n attemptId: string,\n level: CaptureLogLevel,\n message: string,\n fields?: unknown,\n ): Promise<CaptureLog> {\n const seq =\n (await this.db.count(\n this.tables.captureLogs,\n eq(columnsOf(this.tables.captureLogs, \"attemptId\"), attemptId),\n )) + 1;\n return (await this.db.insert(this.tables.captureLogs, {\n id: ulid(),\n projectId,\n buildId,\n attemptId,\n seq,\n level,\n message,\n fields: fields === undefined ? null : JSON.stringify(fields),\n createdAt: new Date().toISOString(),\n })) as unknown as CaptureLog;\n }\n}\n\nfunction columnsOf(table: Table, column: string): SQLWrapper {\n return getTableColumns(table)[column] as unknown as SQLWrapper;\n}\n"],"mappings":";;;;;;;;AAaA,IAAa,kBAAb,MAA6B;CAGR;CAFnB;CACA,YACE,IACA,QACA;EAFiB,KAAA,KAAA;EAGjB,KAAK,SAAS,UAAU,EAAE,aAAa,GAAG,OAAO,YAAY;CAC/D;CAEA,MAAM,cAAc,WAAmB,OAAuC;EAC5E,OAAQ,MAAM,KAAK,GAAG,KAAK,KAAK,OAAO,aAAa;GAClD,OAAO,GAAG,UAAU,KAAK,OAAO,aAAa,WAAW,GAAG,SAAS;GACpE,SAAS,IAAI,UAAU,KAAK,OAAO,aAAa,KAAK,CAAC;GACtD,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM;EACzC,CAAC;CACH;CAEA,MAAM,OACJ,WACA,SACA,WACA,OACA,SACA,QACqB;EACrB,MAAM,MACH,MAAM,KAAK,GAAG,MACb,KAAK,OAAO,aACZ,GAAG,UAAU,KAAK,OAAO,aAAa,WAAW,GAAG,SAAS,CAC/D,IAAK;EACP,OAAQ,MAAM,KAAK,GAAG,OAAO,KAAK,OAAO,aAAa;GACpD,IAAI,KAAK;GACT;GACA;GACA;GACA;GACA;GACA;GACA,QAAQ,WAAW,KAAA,IAAY,OAAO,KAAK,UAAU,MAAM;GAC3D,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;AACF;AAEA,SAAS,UAAU,OAAc,QAA4B;CAC3D,OAAO,gBAAgB,KAAK,CAAC,CAAC;AAChC"}
1
+ {"version":3,"file":"capture-log.mjs","names":[],"sources":["../../src/models/capture-log.ts"],"sourcesContent":["/** Capture log lines: every stored line of a capture attempt. */\nimport { asc, eq } from \"drizzle-orm\";\nimport type { SQLWrapper, Table } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { CaptureLog, CaptureLogLevel } from \"../schema/capture-attempt.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\nimport { col } from \"./columns.ts\";\n\n/** Tables required by {@link CaptureLogModel}. */\nexport interface CaptureLogTables {\n captureLogs: Table;\n}\n\n/** Data operations for capture log lines (every line of an attempt). */\nexport class CaptureLogModel {\n private readonly tables: CaptureLogTables;\n private readonly db: DatabaseAdapter;\n\n constructor(db: DatabaseAdapter, tables?: CaptureLogTables) {\n this.db = db;\n this.tables = tables ?? { captureLogs: db.tables.captureLogs };\n }\n\n async listByAttempt(attemptId: string, limit?: number): Promise<CaptureLog[]> {\n return (await this.db.list(this.tables.captureLogs, {\n where: eq(columnsOf(this.tables.captureLogs, \"attemptId\"), attemptId),\n orderBy: asc(columnsOf(this.tables.captureLogs, \"seq\")),\n ...(limit === undefined ? {} : { limit }),\n })) as unknown as CaptureLog[];\n }\n\n async append(\n projectId: string,\n buildId: string,\n attemptId: string,\n level: CaptureLogLevel,\n message: string,\n fields?: unknown,\n ): Promise<CaptureLog> {\n const seq =\n (await this.db.count(\n this.tables.captureLogs,\n eq(columnsOf(this.tables.captureLogs, \"attemptId\"), attemptId),\n )) + 1;\n return (await this.db.insert(this.tables.captureLogs, {\n id: ulid(),\n projectId,\n buildId,\n attemptId,\n seq,\n level,\n message,\n fields: fields === undefined ? null : JSON.stringify(fields),\n createdAt: new Date().toISOString(),\n })) as unknown as CaptureLog;\n }\n}\n\nfunction columnsOf(table: Table, column: string): SQLWrapper {\n return col(table, column);\n}\n"],"mappings":";;;;;;;;;AAcA,IAAa,kBAAb,MAA6B;CAC3B;CACA;CAEA,YAAY,IAAqB,QAA2B;EAC1D,KAAK,KAAK;EACV,KAAK,SAAS,UAAU,EAAE,aAAa,GAAG,OAAO,YAAY;CAC/D;CAEA,MAAM,cAAc,WAAmB,OAAuC;EAC5E,OAAQ,MAAM,KAAK,GAAG,KAAK,KAAK,OAAO,aAAa;GAClD,OAAO,GAAG,UAAU,KAAK,OAAO,aAAa,WAAW,GAAG,SAAS;GACpE,SAAS,IAAI,UAAU,KAAK,OAAO,aAAa,KAAK,CAAC;GACtD,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM;EACzC,CAAC;CACH;CAEA,MAAM,OACJ,WACA,SACA,WACA,OACA,SACA,QACqB;EACrB,MAAM,MACH,MAAM,KAAK,GAAG,MACb,KAAK,OAAO,aACZ,GAAG,UAAU,KAAK,OAAO,aAAa,WAAW,GAAG,SAAS,CAC/D,IAAK;EACP,OAAQ,MAAM,KAAK,GAAG,OAAO,KAAK,OAAO,aAAa;GACpD,IAAI,KAAK;GACT;GACA;GACA;GACA;GACA;GACA;GACA,QAAQ,WAAW,KAAA,IAAY,OAAO,KAAK,UAAU,MAAM;GAC3D,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;AACF;AAEA,SAAS,UAAU,OAAc,QAA4B;CAC3D,OAAO,IAAI,OAAO,MAAM;AAC1B"}
@@ -0,0 +1,62 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { DatabaseAdapter } from "../adapter/database.mjs";
4
+ import { CaptureShard } from "../schema/capture-shard.mjs";
5
+ import { Table } from "drizzle-orm";
6
+ //#region src/models/capture-shard.d.ts
7
+ /** Tables required by {@link CaptureShardModel}. */
8
+ interface CaptureShardTables {
9
+ captureShards: Table;
10
+ }
11
+ /** A render shard to create: its JSON payload (the slice of stories and engines). */
12
+ interface NewRenderShard {
13
+ payload: string;
14
+ }
15
+ /** Data operations for the stages of a sharded capture. */
16
+ declare class CaptureShardModel {
17
+ #private;
18
+ constructor(db: DatabaseAdapter, tables?: CaptureShardTables);
19
+ get(id: string): Promise<CaptureShard | null>;
20
+ /** Every row of a build, in plan, render, finalize and ordinal order. */
21
+ listForBuild(buildId: string): Promise<CaptureShard[]>;
22
+ renderShards(buildId: string): Promise<CaptureShard[]>;
23
+ plan(buildId: string): Promise<CaptureShard | null>;
24
+ /** Forget every stage of a build: a new plan (retry, recovery) starts clean. */
25
+ clearBuild(buildId: string): Promise<void>;
26
+ /**
27
+ * Record a plan: the plan row (already `done`, it ran) and one `pending` render shard per slice.
28
+ * Returns the render shards in ordinal order.
29
+ */
30
+ createPlan(projectId: string, buildId: string, plan: {
31
+ attemptId: string | null;
32
+ payload: string;
33
+ result: string;
34
+ }, shards: NewRenderShard[]): Promise<CaptureShard[]>;
35
+ /** Mark a shard running. A shard already done stays done: this run is a redelivered duplicate. */
36
+ markRunning(id: string): Promise<void>;
37
+ /** Finish a shard with its outcome (JSON). Idempotent: a redelivered shard overwrites its own result. */
38
+ complete(id: string, result: string): Promise<void>;
39
+ /**
40
+ * A failed run that will be retried: back to `pending`, remembering why. Ignored for a shard
41
+ * already done, whose result a failing duplicate must not undo.
42
+ */
43
+ retry(id: string, error: string): Promise<void>;
44
+ /** The shard's last try failed. Ignored for a shard already done, like {@link retry}. */
45
+ fail(id: string, error: string): Promise<void>;
46
+ /**
47
+ * Try to become the one worker that finalizes the build. The claim row's primary key is
48
+ * `<buildId>:finalize`, so inserting it succeeds for exactly one caller on every database;
49
+ * everyone else gets a key conflict, which this reports as `false`.
50
+ *
51
+ * A claim still `running` after {@link FINALIZE_STALE_MS} belonged to a worker that died
52
+ * mid-finalize, and is taken over. Two late takers can both win; finalizing is idempotent.
53
+ */
54
+ claimFinalize(projectId: string, buildId: string, now?: Date): Promise<boolean>;
55
+ /** The finalize ran to the end: its claim is closed for good. */
56
+ finishFinalize(buildId: string): Promise<void>;
57
+ /** The finalize failed: drop its claim, so the next run to reduce the build can finalize it. */
58
+ releaseFinalize(buildId: string): Promise<void>;
59
+ }
60
+ //#endregion
61
+ export { CaptureShardModel, CaptureShardTables, NewRenderShard };
62
+ //# sourceMappingURL=capture-shard.d.mts.map
@@ -0,0 +1,190 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { ulid } from "../utils/ulid.mjs";
5
+ import { col } from "./columns.mjs";
6
+ import { asc, eq } from "drizzle-orm";
7
+ //#region src/models/capture-shard.ts
8
+ /** Shard rows of a sharded capture: the plan, the render slices, and the finalize claim. */
9
+ /** Data operations for the stages of a sharded capture. */
10
+ var CaptureShardModel = class {
11
+ #db;
12
+ #table;
13
+ constructor(db, tables) {
14
+ this.#db = db;
15
+ this.#table = (tables ?? { captureShards: db.tables.captureShards }).captureShards;
16
+ }
17
+ async get(id) {
18
+ return await this.#db.get(this.#table, id);
19
+ }
20
+ /** Every row of a build, in plan, render, finalize and ordinal order. */
21
+ async listForBuild(buildId) {
22
+ return (await this.#db.list(this.#table, {
23
+ where: eq(col(this.#table, "buildId"), buildId),
24
+ orderBy: asc(col(this.#table, "ordinal"))
25
+ })).toSorted((a, b) => kindRank(a.kind) - kindRank(b.kind) || a.ordinal - b.ordinal);
26
+ }
27
+ async renderShards(buildId) {
28
+ return (await this.listForBuild(buildId)).filter((row) => row.kind === "render");
29
+ }
30
+ async plan(buildId) {
31
+ return (await this.listForBuild(buildId)).find((row) => row.kind === "plan") ?? null;
32
+ }
33
+ /** Forget every stage of a build: a new plan (retry, recovery) starts clean. */
34
+ async clearBuild(buildId) {
35
+ const rows = await this.listForBuild(buildId);
36
+ await Promise.all(rows.map(async (row) => {
37
+ await this.#db.remove(this.#table, row.id);
38
+ }));
39
+ }
40
+ /**
41
+ * Record a plan: the plan row (already `done`, it ran) and one `pending` render shard per slice.
42
+ * Returns the render shards in ordinal order.
43
+ */
44
+ async createPlan(projectId, buildId, plan, shards) {
45
+ const now = (/* @__PURE__ */ new Date()).toISOString();
46
+ await this.#db.insert(this.#table, {
47
+ ...base(projectId, buildId, "plan", 0, now),
48
+ id: `${buildId}:plan`,
49
+ status: "done",
50
+ attemptId: plan.attemptId,
51
+ payload: plan.payload,
52
+ result: plan.result,
53
+ startedAt: now,
54
+ finishedAt: now
55
+ });
56
+ return await Promise.all(shards.map(async (shard, index) => await this.#db.insert(this.#table, {
57
+ ...base(projectId, buildId, "render", index + 1, now),
58
+ id: ulid(),
59
+ payload: shard.payload
60
+ })));
61
+ }
62
+ /** Mark a shard running. A shard already done stays done: this run is a redelivered duplicate. */
63
+ async markRunning(id) {
64
+ const now = (/* @__PURE__ */ new Date()).toISOString();
65
+ await this.#unlessDone(id, {
66
+ status: "running",
67
+ startedAt: now,
68
+ updatedAt: now
69
+ });
70
+ }
71
+ /** Finish a shard with its outcome (JSON). Idempotent: a redelivered shard overwrites its own result. */
72
+ async complete(id, result) {
73
+ const now = (/* @__PURE__ */ new Date()).toISOString();
74
+ await this.#db.update(this.#table, id, {
75
+ status: "done",
76
+ result,
77
+ error: null,
78
+ finishedAt: now,
79
+ updatedAt: now
80
+ });
81
+ }
82
+ /**
83
+ * A failed run that will be retried: back to `pending`, remembering why. Ignored for a shard
84
+ * already done, whose result a failing duplicate must not undo.
85
+ */
86
+ async retry(id, error) {
87
+ await this.#unlessDone(id, {
88
+ status: "pending",
89
+ error,
90
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
91
+ });
92
+ }
93
+ /** The shard's last try failed. Ignored for a shard already done, like {@link retry}. */
94
+ async fail(id, error) {
95
+ const now = (/* @__PURE__ */ new Date()).toISOString();
96
+ await this.#unlessDone(id, {
97
+ status: "failed",
98
+ error,
99
+ finishedAt: now,
100
+ updatedAt: now
101
+ });
102
+ }
103
+ /**
104
+ * A queue that redelivers (at-least-once) can run one shard twice. Whichever finishes first wins;
105
+ * the other must not move the row out of `done`. Read-then-write, not atomic, but the window is
106
+ * the time between two statements, and the outcome of losing it is one extra identical write.
107
+ */
108
+ async #unlessDone(id, patch) {
109
+ if ((await this.get(id))?.status === "done") return;
110
+ await this.#db.update(this.#table, id, patch);
111
+ }
112
+ /**
113
+ * Try to become the one worker that finalizes the build. The claim row's primary key is
114
+ * `<buildId>:finalize`, so inserting it succeeds for exactly one caller on every database;
115
+ * everyone else gets a key conflict, which this reports as `false`.
116
+ *
117
+ * A claim still `running` after {@link FINALIZE_STALE_MS} belonged to a worker that died
118
+ * mid-finalize, and is taken over. Two late takers can both win; finalizing is idempotent.
119
+ */
120
+ async claimFinalize(projectId, buildId, now = /* @__PURE__ */ new Date()) {
121
+ const id = finalizeId(buildId);
122
+ try {
123
+ await this.#db.insert(this.#table, {
124
+ ...base(projectId, buildId, "finalize", 0, now.toISOString()),
125
+ id,
126
+ status: "running",
127
+ startedAt: now.toISOString()
128
+ });
129
+ return true;
130
+ } catch (error) {
131
+ const held = await this.get(id);
132
+ if (held === null) throw error;
133
+ return await this.#takeOverStale(held, now);
134
+ }
135
+ }
136
+ /** The finalize ran to the end: its claim is closed for good. */
137
+ async finishFinalize(buildId) {
138
+ const now = (/* @__PURE__ */ new Date()).toISOString();
139
+ await this.#db.update(this.#table, finalizeId(buildId), {
140
+ status: "done",
141
+ finishedAt: now,
142
+ updatedAt: now
143
+ });
144
+ }
145
+ /** The finalize failed: drop its claim, so the next run to reduce the build can finalize it. */
146
+ async releaseFinalize(buildId) {
147
+ await this.#db.remove(this.#table, finalizeId(buildId));
148
+ }
149
+ async #takeOverStale(held, now) {
150
+ const since = Date.parse(held.startedAt ?? held.createdAt);
151
+ if (held.status !== "running" || now.getTime() - since < 3e5) return false;
152
+ await this.#db.update(this.#table, held.id, {
153
+ startedAt: now.toISOString(),
154
+ updatedAt: now.toISOString()
155
+ });
156
+ return true;
157
+ }
158
+ };
159
+ /**
160
+ * How long a finalize claim may stay `running` before it counts as abandoned. Finalizing is a
161
+ * handful of queries over the build's snapshots, so minutes is long past any healthy run.
162
+ */
163
+ const FINALIZE_STALE_MS = 3e5;
164
+ function finalizeId(buildId) {
165
+ return `${buildId}:finalize`;
166
+ }
167
+ function base(projectId, buildId, kind, ordinal, now) {
168
+ return {
169
+ projectId,
170
+ buildId,
171
+ kind,
172
+ ordinal,
173
+ status: "pending",
174
+ attemptId: null,
175
+ payload: null,
176
+ result: null,
177
+ error: null,
178
+ startedAt: null,
179
+ finishedAt: null,
180
+ createdAt: now,
181
+ updatedAt: now
182
+ };
183
+ }
184
+ function kindRank(kind) {
185
+ return kind === "plan" ? 0 : kind === "render" ? 1 : 2;
186
+ }
187
+ //#endregion
188
+ export { CaptureShardModel, FINALIZE_STALE_MS };
189
+
190
+ //# sourceMappingURL=capture-shard.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-shard.mjs","names":[],"sources":["../../src/models/capture-shard.ts"],"sourcesContent":["/** Shard rows of a sharded capture: the plan, the render slices, and the finalize claim. */\nimport { asc, eq } from \"drizzle-orm\";\nimport type { Table } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { CaptureShard, CaptureShardKind } from \"../schema/capture-shard.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\nimport { col } from \"./columns.ts\";\n\n/** Tables required by {@link CaptureShardModel}. */\nexport interface CaptureShardTables {\n captureShards: Table;\n}\n\n/** A render shard to create: its JSON payload (the slice of stories and engines). */\nexport interface NewRenderShard {\n payload: string;\n}\n\n/** Data operations for the stages of a sharded capture. */\nexport class CaptureShardModel {\n readonly #db: DatabaseAdapter;\n readonly #table: Table;\n\n constructor(db: DatabaseAdapter, tables?: CaptureShardTables) {\n this.#db = db;\n this.#table = (tables ?? { captureShards: db.tables.captureShards }).captureShards;\n }\n\n async get(id: string): Promise<CaptureShard | null> {\n return (await this.#db.get(this.#table, id)) as unknown as CaptureShard | null;\n }\n\n /** Every row of a build, in plan, render, finalize and ordinal order. */\n async listForBuild(buildId: string): Promise<CaptureShard[]> {\n const rows = (await this.#db.list(this.#table, {\n where: eq(col(this.#table, \"buildId\"), buildId),\n orderBy: asc(col(this.#table, \"ordinal\")),\n })) as unknown as CaptureShard[];\n return rows.toSorted((a, b) => kindRank(a.kind) - kindRank(b.kind) || a.ordinal - b.ordinal);\n }\n\n async renderShards(buildId: string): Promise<CaptureShard[]> {\n return (await this.listForBuild(buildId)).filter((row) => row.kind === \"render\");\n }\n\n async plan(buildId: string): Promise<CaptureShard | null> {\n return (await this.listForBuild(buildId)).find((row) => row.kind === \"plan\") ?? null;\n }\n\n /** Forget every stage of a build: a new plan (retry, recovery) starts clean. */\n async clearBuild(buildId: string): Promise<void> {\n const rows = await this.listForBuild(buildId);\n await Promise.all(\n rows.map(async (row) => {\n await this.#db.remove(this.#table, row.id);\n }),\n );\n }\n\n /**\n * Record a plan: the plan row (already `done`, it ran) and one `pending` render shard per slice.\n * Returns the render shards in ordinal order.\n */\n async createPlan(\n projectId: string,\n buildId: string,\n plan: { attemptId: string | null; payload: string; result: string },\n shards: NewRenderShard[],\n ): Promise<CaptureShard[]> {\n const now = new Date().toISOString();\n await this.#db.insert(this.#table, {\n ...base(projectId, buildId, \"plan\", 0, now),\n id: `${buildId}:plan`,\n status: \"done\",\n attemptId: plan.attemptId,\n payload: plan.payload,\n result: plan.result,\n startedAt: now,\n finishedAt: now,\n });\n return await Promise.all(\n shards.map(\n async (shard, index) =>\n (await this.#db.insert(this.#table, {\n ...base(projectId, buildId, \"render\", index + 1, now),\n id: ulid(),\n payload: shard.payload,\n })) as unknown as CaptureShard,\n ),\n );\n }\n\n /** Mark a shard running. A shard already done stays done: this run is a redelivered duplicate. */\n async markRunning(id: string): Promise<void> {\n const now = new Date().toISOString();\n await this.#unlessDone(id, { status: \"running\", startedAt: now, updatedAt: now });\n }\n\n /** Finish a shard with its outcome (JSON). Idempotent: a redelivered shard overwrites its own result. */\n async complete(id: string, result: string): Promise<void> {\n const now = new Date().toISOString();\n await this.#db.update(this.#table, id, {\n status: \"done\",\n result,\n error: null,\n finishedAt: now,\n updatedAt: now,\n });\n }\n\n /**\n * A failed run that will be retried: back to `pending`, remembering why. Ignored for a shard\n * already done, whose result a failing duplicate must not undo.\n */\n async retry(id: string, error: string): Promise<void> {\n await this.#unlessDone(id, {\n status: \"pending\",\n error,\n updatedAt: new Date().toISOString(),\n });\n }\n\n /** The shard's last try failed. Ignored for a shard already done, like {@link retry}. */\n async fail(id: string, error: string): Promise<void> {\n const now = new Date().toISOString();\n await this.#unlessDone(id, { status: \"failed\", error, finishedAt: now, updatedAt: now });\n }\n\n /**\n * A queue that redelivers (at-least-once) can run one shard twice. Whichever finishes first wins;\n * the other must not move the row out of `done`. Read-then-write, not atomic, but the window is\n * the time between two statements, and the outcome of losing it is one extra identical write.\n */\n async #unlessDone(id: string, patch: Record<string, unknown>): Promise<void> {\n if ((await this.get(id))?.status === \"done\") {\n return;\n }\n await this.#db.update(this.#table, id, patch);\n }\n\n /**\n * Try to become the one worker that finalizes the build. The claim row's primary key is\n * `<buildId>:finalize`, so inserting it succeeds for exactly one caller on every database;\n * everyone else gets a key conflict, which this reports as `false`.\n *\n * A claim still `running` after {@link FINALIZE_STALE_MS} belonged to a worker that died\n * mid-finalize, and is taken over. Two late takers can both win; finalizing is idempotent.\n */\n async claimFinalize(projectId: string, buildId: string, now = new Date()): Promise<boolean> {\n const id = finalizeId(buildId);\n try {\n await this.#db.insert(this.#table, {\n ...base(projectId, buildId, \"finalize\", 0, now.toISOString()),\n id,\n status: \"running\",\n startedAt: now.toISOString(),\n });\n return true;\n } catch (error) {\n // A conflict means someone else holds the claim; anything else is a real failure.\n const held = await this.get(id);\n if (held === null) {\n throw error;\n }\n return await this.#takeOverStale(held, now);\n }\n }\n\n /** The finalize ran to the end: its claim is closed for good. */\n async finishFinalize(buildId: string): Promise<void> {\n const now = new Date().toISOString();\n await this.#db.update(this.#table, finalizeId(buildId), {\n status: \"done\",\n finishedAt: now,\n updatedAt: now,\n });\n }\n\n /** The finalize failed: drop its claim, so the next run to reduce the build can finalize it. */\n async releaseFinalize(buildId: string): Promise<void> {\n await this.#db.remove(this.#table, finalizeId(buildId));\n }\n\n async #takeOverStale(held: CaptureShard, now: Date): Promise<boolean> {\n const since = Date.parse(held.startedAt ?? held.createdAt);\n if (held.status !== \"running\" || now.getTime() - since < FINALIZE_STALE_MS) {\n return false;\n }\n await this.#db.update(this.#table, held.id, {\n startedAt: now.toISOString(),\n updatedAt: now.toISOString(),\n });\n return true;\n }\n}\n\n/**\n * How long a finalize claim may stay `running` before it counts as abandoned. Finalizing is a\n * handful of queries over the build's snapshots, so minutes is long past any healthy run.\n */\nexport const FINALIZE_STALE_MS = 5 * 60_000;\n\nfunction finalizeId(buildId: string): string {\n return `${buildId}:finalize`;\n}\n\ntype NewShardRow = Omit<CaptureShard, \"id\">;\n\nfunction base(\n projectId: string,\n buildId: string,\n kind: CaptureShardKind,\n ordinal: number,\n now: string,\n): NewShardRow {\n return {\n projectId,\n buildId,\n kind,\n ordinal,\n status: \"pending\",\n attemptId: null,\n payload: null,\n result: null,\n error: null,\n startedAt: null,\n finishedAt: null,\n createdAt: now,\n updatedAt: now,\n };\n}\n\nfunction kindRank(kind: CaptureShardKind): number {\n return kind === \"plan\" ? 0 : kind === \"render\" ? 1 : 2;\n}\n"],"mappings":"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"}