@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,56 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { BuildStateDeps } from "./types.mjs";
4
+ import { ConsumerJob, KindHandler } from "../consumer/types.mjs";
5
+ import { QueueJobInput } from "../db-queue/types.mjs";
6
+ //#region src/capture/build-state/notify-job.d.ts
7
+ /** Attempts a delivery gets before it is given up (the first try and four retries). */
8
+ declare const NOTIFY_MAX_ATTEMPTS = 5;
9
+ /** Retry delay of a delivery: 10 s, then doubling to at most five minutes. */
10
+ declare const NOTIFY_BACKOFF: {
11
+ readonly baseMs: 10000;
12
+ readonly maxMs: number;
13
+ };
14
+ /** A delivery that was given up: the channel shows the error and the admin event fires. */
15
+ interface NotifyFailure {
16
+ channelId: string;
17
+ projectId: string | null;
18
+ provider: string;
19
+ buildId: string;
20
+ /** Sanitized error text. */
21
+ error: string;
22
+ }
23
+ /** What {@link createNotifyHandler} needs. */
24
+ interface NotifyHandlerOptions {
25
+ deps: BuildStateDeps;
26
+ /** Called once when a delivery is given up, after the channel row recorded the error. Failures are logged. */
27
+ onFailed?: (failure: NotifyFailure) => Promise<void>;
28
+ }
29
+ /**
30
+ * The handler. Resolving acks the job (also when the build or channel is gone: nothing is left to
31
+ * show). A refused status rejects, so the consumer requeues with {@link NOTIFY_BACKOFF}; the last
32
+ * attempt records the error on the channel and raises `onFailed`.
33
+ */
34
+ declare function createNotifyHandler(options: NotifyHandlerOptions): KindHandler;
35
+ /**
36
+ * For the queue's `onExhausted` hook: a last attempt that lost its lease never ran its handler to
37
+ * the end, so the failure is surfaced here (the hook may run on several instances; recording the
38
+ * same error twice is harmless, the admin event may repeat).
39
+ */
40
+ declare function notifyExhausted(options: NotifyHandlerOptions): (job: ConsumerJob, error: string) => Promise<void>;
41
+ /** The slice of the database queue the enqueuer uses. */
42
+ interface NotifyJobQueue {
43
+ enqueueJob(job: QueueJobInput): Promise<string>;
44
+ }
45
+ /**
46
+ * Enqueue a `notify` job for a channel and build, unless one is already waiting: it will read the
47
+ * build's state when it runs, so a second would deliver the same thing. A job that is already
48
+ * running may have read an older state, so a new one is queued behind it.
49
+ */
50
+ declare function createNotifyEnqueuer(deps: Pick<BuildStateDeps, "db">, queue: NotifyJobQueue): (job: {
51
+ channelId: string;
52
+ buildId: string;
53
+ }) => Promise<void>;
54
+ //#endregion
55
+ export { NOTIFY_BACKOFF, NOTIFY_MAX_ATTEMPTS, NotifyFailure, NotifyHandlerOptions, NotifyJobQueue, createNotifyEnqueuer, createNotifyHandler, notifyExhausted };
56
+ //# sourceMappingURL=notify-job.d.mts.map
@@ -0,0 +1,133 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { sanitizeErrorText } from "../../utils/redact.mjs";
5
+ import { NotificationChannelModel } from "../../models/notification-channel.mjs";
6
+ import { deliverBuildState } from "./deliver.mjs";
7
+ import { CaptureJobModel } from "../../models/capture-job.mjs";
8
+ import { isFinalAttempt } from "../consumer/retry.mjs";
9
+ import { decodePayload } from "../db-queue/codec.mjs";
10
+ //#region src/capture/build-state/notify-job.ts
11
+ /**
12
+ * The `notify` job kind (ADR 0014, decision 7; ADR 0006, decision 4): one job per `(channel,
13
+ * build)` state change. The job names a channel and a build, never a status: the handler reads the
14
+ * build when it runs, so a retried or reordered job cannot post a stale state, and a queued job
15
+ * for the same pair already covers a newer request.
16
+ */
17
+ /** Attempts a delivery gets before it is given up (the first try and four retries). */
18
+ const NOTIFY_MAX_ATTEMPTS = 5;
19
+ /** Retry delay of a delivery: 10 s, then doubling to at most five minutes. */
20
+ const NOTIFY_BACKOFF = {
21
+ baseMs: 1e4,
22
+ maxMs: 3e5
23
+ };
24
+ /** Longest error text kept on a channel row. */
25
+ const MAX_ERROR_LENGTH = 500;
26
+ /** The channel a `notify` job delivers to. */
27
+ function channelIdOf(job) {
28
+ const id = job.data?.["channelId"];
29
+ return typeof id === "string" ? id : "";
30
+ }
31
+ /**
32
+ * The handler. Resolving acks the job (also when the build or channel is gone: nothing is left to
33
+ * show). A refused status rejects, so the consumer requeues with {@link NOTIFY_BACKOFF}; the last
34
+ * attempt records the error on the channel and raises `onFailed`.
35
+ */
36
+ function createNotifyHandler(options) {
37
+ const { deps } = options;
38
+ return {
39
+ accepts: (job) => job.buildId.length > 0 && channelIdOf(job).length > 0,
40
+ backoff: NOTIFY_BACKOFF,
41
+ run: async (job, context) => {
42
+ const channelId = channelIdOf(job);
43
+ try {
44
+ const result = await deliverBuildState(deps, {
45
+ buildId: job.buildId,
46
+ channelId
47
+ });
48
+ if (result === "delivered") await channelModel(deps).recordOutcome(channelId);
49
+ else context.logger.info({ result }, "nothing to deliver");
50
+ } catch (error) {
51
+ if (isFinalAttempt(job)) await surfaceFailure(options, {
52
+ channelId,
53
+ buildId: job.buildId,
54
+ error
55
+ });
56
+ throw error;
57
+ }
58
+ }
59
+ };
60
+ }
61
+ /**
62
+ * For the queue's `onExhausted` hook: a last attempt that lost its lease never ran its handler to
63
+ * the end, so the failure is surfaced here (the hook may run on several instances; recording the
64
+ * same error twice is harmless, the admin event may repeat).
65
+ */
66
+ function notifyExhausted(options) {
67
+ return async (job, error) => {
68
+ await surfaceFailure(options, {
69
+ channelId: channelIdOf(job),
70
+ buildId: job.buildId,
71
+ error
72
+ });
73
+ };
74
+ }
75
+ async function surfaceFailure(options, failure) {
76
+ const { deps } = options;
77
+ const text = sanitizeErrorText(errorText(failure.error), MAX_ERROR_LENGTH);
78
+ deps.logger.error({
79
+ channelId: failure.channelId,
80
+ buildId: failure.buildId,
81
+ error: text
82
+ }, "state delivery given up");
83
+ try {
84
+ await recordAndAlert(options, {
85
+ ...failure,
86
+ error: text
87
+ });
88
+ } catch (error) {
89
+ deps.logger.warn({
90
+ err: error,
91
+ channelId: failure.channelId
92
+ }, "failed to record the failure");
93
+ }
94
+ }
95
+ /** Put the error on the channel row, then raise `onFailed`; a channel that is gone needs neither. */
96
+ async function recordAndAlert(options, failure) {
97
+ const model = channelModel(options.deps);
98
+ const row = await model.get(failure.channelId);
99
+ if (row === null) return;
100
+ await model.recordOutcome(row.id, failure.error);
101
+ await options.onFailed?.({
102
+ channelId: row.id,
103
+ projectId: row.projectId,
104
+ provider: row.provider,
105
+ buildId: failure.buildId,
106
+ error: failure.error
107
+ });
108
+ }
109
+ function errorText(error) {
110
+ return error instanceof Error ? error.message : String(error);
111
+ }
112
+ function channelModel(deps) {
113
+ return new NotificationChannelModel(deps.db, { notificationChannels: deps.db.tables.notificationChannels }, deps.secret);
114
+ }
115
+ /**
116
+ * Enqueue a `notify` job for a channel and build, unless one is already waiting: it will read the
117
+ * build's state when it runs, so a second would deliver the same thing. A job that is already
118
+ * running may have read an older state, so a new one is queued behind it.
119
+ */
120
+ function createNotifyEnqueuer(deps, queue) {
121
+ return async ({ channelId, buildId }) => {
122
+ if (!(await new CaptureJobModel(deps.db).listForBuild(buildId)).some((row) => row.kind === "notify" && row.status === "queued" && decodePayload(row.payload).data?.["channelId"] === channelId)) await queue.enqueueJob({
123
+ kind: "notify",
124
+ buildId,
125
+ data: { channelId },
126
+ maxAttempts: 5
127
+ });
128
+ };
129
+ }
130
+ //#endregion
131
+ export { NOTIFY_BACKOFF, NOTIFY_MAX_ATTEMPTS, createNotifyEnqueuer, createNotifyHandler, notifyExhausted };
132
+
133
+ //# sourceMappingURL=notify-job.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify-job.mjs","names":[],"sources":["../../../src/capture/build-state/notify-job.ts"],"sourcesContent":["/**\n * The `notify` job kind (ADR 0014, decision 7; ADR 0006, decision 4): one job per `(channel,\n * build)` state change. The job names a channel and a build, never a status: the handler reads the\n * build when it runs, so a retried or reordered job cannot post a stale state, and a queued job\n * for the same pair already covers a newer request.\n */\nimport { CaptureJobModel } from \"../../models/capture-job.ts\";\nimport { NotificationChannelModel } from \"../../models/notification-channel.ts\";\nimport { sanitizeErrorText } from \"../../utils/redact.ts\";\nimport { isFinalAttempt } from \"../consumer/retry.ts\";\nimport type { ConsumerJob, KindHandler } from \"../consumer/types.ts\";\nimport { decodePayload } from \"../db-queue/codec.ts\";\nimport type { QueueJobInput } from \"../db-queue/types.ts\";\nimport { deliverBuildState } from \"./deliver.ts\";\nimport type { BuildStateDeps } from \"./types.ts\";\n\n/** Attempts a delivery gets before it is given up (the first try and four retries). */\nexport const NOTIFY_MAX_ATTEMPTS = 5;\n\n/** Retry delay of a delivery: 10 s, then doubling to at most five minutes. */\nexport const NOTIFY_BACKOFF = { baseMs: 10_000, maxMs: 5 * 60_000 } as const;\n\n/** Longest error text kept on a channel row. */\nconst MAX_ERROR_LENGTH = 500;\n\n/** A delivery that was given up: the channel shows the error and the admin event fires. */\nexport interface NotifyFailure {\n channelId: string;\n projectId: string | null;\n provider: string;\n buildId: string;\n /** Sanitized error text. */\n error: string;\n}\n\n/** What {@link createNotifyHandler} needs. */\nexport interface NotifyHandlerOptions {\n deps: BuildStateDeps;\n /** Called once when a delivery is given up, after the channel row recorded the error. Failures are logged. */\n onFailed?: (failure: NotifyFailure) => Promise<void>;\n}\n\n/** The channel a `notify` job delivers to. */\nfunction channelIdOf(job: Pick<ConsumerJob, \"data\">): string {\n const id = job.data?.[\"channelId\"];\n return typeof id === \"string\" ? id : \"\";\n}\n\n/**\n * The handler. Resolving acks the job (also when the build or channel is gone: nothing is left to\n * show). A refused status rejects, so the consumer requeues with {@link NOTIFY_BACKOFF}; the last\n * attempt records the error on the channel and raises `onFailed`.\n */\nexport function createNotifyHandler(options: NotifyHandlerOptions): KindHandler {\n const { deps } = options;\n return {\n accepts: (job) => job.buildId.length > 0 && channelIdOf(job).length > 0,\n backoff: NOTIFY_BACKOFF,\n run: async (job, context) => {\n const channelId = channelIdOf(job);\n try {\n const result = await deliverBuildState(deps, { buildId: job.buildId, channelId });\n if (result === \"delivered\") {\n await channelModel(deps).recordOutcome(channelId);\n } else {\n context.logger.info({ result }, \"nothing to deliver\");\n }\n } catch (error) {\n if (isFinalAttempt(job)) {\n await surfaceFailure(options, { channelId, buildId: job.buildId, error });\n }\n throw error;\n }\n },\n };\n}\n\n/**\n * For the queue's `onExhausted` hook: a last attempt that lost its lease never ran its handler to\n * the end, so the failure is surfaced here (the hook may run on several instances; recording the\n * same error twice is harmless, the admin event may repeat).\n */\nexport function notifyExhausted(\n options: NotifyHandlerOptions,\n): (job: ConsumerJob, error: string) => Promise<void> {\n return async (job, error) => {\n await surfaceFailure(options, { channelId: channelIdOf(job), buildId: job.buildId, error });\n };\n}\n\nasync function surfaceFailure(\n options: NotifyHandlerOptions,\n failure: { channelId: string; buildId: string; error: unknown },\n): Promise<void> {\n const { deps } = options;\n const text = sanitizeErrorText(errorText(failure.error), MAX_ERROR_LENGTH);\n deps.logger.error(\n { channelId: failure.channelId, buildId: failure.buildId, error: text },\n \"state delivery given up\",\n );\n try {\n await recordAndAlert(options, { ...failure, error: text });\n } catch (error) {\n deps.logger.warn({ err: error, channelId: failure.channelId }, \"failed to record the failure\");\n }\n}\n\n/** Put the error on the channel row, then raise `onFailed`; a channel that is gone needs neither. */\nasync function recordAndAlert(\n options: NotifyHandlerOptions,\n failure: { channelId: string; buildId: string; error: string },\n): Promise<void> {\n const model = channelModel(options.deps);\n const row = await model.get(failure.channelId);\n if (row === null) {\n return;\n }\n await model.recordOutcome(row.id, failure.error);\n await options.onFailed?.({\n channelId: row.id,\n projectId: row.projectId,\n provider: row.provider,\n buildId: failure.buildId,\n error: failure.error,\n });\n}\n\nfunction errorText(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction channelModel(deps: BuildStateDeps): NotificationChannelModel {\n return new NotificationChannelModel(\n deps.db,\n { notificationChannels: deps.db.tables.notificationChannels },\n deps.secret,\n );\n}\n\n/** The slice of the database queue the enqueuer uses. */\nexport interface NotifyJobQueue {\n enqueueJob(job: QueueJobInput): Promise<string>;\n}\n\n/**\n * Enqueue a `notify` job for a channel and build, unless one is already waiting: it will read the\n * build's state when it runs, so a second would deliver the same thing. A job that is already\n * running may have read an older state, so a new one is queued behind it.\n */\nexport function createNotifyEnqueuer(\n deps: Pick<BuildStateDeps, \"db\">,\n queue: NotifyJobQueue,\n): (job: { channelId: string; buildId: string }) => Promise<void> {\n return async ({ channelId, buildId }) => {\n // Built per call: a host that never enqueues must not touch `capture_jobs`.\n const rows = await new CaptureJobModel(deps.db).listForBuild(buildId);\n const waiting = rows.some(\n (row) =>\n row.kind === \"notify\" &&\n row.status === \"queued\" &&\n decodePayload(row.payload).data?.[\"channelId\"] === channelId,\n );\n if (!waiting) {\n await queue.enqueueJob({\n kind: \"notify\",\n buildId,\n data: { channelId },\n maxAttempts: NOTIFY_MAX_ATTEMPTS,\n });\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,MAAa,sBAAsB;;AAGnC,MAAa,iBAAiB;CAAE,QAAQ;CAAQ,OAAO;AAAW;;AAGlE,MAAM,mBAAmB;;AAoBzB,SAAS,YAAY,KAAwC;CAC3D,MAAM,KAAK,IAAI,OAAO;CACtB,OAAO,OAAO,OAAO,WAAW,KAAK;AACvC;;;;;;AAOA,SAAgB,oBAAoB,SAA4C;CAC9E,MAAM,EAAE,SAAS;CACjB,OAAO;EACL,UAAU,QAAQ,IAAI,QAAQ,SAAS,KAAK,YAAY,GAAG,CAAC,CAAC,SAAS;EACtE,SAAS;EACT,KAAK,OAAO,KAAK,YAAY;GAC3B,MAAM,YAAY,YAAY,GAAG;GACjC,IAAI;IACF,MAAM,SAAS,MAAM,kBAAkB,MAAM;KAAE,SAAS,IAAI;KAAS;IAAU,CAAC;IAChF,IAAI,WAAW,aACb,MAAM,aAAa,IAAI,CAAC,CAAC,cAAc,SAAS;SAEhD,QAAQ,OAAO,KAAK,EAAE,OAAO,GAAG,oBAAoB;GAExD,SAAS,OAAO;IACd,IAAI,eAAe,GAAG,GACpB,MAAM,eAAe,SAAS;KAAE;KAAW,SAAS,IAAI;KAAS;IAAM,CAAC;IAE1E,MAAM;GACR;EACF;CACF;AACF;;;;;;AAOA,SAAgB,gBACd,SACoD;CACpD,OAAO,OAAO,KAAK,UAAU;EAC3B,MAAM,eAAe,SAAS;GAAE,WAAW,YAAY,GAAG;GAAG,SAAS,IAAI;GAAS;EAAM,CAAC;CAC5F;AACF;AAEA,eAAe,eACb,SACA,SACe;CACf,MAAM,EAAE,SAAS;CACjB,MAAM,OAAO,kBAAkB,UAAU,QAAQ,KAAK,GAAG,gBAAgB;CACzE,KAAK,OAAO,MACV;EAAE,WAAW,QAAQ;EAAW,SAAS,QAAQ;EAAS,OAAO;CAAK,GACtE,yBACF;CACA,IAAI;EACF,MAAM,eAAe,SAAS;GAAE,GAAG;GAAS,OAAO;EAAK,CAAC;CAC3D,SAAS,OAAO;EACd,KAAK,OAAO,KAAK;GAAE,KAAK;GAAO,WAAW,QAAQ;EAAU,GAAG,8BAA8B;CAC/F;AACF;;AAGA,eAAe,eACb,SACA,SACe;CACf,MAAM,QAAQ,aAAa,QAAQ,IAAI;CACvC,MAAM,MAAM,MAAM,MAAM,IAAI,QAAQ,SAAS;CAC7C,IAAI,QAAQ,MACV;CAEF,MAAM,MAAM,cAAc,IAAI,IAAI,QAAQ,KAAK;CAC/C,MAAM,QAAQ,WAAW;EACvB,WAAW,IAAI;EACf,WAAW,IAAI;EACf,UAAU,IAAI;EACd,SAAS,QAAQ;EACjB,OAAO,QAAQ;CACjB,CAAC;AACH;AAEA,SAAS,UAAU,OAAwB;CACzC,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAEA,SAAS,aAAa,MAAgD;CACpE,OAAO,IAAI,yBACT,KAAK,IACL,EAAE,sBAAsB,KAAK,GAAG,OAAO,qBAAqB,GAC5D,KAAK,MACP;AACF;;;;;;AAYA,SAAgB,qBACd,MACA,OACgE;CAChE,OAAO,OAAO,EAAE,WAAW,cAAc;EASvC,IAAI,EANY,MADG,IAAI,gBAAgB,KAAK,EAAE,CAAC,CAAC,aAAa,OAAO,EAAA,CAC/C,MAClB,QACC,IAAI,SAAS,YACb,IAAI,WAAW,YACf,cAAc,IAAI,OAAO,CAAC,CAAC,OAAO,iBAAiB,SAE5C,GACT,MAAM,MAAM,WAAW;GACrB,MAAM;GACN;GACA,MAAM,EAAE,UAAU;GAClB,aAAA;EACF,CAAC;CAEL;AACF"}
@@ -0,0 +1,23 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { BuildStateDeps, BuildStatePublisher } from "./types.mjs";
4
+ //#region src/capture/build-state/publisher.d.ts
5
+ /** What {@link createBuildStatePublisher} needs. */
6
+ interface BuildStatePublisherOptions {
7
+ deps: BuildStateDeps;
8
+ /**
9
+ * Queues a `notify` job. Set when this instance (or another one) consumes the queue; absent, the
10
+ * state is delivered inline in the caller's request, without durability.
11
+ */
12
+ enqueue?: ((job: {
13
+ channelId: string;
14
+ buildId: string;
15
+ }) => Promise<void>) | undefined;
16
+ }
17
+ /** Logged once when the first request finds no public origin, and again after one came back and went. */
18
+ declare const PAUSED_MESSAGE = "notifications are paused until a public URL is set (PUBLIC_BASE_URL or a confirmed detected origin)";
19
+ /** A publisher over `options.deps`. */
20
+ declare function createBuildStatePublisher(options: BuildStatePublisherOptions): BuildStatePublisher;
21
+ //#endregion
22
+ export { BuildStatePublisherOptions, PAUSED_MESSAGE, createBuildStatePublisher };
23
+ //# sourceMappingURL=publisher.d.mts.map
@@ -0,0 +1,107 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { sanitizeErrorText } from "../../utils/redact.mjs";
5
+ import { NotificationChannelModel } from "../../models/notification-channel.mjs";
6
+ import { loadBuild, stateTargets, writeTo } from "./deliver.mjs";
7
+ //#region src/capture/build-state/publisher.ts
8
+ /**
9
+ * The one entry point for "this build changed, show it" (ADR 0014, decisions 5 and 7). Every build
10
+ * transition and the end of publishing call `request`; it hands each git channel of the project a
11
+ * durable `notify` job, or, on a host with no queue consumer, delivers inline. While no public
12
+ * origin is resolved it does neither (ADR 0014, decision 11): every link would be empty.
13
+ */
14
+ /** Logged once when the first request finds no public origin, and again after one came back and went. */
15
+ const PAUSED_MESSAGE = "notifications are paused until a public URL is set (PUBLIC_BASE_URL or a confirmed detected origin)";
16
+ /** A publisher over `options.deps`. */
17
+ function createBuildStatePublisher(options) {
18
+ const { deps } = options;
19
+ const pause = { logged: false };
20
+ return { request: async (buildId) => {
21
+ try {
22
+ const origin = await deps.publicOrigin();
23
+ if (!origin) {
24
+ logPaused(deps, pause, buildId);
25
+ return;
26
+ }
27
+ pause.logged = false;
28
+ await requestAll(options, {
29
+ buildId,
30
+ origin
31
+ });
32
+ } catch (error) {
33
+ deps.logger.warn({
34
+ err: error,
35
+ buildId
36
+ }, "failed to request the build state");
37
+ }
38
+ } };
39
+ }
40
+ /** Warn on the first request of a pause only. */
41
+ function logPaused(deps, pause, buildId) {
42
+ if (!pause.logged) deps.logger.warn({ buildId }, PAUSED_MESSAGE);
43
+ pause.logged = true;
44
+ }
45
+ /** Ask every state channel of the build's project to show it. */
46
+ async function requestAll(options, where) {
47
+ const { deps } = options;
48
+ const { buildId, origin } = where;
49
+ const loaded = await loadBuild(deps, buildId);
50
+ if (loaded === null) return;
51
+ const targets = await stateTargets(deps, loaded.project.id);
52
+ await Promise.allSettled(targets.map(async (target) => {
53
+ await requestOne(options, {
54
+ buildId,
55
+ loaded,
56
+ target,
57
+ origin
58
+ });
59
+ }));
60
+ }
61
+ /** Queue the delivery; inline when there is no queue, or when queueing itself failed. */
62
+ async function requestOne(options, request) {
63
+ const { enqueue, deps } = options;
64
+ const { buildId, loaded, target, origin } = request;
65
+ if (enqueue !== void 0) try {
66
+ await enqueue({
67
+ channelId: target.channel.id,
68
+ buildId
69
+ });
70
+ return;
71
+ } catch (error) {
72
+ deps.logger.warn({
73
+ err: error,
74
+ channelId: target.channel.id,
75
+ buildId
76
+ }, "failed to queue the state delivery; delivering inline");
77
+ }
78
+ await deliverInline(deps, loaded, {
79
+ target,
80
+ origin
81
+ });
82
+ }
83
+ /** Deliver now and record the outcome on the channel; never throws. */
84
+ async function deliverInline(deps, loaded, where) {
85
+ const { target } = where;
86
+ const channels = new NotificationChannelModel(deps.db, { notificationChannels: deps.db.tables.notificationChannels }, deps.secret);
87
+ let error;
88
+ try {
89
+ await writeTo(deps, loaded, where);
90
+ } catch (writeError) {
91
+ deps.logger.warn({
92
+ err: writeError,
93
+ channelId: target.channel.id
94
+ }, "state delivery failed");
95
+ error = sanitizeErrorText(writeError instanceof Error ? writeError.message : String(writeError), 500);
96
+ }
97
+ await channels.recordOutcome(target.channel.id, error).catch((recordError) => {
98
+ deps.logger.warn({
99
+ err: recordError,
100
+ channelId: target.channel.id
101
+ }, "failed to record outcome");
102
+ });
103
+ }
104
+ //#endregion
105
+ export { PAUSED_MESSAGE, createBuildStatePublisher };
106
+
107
+ //# sourceMappingURL=publisher.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publisher.mjs","names":[],"sources":["../../../src/capture/build-state/publisher.ts"],"sourcesContent":["/**\n * The one entry point for \"this build changed, show it\" (ADR 0014, decisions 5 and 7). Every build\n * transition and the end of publishing call `request`; it hands each git channel of the project a\n * durable `notify` job, or, on a host with no queue consumer, delivers inline. While no public\n * origin is resolved it does neither (ADR 0014, decision 11): every link would be empty.\n */\nimport { NotificationChannelModel } from \"../../models/notification-channel.ts\";\nimport { sanitizeErrorText } from \"../../utils/redact.ts\";\nimport { loadBuild, stateTargets, writeTo } from \"./deliver.ts\";\nimport type { BuildStateDeps, BuildStatePublisher, StateTarget } from \"./types.ts\";\n\n/** What {@link createBuildStatePublisher} needs. */\nexport interface BuildStatePublisherOptions {\n deps: BuildStateDeps;\n /**\n * Queues a `notify` job. Set when this instance (or another one) consumes the queue; absent, the\n * state is delivered inline in the caller's request, without durability.\n */\n enqueue?: ((job: { channelId: string; buildId: string }) => Promise<void>) | undefined;\n}\n\n/** Logged once when the first request finds no public origin, and again after one came back and went. */\nexport const PAUSED_MESSAGE =\n \"notifications are paused until a public URL is set (PUBLIC_BASE_URL or a confirmed detected origin)\";\n\n/** A publisher over `options.deps`. */\nexport function createBuildStatePublisher(\n options: BuildStatePublisherOptions,\n): BuildStatePublisher {\n const { deps } = options;\n const pause = { logged: false };\n return {\n request: async (buildId) => {\n try {\n const origin = await deps.publicOrigin();\n if (!origin) {\n logPaused(deps, pause, buildId);\n return;\n }\n pause.logged = false;\n await requestAll(options, { buildId, origin });\n } catch (error) {\n deps.logger.warn({ err: error, buildId }, \"failed to request the build state\");\n }\n },\n };\n}\n\n/** Warn on the first request of a pause only. */\nfunction logPaused(deps: BuildStateDeps, pause: { logged: boolean }, buildId: string): void {\n if (!pause.logged) {\n deps.logger.warn({ buildId }, PAUSED_MESSAGE);\n }\n pause.logged = true;\n}\n\n/** Ask every state channel of the build's project to show it. */\nasync function requestAll(\n options: BuildStatePublisherOptions,\n where: { buildId: string; origin: string },\n): Promise<void> {\n const { deps } = options;\n const { buildId, origin } = where;\n const loaded = await loadBuild(deps, buildId);\n if (loaded === null) {\n return;\n }\n const targets = await stateTargets(deps, loaded.project.id);\n await Promise.allSettled(\n targets.map(async (target) => {\n await requestOne(options, { buildId, loaded, target, origin });\n }),\n );\n}\n\n/** Queue the delivery; inline when there is no queue, or when queueing itself failed. */\nasync function requestOne(\n options: BuildStatePublisherOptions,\n request: {\n buildId: string;\n loaded: NonNullable<Awaited<ReturnType<typeof loadBuild>>>;\n target: StateTarget;\n origin: string;\n },\n): Promise<void> {\n const { enqueue, deps } = options;\n const { buildId, loaded, target, origin } = request;\n if (enqueue !== undefined) {\n try {\n await enqueue({ channelId: target.channel.id, buildId });\n return;\n } catch (error) {\n deps.logger.warn(\n { err: error, channelId: target.channel.id, buildId },\n \"failed to queue the state delivery; delivering inline\",\n );\n }\n }\n await deliverInline(deps, loaded, { target, origin });\n}\n\n/** Deliver now and record the outcome on the channel; never throws. */\nasync function deliverInline(\n deps: BuildStateDeps,\n loaded: Parameters<typeof writeTo>[1],\n where: Parameters<typeof writeTo>[2],\n): Promise<void> {\n const { target } = where;\n const channels = new NotificationChannelModel(\n deps.db,\n { notificationChannels: deps.db.tables.notificationChannels },\n deps.secret,\n );\n let error: string | undefined;\n try {\n await writeTo(deps, loaded, where);\n } catch (writeError) {\n deps.logger.warn({ err: writeError, channelId: target.channel.id }, \"state delivery failed\");\n error = sanitizeErrorText(\n writeError instanceof Error ? writeError.message : String(writeError),\n 500,\n );\n }\n await channels.recordOutcome(target.channel.id, error).catch((recordError: unknown) => {\n deps.logger.warn(\n { err: recordError, channelId: target.channel.id },\n \"failed to record outcome\",\n );\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAsBA,MAAa,iBACX;;AAGF,SAAgB,0BACd,SACqB;CACrB,MAAM,EAAE,SAAS;CACjB,MAAM,QAAQ,EAAE,QAAQ,MAAM;CAC9B,OAAO,EACL,SAAS,OAAO,YAAY;EAC1B,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,aAAa;GACvC,IAAI,CAAC,QAAQ;IACX,UAAU,MAAM,OAAO,OAAO;IAC9B;GACF;GACA,MAAM,SAAS;GACf,MAAM,WAAW,SAAS;IAAE;IAAS;GAAO,CAAC;EAC/C,SAAS,OAAO;GACd,KAAK,OAAO,KAAK;IAAE,KAAK;IAAO;GAAQ,GAAG,mCAAmC;EAC/E;CACF,EACF;AACF;;AAGA,SAAS,UAAU,MAAsB,OAA4B,SAAuB;CAC1F,IAAI,CAAC,MAAM,QACT,KAAK,OAAO,KAAK,EAAE,QAAQ,GAAG,cAAc;CAE9C,MAAM,SAAS;AACjB;;AAGA,eAAe,WACb,SACA,OACe;CACf,MAAM,EAAE,SAAS;CACjB,MAAM,EAAE,SAAS,WAAW;CAC5B,MAAM,SAAS,MAAM,UAAU,MAAM,OAAO;CAC5C,IAAI,WAAW,MACb;CAEF,MAAM,UAAU,MAAM,aAAa,MAAM,OAAO,QAAQ,EAAE;CAC1D,MAAM,QAAQ,WACZ,QAAQ,IAAI,OAAO,WAAW;EAC5B,MAAM,WAAW,SAAS;GAAE;GAAS;GAAQ;GAAQ;EAAO,CAAC;CAC/D,CAAC,CACH;AACF;;AAGA,eAAe,WACb,SACA,SAMe;CACf,MAAM,EAAE,SAAS,SAAS;CAC1B,MAAM,EAAE,SAAS,QAAQ,QAAQ,WAAW;CAC5C,IAAI,YAAY,KAAA,GACd,IAAI;EACF,MAAM,QAAQ;GAAE,WAAW,OAAO,QAAQ;GAAI;EAAQ,CAAC;EACvD;CACF,SAAS,OAAO;EACd,KAAK,OAAO,KACV;GAAE,KAAK;GAAO,WAAW,OAAO,QAAQ;GAAI;EAAQ,GACpD,uDACF;CACF;CAEF,MAAM,cAAc,MAAM,QAAQ;EAAE;EAAQ;CAAO,CAAC;AACtD;;AAGA,eAAe,cACb,MACA,QACA,OACe;CACf,MAAM,EAAE,WAAW;CACnB,MAAM,WAAW,IAAI,yBACnB,KAAK,IACL,EAAE,sBAAsB,KAAK,GAAG,OAAO,qBAAqB,GAC5D,KAAK,MACP;CACA,IAAI;CACJ,IAAI;EACF,MAAM,QAAQ,MAAM,QAAQ,KAAK;CACnC,SAAS,YAAY;EACnB,KAAK,OAAO,KAAK;GAAE,KAAK;GAAY,WAAW,OAAO,QAAQ;EAAG,GAAG,uBAAuB;EAC3F,QAAQ,kBACN,sBAAsB,QAAQ,WAAW,UAAU,OAAO,UAAU,GACpE,GACF;CACF;CACA,MAAM,SAAS,cAAc,OAAO,QAAQ,IAAI,KAAK,CAAC,CAAC,OAAO,gBAAyB;EACrF,KAAK,OAAO,KACV;GAAE,KAAK;GAAa,WAAW,OAAO,QAAQ;EAAG,GACjD,0BACF;CACF,CAAC;AACH"}
@@ -0,0 +1,9 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { BuildStateDeps, BuildStatePublisher } from "./types.mjs";
4
+ //#region src/capture/build-state/resume.d.ts
5
+ /** Ask `publisher` for the state of every open recent build; resolves to how many were asked. Never rejects. */
6
+ declare function requestOpenBuildStates(deps: Pick<BuildStateDeps, "db" | "logger">, publisher: BuildStatePublisher): Promise<number>;
7
+ //#endregion
8
+ export { requestOpenBuildStates };
9
+ //# sourceMappingURL=resume.d.mts.map
@@ -0,0 +1,47 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { BuildModel } from "../../models/build.mjs";
5
+ import { ProjectModel } from "../../models/project.mjs";
6
+ import { LOCAL_SHA_PREFIX } from "@storyshelf/affected";
7
+ //#region src/capture/build-state/resume.ts
8
+ /**
9
+ * Re-requesting the state of builds still open. Requests made while no public origin was resolved
10
+ * were dropped (ADR 0014, decision 11), so once one is, the builds that can still change are asked
11
+ * again; settled builds keep what they last showed until their next transition.
12
+ */
13
+ /** Builds that have not reached a verdict. */
14
+ const OPEN = /* @__PURE__ */ new Set([
15
+ "pending",
16
+ "capturing",
17
+ "comparing",
18
+ "reviewing"
19
+ ]);
20
+ /** Newest builds looked at per project. */
21
+ const PER_PROJECT = 25;
22
+ /** Ask `publisher` for the state of every open recent build; resolves to how many were asked. Never rejects. */
23
+ async function requestOpenBuildStates(deps, publisher) {
24
+ try {
25
+ const projects = await new ProjectModel(deps.db).list();
26
+ const ids = (await Promise.all(projects.map(async (project) => await openBuildIds(deps, project.id)))).flat();
27
+ await Promise.all(ids.map(async (id) => {
28
+ await publisher.request(id);
29
+ }));
30
+ return ids.length;
31
+ } catch (error) {
32
+ deps.logger.warn({ err: error }, "failed to re-request the state of open builds");
33
+ return 0;
34
+ }
35
+ }
36
+ /** The newest builds of a project that have not reached a verdict. */
37
+ async function openBuildIds(deps, projectId) {
38
+ return (await new BuildModel(deps.db).list(projectId, { limit: PER_PROJECT })).filter((build) => OPEN.has(build.status) && hasGitCommit(build.gitSha)).map((build) => build.id);
39
+ }
40
+ /** A build with no commit, or a local upload, has no git state to show (see `statusesForBuild`). */
41
+ function hasGitCommit(gitSha) {
42
+ return gitSha.length > 0 && !gitSha.startsWith(LOCAL_SHA_PREFIX);
43
+ }
44
+ //#endregion
45
+ export { requestOpenBuildStates };
46
+
47
+ //# sourceMappingURL=resume.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resume.mjs","names":[],"sources":["../../../src/capture/build-state/resume.ts"],"sourcesContent":["/**\n * Re-requesting the state of builds still open. Requests made while no public origin was resolved\n * were dropped (ADR 0014, decision 11), so once one is, the builds that can still change are asked\n * again; settled builds keep what they last showed until their next transition.\n */\nimport { LOCAL_SHA_PREFIX } from \"@storyshelf/affected\";\nimport { BuildModel } from \"../../models/build.ts\";\nimport { ProjectModel } from \"../../models/project.ts\";\nimport type { BuildStatus } from \"../../types.ts\";\nimport type { BuildStateDeps, BuildStatePublisher } from \"./types.ts\";\n\n/** Builds that have not reached a verdict. */\nconst OPEN: ReadonlySet<BuildStatus> = new Set([\"pending\", \"capturing\", \"comparing\", \"reviewing\"]);\n\n/** Newest builds looked at per project. */\nconst PER_PROJECT = 25;\n\n/** Ask `publisher` for the state of every open recent build; resolves to how many were asked. Never rejects. */\nexport async function requestOpenBuildStates(\n deps: Pick<BuildStateDeps, \"db\" | \"logger\">,\n publisher: BuildStatePublisher,\n): Promise<number> {\n try {\n const projects = await new ProjectModel(deps.db).list();\n const lists = await Promise.all(\n projects.map(async (project) => await openBuildIds(deps, project.id)),\n );\n const ids = lists.flat();\n await Promise.all(\n ids.map(async (id) => {\n await publisher.request(id);\n }),\n );\n return ids.length;\n } catch (error) {\n deps.logger.warn({ err: error }, \"failed to re-request the state of open builds\");\n return 0;\n }\n}\n\n/** The newest builds of a project that have not reached a verdict. */\nasync function openBuildIds(\n deps: Pick<BuildStateDeps, \"db\">,\n projectId: string,\n): Promise<string[]> {\n const recent = await new BuildModel(deps.db).list(projectId, { limit: PER_PROJECT });\n return recent\n .filter((build) => OPEN.has(build.status) && hasGitCommit(build.gitSha))\n .map((build) => build.id);\n}\n\n/** A build with no commit, or a local upload, has no git state to show (see `statusesForBuild`). */\nfunction hasGitCommit(gitSha: string): boolean {\n return gitSha.length > 0 && !gitSha.startsWith(LOCAL_SHA_PREFIX);\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,MAAM,uBAAiC,IAAI,IAAI;CAAC;CAAW;CAAa;CAAa;AAAW,CAAC;;AAGjG,MAAM,cAAc;;AAGpB,eAAsB,uBACpB,MACA,WACiB;CACjB,IAAI;EACF,MAAM,WAAW,MAAM,IAAI,aAAa,KAAK,EAAE,CAAC,CAAC,KAAK;EAItD,MAAM,OAAM,MAHQ,QAAQ,IAC1B,SAAS,IAAI,OAAO,YAAY,MAAM,aAAa,MAAM,QAAQ,EAAE,CAAC,CACtE,EAAA,CACkB,KAAK;EACvB,MAAM,QAAQ,IACZ,IAAI,IAAI,OAAO,OAAO;GACpB,MAAM,UAAU,QAAQ,EAAE;EAC5B,CAAC,CACH;EACA,OAAO,IAAI;CACb,SAAS,OAAO;EACd,KAAK,OAAO,KAAK,EAAE,KAAK,MAAM,GAAG,+CAA+C;EAChF,OAAO;CACT;AACF;;AAGA,eAAe,aACb,MACA,WACmB;CAEnB,QAAO,MADc,IAAI,WAAW,KAAK,EAAE,CAAC,CAAC,KAAK,WAAW,EAAE,OAAO,YAAY,CAAC,EAAA,CAEhF,QAAQ,UAAU,KAAK,IAAI,MAAM,MAAM,KAAK,aAAa,MAAM,MAAM,CAAC,CAAC,CACvE,KAAK,UAAU,MAAM,EAAE;AAC5B;;AAGA,SAAS,aAAa,QAAyB;CAC7C,OAAO,OAAO,SAAS,KAAK,CAAC,OAAO,WAAW,gBAAgB;AACjE"}
@@ -0,0 +1,49 @@
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 { Logger } from "../../logger.mjs";
5
+ import { NotificationChannel } from "../../adapters/notifier/types.mjs";
6
+ import { NotifierProvider, StateNotifierProvider } from "../../adapters/notifier/provider.mjs";
7
+ import { StorageAdapter } from "../../adapter/storage.mjs";
8
+ import { ShelfConfig } from "../../config.mjs";
9
+ import { BuildStateFlags } from "../../adapters/notifier/build-state.mjs";
10
+ import { SecretInput } from "../../utils/encrypt.mjs";
11
+ import { Project } from "../../schema/project.mjs";
12
+ //#region src/capture/build-state/types.d.ts
13
+ /** What delivering a build's state needs. */
14
+ interface BuildStateDeps {
15
+ db: DatabaseAdapter;
16
+ /** Read to tell whether the build's Storybook is published; without it publishing reads as pending. */
17
+ storage?: StorageAdapter | undefined;
18
+ /** Notifier providers; only the `state` ones (git hosts) take part. */
19
+ notifiers: readonly NotifierProvider[];
20
+ /** Server secret that decrypts channel tokens. */
21
+ secret: SecretInput;
22
+ /** The published-Storybook domain for Storybook links. */
23
+ config: Pick<ShelfConfig, "publishedBaseDomain">;
24
+ /**
25
+ * The resolved public origin (ADR 0007, decision 6): the origin every link starts with. While it
26
+ * resolves to nothing, no state is requested or delivered (ADR 0014, decision 11).
27
+ */
28
+ publicOrigin: () => Promise<string | undefined>;
29
+ /** Which statuses a project's builds show: `testing` from the server options, review from the project. */
30
+ flags: (project: Project) => BuildStateFlags;
31
+ logger: Logger;
32
+ }
33
+ /** A channel with the provider that delivers to it. */
34
+ interface StateTarget {
35
+ channel: NotificationChannel;
36
+ provider: StateNotifierProvider;
37
+ }
38
+ /** Why a delivery wrote nothing. */
39
+ type SkipReason = "no-build" | "no-channel" | "disabled" | "no-provider" | "no-origin";
40
+ /**
41
+ * Asks for a build's current state to be shown on its project's git channels. Never rejects: the
42
+ * request that triggered it must not fail because a git host did.
43
+ */
44
+ interface BuildStatePublisher {
45
+ request(buildId: string): Promise<void>;
46
+ }
47
+ //#endregion
48
+ export { BuildStateDeps, BuildStatePublisher, SkipReason, StateTarget };
49
+ //# sourceMappingURL=types.d.mts.map
@@ -0,0 +1,22 @@
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 { StorageAdapter } from "../../adapter/storage.mjs";
5
+ import { Catalog } from "./types.mjs";
6
+ //#region src/capture/catalog/catalogue.d.ts
7
+ /** What cataloguing reads and writes. */
8
+ interface CatalogDeps {
9
+ db: DatabaseAdapter;
10
+ storage: StorageAdapter;
11
+ }
12
+ /**
13
+ * Catalogue a build from its published statics: replace its `build_components` rows, then persist
14
+ * `catalog.json`. Resolves to the catalog, or null when the build has no readable index
15
+ * (statics purged, or not a Storybook), in which case nothing is written.
16
+ */
17
+ declare function catalogueBuild(deps: CatalogDeps, projectId: string, buildId: string): Promise<Catalog | null>;
18
+ /** A build's persisted catalog, or null when it has none (or an unreadable one). */
19
+ declare function readCatalog(storage: StorageAdapter, projectId: string, buildId: string): Promise<Catalog | null>;
20
+ //#endregion
21
+ export { CatalogDeps, catalogueBuild, readCatalog };
22
+ //# sourceMappingURL=catalogue.d.mts.map
@@ -0,0 +1,53 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { catalogPath } from "../../paths.mjs";
5
+ import { BuildComponentModel } from "../../models/build-component.mjs";
6
+ import { groupComponents, toBuildComponent } from "./components.mjs";
7
+ import { parseStorybookIndexText } from "./parse.mjs";
8
+ import { readPublishedFile } from "./read.mjs";
9
+ //#region src/capture/catalog/catalogue.ts
10
+ /** Index files a Storybook may publish, newest format first. */
11
+ const INDEX_FILES = ["index.json", "stories.json"];
12
+ /**
13
+ * Catalogue a build from its published statics: replace its `build_components` rows, then persist
14
+ * `catalog.json`. Resolves to the catalog, or null when the build has no readable index
15
+ * (statics purged, or not a Storybook), in which case nothing is written.
16
+ */
17
+ async function catalogueBuild(deps, projectId, buildId) {
18
+ const entries = await readIndexEntries(deps.storage, projectId, buildId);
19
+ if (!entries) return null;
20
+ const catalog = {
21
+ v: 1,
22
+ entries
23
+ };
24
+ await new BuildComponentModel(deps.db).replaceForBuild(projectId, buildId, groupComponents(entries).map((component) => toBuildComponent(component)));
25
+ await deps.storage.write(catalogPath(projectId, buildId), Buffer.from(JSON.stringify(catalog)));
26
+ return catalog;
27
+ }
28
+ /** A build's persisted catalog, or null when it has none (or an unreadable one). */
29
+ async function readCatalog(storage, projectId, buildId) {
30
+ const path = catalogPath(projectId, buildId);
31
+ try {
32
+ if (!await storage.exists(path)) return null;
33
+ const parsed = JSON.parse((await storage.read(path)).toString("utf8"));
34
+ return parsed.v === 1 && Array.isArray(parsed.entries) ? {
35
+ v: 1,
36
+ entries: parsed.entries
37
+ } : null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+ async function readIndexEntries(storage, projectId, buildId) {
43
+ for (const name of INDEX_FILES) {
44
+ const file = await readPublishedFile(storage, projectId, buildId, name).catch(() => null);
45
+ const entries = file ? parseStorybookIndexText(file.toString("utf8")) : null;
46
+ if (entries) return entries;
47
+ }
48
+ return null;
49
+ }
50
+ //#endregion
51
+ export { catalogueBuild, readCatalog };
52
+
53
+ //# sourceMappingURL=catalogue.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalogue.mjs","names":[],"sources":["../../../src/capture/catalog/catalogue.ts"],"sourcesContent":["/** Cataloguing a published build: read its Storybook index, persist `catalog.json` and component rows. */\nimport type { DatabaseAdapter } from \"../../adapters/database.ts\";\nimport type { StorageAdapter } from \"../../adapters/storage.ts\";\nimport { BuildComponentModel } from \"../../models/build-component.ts\";\nimport { catalogPath } from \"../../utils/paths.ts\";\nimport { groupComponents, toBuildComponent } from \"./components.ts\";\nimport { parseStorybookIndexText } from \"./parse.ts\";\nimport { readPublishedFile } from \"./read.ts\";\nimport type { Catalog, CatalogEntry } from \"./types.ts\";\n\n/** What cataloguing reads and writes. */\nexport interface CatalogDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n}\n\n/** Index files a Storybook may publish, newest format first. */\nconst INDEX_FILES = [\"index.json\", \"stories.json\"] as const;\n\n/**\n * Catalogue a build from its published statics: replace its `build_components` rows, then persist\n * `catalog.json`. Resolves to the catalog, or null when the build has no readable index\n * (statics purged, or not a Storybook), in which case nothing is written.\n */\nexport async function catalogueBuild(\n deps: CatalogDeps,\n projectId: string,\n buildId: string,\n): Promise<Catalog | null> {\n const entries = await readIndexEntries(deps.storage, projectId, buildId);\n if (!entries) {\n return null;\n }\n const catalog: Catalog = { v: 1, entries };\n // The rows go first: catalog.json is the marker that cataloguing finished, so a crash between the\n // two leaves a build the lazy loader catalogues again, never one with a catalog and no rows.\n await new BuildComponentModel(deps.db).replaceForBuild(\n projectId,\n buildId,\n groupComponents(entries).map((component) => toBuildComponent(component)),\n );\n await deps.storage.write(catalogPath(projectId, buildId), Buffer.from(JSON.stringify(catalog)));\n return catalog;\n}\n\n/** A build's persisted catalog, or null when it has none (or an unreadable one). */\nexport async function readCatalog(\n storage: StorageAdapter,\n projectId: string,\n buildId: string,\n): Promise<Catalog | null> {\n const path = catalogPath(projectId, buildId);\n try {\n if (!(await storage.exists(path))) {\n return null;\n }\n const parsed = JSON.parse((await storage.read(path)).toString(\"utf8\")) as Partial<Catalog>;\n return parsed.v === 1 && Array.isArray(parsed.entries)\n ? { v: 1, entries: parsed.entries }\n : null;\n } catch {\n return null;\n }\n}\n\nasync function readIndexEntries(\n storage: StorageAdapter,\n projectId: string,\n buildId: string,\n): Promise<CatalogEntry[] | null> {\n for (const name of INDEX_FILES) {\n // eslint-disable-next-line no-await-in-loop -- stop at the first index that parses\n const file = await readPublishedFile(storage, projectId, buildId, name).catch(() => null);\n const entries = file ? parseStorybookIndexText(file.toString(\"utf8\")) : null;\n if (entries) {\n return entries;\n }\n }\n return null;\n}\n"],"mappings":";;;;;;;;;;AAiBA,MAAM,cAAc,CAAC,cAAc,cAAc;;;;;;AAOjD,eAAsB,eACpB,MACA,WACA,SACyB;CACzB,MAAM,UAAU,MAAM,iBAAiB,KAAK,SAAS,WAAW,OAAO;CACvE,IAAI,CAAC,SACH,OAAO;CAET,MAAM,UAAmB;EAAE,GAAG;EAAG;CAAQ;CAGzC,MAAM,IAAI,oBAAoB,KAAK,EAAE,CAAC,CAAC,gBACrC,WACA,SACA,gBAAgB,OAAO,CAAC,CAAC,KAAK,cAAc,iBAAiB,SAAS,CAAC,CACzE;CACA,MAAM,KAAK,QAAQ,MAAM,YAAY,WAAW,OAAO,GAAG,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,CAAC;CAC9F,OAAO;AACT;;AAGA,eAAsB,YACpB,SACA,WACA,SACyB;CACzB,MAAM,OAAO,YAAY,WAAW,OAAO;CAC3C,IAAI;EACF,IAAI,CAAE,MAAM,QAAQ,OAAO,IAAI,GAC7B,OAAO;EAET,MAAM,SAAS,KAAK,OAAO,MAAM,QAAQ,KAAK,IAAI,EAAA,CAAG,SAAS,MAAM,CAAC;EACrE,OAAO,OAAO,MAAM,KAAK,MAAM,QAAQ,OAAO,OAAO,IACjD;GAAE,GAAG;GAAG,SAAS,OAAO;EAAQ,IAChC;CACN,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,iBACb,SACA,WACA,SACgC;CAChC,KAAK,MAAM,QAAQ,aAAa;EAE9B,MAAM,OAAO,MAAM,kBAAkB,SAAS,WAAW,SAAS,IAAI,CAAC,CAAC,YAAY,IAAI;EACxF,MAAM,UAAU,OAAO,wBAAwB,KAAK,SAAS,MAAM,CAAC,IAAI;EACxE,IAAI,SACF,OAAO;CAEX;CACA,OAAO;AACT"}
@@ -0,0 +1,23 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { CatalogComponent, CatalogEntry } from "./types.mjs";
4
+ import { BuildComponentInput } from "../../models/build-component.mjs";
5
+ //#region src/capture/catalog/components.d.ts
6
+ /** Group entries by story file (entries without one by title), ordered by title then file. */
7
+ declare function groupComponents(entries: readonly CatalogEntry[]): CatalogComponent[];
8
+ /** What `build_components` stores of a component. */
9
+ declare function toBuildComponent(component: CatalogComponent): BuildComponentInput;
10
+ /** SHA-256 over the entries' id, name, type and title, in id order: equal sets hash equal. */
11
+ declare function hashEntries(entries: readonly CatalogEntry[]): string;
12
+ /** How one component's entries differ between two builds. */
13
+ interface EntryChanges {
14
+ added: number;
15
+ removed: number;
16
+ /** Same id, new display name. */
17
+ renamed: number;
18
+ }
19
+ /** Entries added, removed and renamed from `before` to `after` (matched by entry id). */
20
+ declare function diffEntries(before: readonly CatalogEntry[], after: readonly CatalogEntry[]): EntryChanges;
21
+ //#endregion
22
+ export { EntryChanges, diffEntries, groupComponents, hashEntries, toBuildComponent };
23
+ //# sourceMappingURL=components.d.mts.map
@@ -0,0 +1,55 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { createHash } from "node:crypto";
5
+ //#region src/capture/catalog/components.ts
6
+ /** Catalog entries grouped into components, hashed, and compared between builds. */
7
+ /** Group entries by story file (entries without one by title), ordered by title then file. */
8
+ function groupComponents(entries) {
9
+ return [...Map.groupBy(entries, (entry) => entry.importPath || `title:${entry.title}`).values()].map((group) => {
10
+ const [first] = group;
11
+ return {
12
+ importPath: first?.importPath ?? "",
13
+ title: first?.title ?? "",
14
+ storyCount: group.filter((entry) => entry.type === "story").length,
15
+ hash: hashEntries(group),
16
+ entries: group
17
+ };
18
+ }).toSorted((a, b) => a.title.localeCompare(b.title) || a.importPath.localeCompare(b.importPath));
19
+ }
20
+ /** What `build_components` stores of a component. */
21
+ function toBuildComponent(component) {
22
+ const { importPath, title, storyCount, hash } = component;
23
+ return {
24
+ importPath,
25
+ title,
26
+ storyCount,
27
+ hash
28
+ };
29
+ }
30
+ /** SHA-256 over the entries' id, name, type and title, in id order: equal sets hash equal. */
31
+ function hashEntries(entries) {
32
+ const lines = entries.map((entry) => [
33
+ entry.id,
34
+ entry.name,
35
+ entry.type,
36
+ entry.title
37
+ ].join(" ")).toSorted();
38
+ return createHash("sha256").update(lines.join("\n")).digest("hex");
39
+ }
40
+ /** Entries added, removed and renamed from `before` to `after` (matched by entry id). */
41
+ function diffEntries(before, after) {
42
+ const was = new Map(before.map((entry) => [entry.id, entry]));
43
+ const now = new Map(after.map((entry) => [entry.id, entry]));
44
+ let renamed = 0;
45
+ for (const [id, entry] of now) if (was.has(id) && was.get(id)?.name !== entry.name) renamed += 1;
46
+ return {
47
+ added: [...now.keys()].filter((id) => !was.has(id)).length,
48
+ removed: [...was.keys()].filter((id) => !now.has(id)).length,
49
+ renamed
50
+ };
51
+ }
52
+ //#endregion
53
+ export { diffEntries, groupComponents, hashEntries, toBuildComponent };
54
+
55
+ //# sourceMappingURL=components.mjs.map