@storyshelf/app 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 (361) hide show
  1. package/README.md +42 -4
  2. package/dist/app-types.d.mts +1 -3
  3. package/dist/asset-manifest.mjs +13 -5
  4. package/dist/asset-manifest.mjs.map +1 -1
  5. package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
  6. package/dist/assets/htmx.mjs.map +1 -0
  7. package/dist/assets/mark.mjs +10 -0
  8. package/dist/assets/mark.mjs.map +1 -0
  9. package/dist/boot-tasks.mjs +80 -0
  10. package/dist/boot-tasks.mjs.map +1 -0
  11. package/dist/build-state-flags.mjs +18 -0
  12. package/dist/build-state-flags.mjs.map +1 -0
  13. package/dist/build-state-setup.mjs +75 -0
  14. package/dist/build-state-setup.mjs.map +1 -0
  15. package/dist/capture-alerts.mjs +46 -0
  16. package/dist/capture-alerts.mjs.map +1 -0
  17. package/dist/capture-process.mjs +59 -0
  18. package/dist/capture-process.mjs.map +1 -0
  19. package/dist/capture-setup.mjs +95 -117
  20. package/dist/capture-setup.mjs.map +1 -1
  21. package/dist/connections.mjs +34 -0
  22. package/dist/connections.mjs.map +1 -0
  23. package/dist/consumer-setup.mjs +58 -0
  24. package/dist/consumer-setup.mjs.map +1 -0
  25. package/dist/git-status.mjs +46 -0
  26. package/dist/git-status.mjs.map +1 -0
  27. package/dist/hidden-data.mjs +24 -0
  28. package/dist/hidden-data.mjs.map +1 -0
  29. package/dist/index.d.mts +3 -3
  30. package/dist/index.mjs +203 -125
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/insights/budget.mjs +49 -0
  33. package/dist/insights/budget.mjs.map +1 -0
  34. package/dist/insights/deps.mjs +21 -0
  35. package/dist/insights/deps.mjs.map +1 -0
  36. package/dist/insights/errors.mjs +22 -0
  37. package/dist/insights/errors.mjs.map +1 -0
  38. package/dist/insights/health-panel.mjs +40 -0
  39. package/dist/insights/health-panel.mjs.map +1 -0
  40. package/dist/insights/job.mjs +103 -0
  41. package/dist/insights/job.mjs.map +1 -0
  42. package/dist/insights/load-health.mjs +59 -0
  43. package/dist/insights/load-health.mjs.map +1 -0
  44. package/dist/insights/load-triage.mjs +98 -0
  45. package/dist/insights/load-triage.mjs.map +1 -0
  46. package/dist/insights/panel.mjs +28 -0
  47. package/dist/insights/panel.mjs.map +1 -0
  48. package/dist/insights/request.mjs +113 -0
  49. package/dist/insights/request.mjs.map +1 -0
  50. package/dist/insights/stale.mjs +21 -0
  51. package/dist/insights/stale.mjs.map +1 -0
  52. package/dist/insights/usage-report.mjs +60 -0
  53. package/dist/insights/usage-report.mjs.map +1 -0
  54. package/dist/insights/view.mjs +48 -0
  55. package/dist/insights/view.mjs.map +1 -0
  56. package/dist/job-canceller.mjs +14 -0
  57. package/dist/job-canceller.mjs.map +1 -0
  58. package/dist/job-queue.mjs +51 -0
  59. package/dist/job-queue.mjs.map +1 -0
  60. package/dist/jobs-setup.mjs +132 -0
  61. package/dist/jobs-setup.mjs.map +1 -0
  62. package/dist/lifecycle.mjs +91 -19
  63. package/dist/lifecycle.mjs.map +1 -1
  64. package/dist/middleware/auth-gate.mjs +4 -2
  65. package/dist/middleware/auth-gate.mjs.map +1 -1
  66. package/dist/middleware/rate-limit.mjs +41 -33
  67. package/dist/middleware/rate-limit.mjs.map +1 -1
  68. package/dist/middleware/request-log.mjs +3 -2
  69. package/dist/middleware/request-log.mjs.map +1 -1
  70. package/dist/middleware/require-review.mjs +51 -0
  71. package/dist/middleware/require-review.mjs.map +1 -0
  72. package/dist/middleware/require-testing.mjs +41 -0
  73. package/dist/middleware/require-testing.mjs.map +1 -0
  74. package/dist/middleware/setup-gate.mjs.map +1 -1
  75. package/dist/middleware/store-scope.mjs +12 -2
  76. package/dist/middleware/store-scope.mjs.map +1 -1
  77. package/dist/notifications-pause.mjs +33 -0
  78. package/dist/notifications-pause.mjs.map +1 -0
  79. package/dist/notify.d.mts +2 -1
  80. package/dist/notify.mjs +78 -39
  81. package/dist/notify.mjs.map +1 -1
  82. package/dist/pages/a11y-violations.mjs +116 -0
  83. package/dist/pages/a11y-violations.mjs.map +1 -0
  84. package/dist/pages/admin-ai.mjs +78 -0
  85. package/dist/pages/admin-ai.mjs.map +1 -0
  86. package/dist/pages/admin-system-origin.mjs +123 -0
  87. package/dist/pages/admin-system-origin.mjs.map +1 -0
  88. package/dist/pages/admin-system-targets.mjs +53 -0
  89. package/dist/pages/admin-system-targets.mjs.map +1 -0
  90. package/dist/pages/admin-system.mjs +63 -13
  91. package/dist/pages/admin-system.mjs.map +1 -1
  92. package/dist/pages/build-detail.mjs +104 -76
  93. package/dist/pages/build-detail.mjs.map +1 -1
  94. package/dist/pages/build-diff-actions.mjs +87 -33
  95. package/dist/pages/build-diff-actions.mjs.map +1 -1
  96. package/dist/pages/build-diff-comments.mjs +16 -14
  97. package/dist/pages/build-diff-comments.mjs.map +1 -1
  98. package/dist/pages/build-diff-environment.mjs +82 -0
  99. package/dist/pages/build-diff-environment.mjs.map +1 -0
  100. package/dist/pages/build-diff-header.mjs +22 -4
  101. package/dist/pages/build-diff-header.mjs.map +1 -1
  102. package/dist/pages/build-diff-insight.mjs +113 -0
  103. package/dist/pages/build-diff-insight.mjs.map +1 -0
  104. package/dist/pages/build-diff-model.mjs +55 -10
  105. package/dist/pages/build-diff-model.mjs.map +1 -1
  106. package/dist/pages/build-diff-nav.mjs +28 -12
  107. package/dist/pages/build-diff-nav.mjs.map +1 -1
  108. package/dist/pages/build-diff-shortcuts.mjs +5 -1
  109. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  110. package/dist/pages/build-diff-usage.mjs +63 -0
  111. package/dist/pages/build-diff-usage.mjs.map +1 -0
  112. package/dist/pages/build-diff-viewer.mjs +62 -5
  113. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  114. package/dist/pages/build-diff.mjs +34 -13
  115. package/dist/pages/build-diff.mjs.map +1 -1
  116. package/dist/pages/build-tests.mjs +74 -0
  117. package/dist/pages/build-tests.mjs.map +1 -0
  118. package/dist/pages/component-detail.mjs +249 -0
  119. package/dist/pages/component-detail.mjs.map +1 -0
  120. package/dist/pages/component-stories.mjs +168 -0
  121. package/dist/pages/component-stories.mjs.map +1 -0
  122. package/dist/pages/compute-jobs.mjs +13 -26
  123. package/dist/pages/compute-jobs.mjs.map +1 -1
  124. package/dist/pages/design-links.mjs +182 -0
  125. package/dist/pages/design-links.mjs.map +1 -0
  126. package/dist/pages/environment-change.mjs +53 -0
  127. package/dist/pages/environment-change.mjs.map +1 -0
  128. package/dist/pages/health-panel.mjs +126 -0
  129. package/dist/pages/health-panel.mjs.map +1 -0
  130. package/dist/pages/library-grid.mjs +105 -0
  131. package/dist/pages/library-grid.mjs.map +1 -0
  132. package/dist/pages/library-model.mjs +47 -0
  133. package/dist/pages/library-model.mjs.map +1 -0
  134. package/dist/pages/library-snapshots.mjs +130 -0
  135. package/dist/pages/library-snapshots.mjs.map +1 -0
  136. package/dist/pages/library.mjs +112 -218
  137. package/dist/pages/library.mjs.map +1 -1
  138. package/dist/pages/notifications-paused.mjs +22 -0
  139. package/dist/pages/notifications-paused.mjs.map +1 -0
  140. package/dist/pages/profile.mjs +53 -48
  141. package/dist/pages/profile.mjs.map +1 -1
  142. package/dist/pages/project-builds-table.mjs +2 -2
  143. package/dist/pages/project-builds-table.mjs.map +1 -1
  144. package/dist/pages/project-builds.mjs +51 -21
  145. package/dist/pages/project-builds.mjs.map +1 -1
  146. package/dist/pages/project-settings.mjs +51 -12
  147. package/dist/pages/project-settings.mjs.map +1 -1
  148. package/dist/pages/projects-card.mjs +14 -1
  149. package/dist/pages/projects-card.mjs.map +1 -1
  150. package/dist/pages/projects.mjs +6 -2
  151. package/dist/pages/projects.mjs.map +1 -1
  152. package/dist/pages/review-changeset.mjs +116 -0
  153. package/dist/pages/review-changeset.mjs.map +1 -0
  154. package/dist/pages/review-comments.mjs +103 -0
  155. package/dist/pages/review-comments.mjs.map +1 -0
  156. package/dist/pages/review-verdict.mjs +102 -0
  157. package/dist/pages/review-verdict.mjs.map +1 -0
  158. package/dist/pages/settings-ai.mjs +69 -0
  159. package/dist/pages/settings-ai.mjs.map +1 -0
  160. package/dist/pages/settings-capture.mjs +124 -0
  161. package/dist/pages/settings-capture.mjs.map +1 -0
  162. package/dist/pages/settings-design-credential.mjs +99 -0
  163. package/dist/pages/settings-design-credential.mjs.map +1 -0
  164. package/dist/pages/settings-design-links.mjs +179 -0
  165. package/dist/pages/settings-design-links.mjs.map +1 -0
  166. package/dist/pages/settings-design.mjs +48 -0
  167. package/dist/pages/settings-design.mjs.map +1 -0
  168. package/dist/pages/settings-general.mjs +46 -21
  169. package/dist/pages/settings-general.mjs.map +1 -1
  170. package/dist/pages/settings-labels.mjs +43 -2
  171. package/dist/pages/settings-labels.mjs.map +1 -1
  172. package/dist/pages/settings-links.mjs +112 -0
  173. package/dist/pages/settings-links.mjs.map +1 -0
  174. package/dist/pages/settings-notifications-git.mjs +128 -0
  175. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  176. package/dist/pages/settings-notifications.mjs +79 -42
  177. package/dist/pages/settings-notifications.mjs.map +1 -1
  178. package/dist/pages/settings-tests.mjs +33 -2
  179. package/dist/pages/settings-tests.mjs.map +1 -1
  180. package/dist/pages/target-choices.mjs +39 -0
  181. package/dist/pages/target-choices.mjs.map +1 -0
  182. package/dist/publish-setup.mjs +89 -0
  183. package/dist/publish-setup.mjs.map +1 -0
  184. package/dist/retention-timer.mjs +2 -1
  185. package/dist/retention-timer.mjs.map +1 -1
  186. package/dist/review-events.mjs +48 -0
  187. package/dist/review-events.mjs.map +1 -0
  188. package/dist/routers/admin-pages.mjs +45 -3
  189. package/dist/routers/admin-pages.mjs.map +1 -1
  190. package/dist/routers/admin-public-origin.mjs +85 -0
  191. package/dist/routers/admin-public-origin.mjs.map +1 -0
  192. package/dist/routers/assets.mjs +9 -2
  193. package/dist/routers/assets.mjs.map +1 -1
  194. package/dist/routers/auth.mjs +1 -1
  195. package/dist/routers/baseline-guard.mjs +2 -2
  196. package/dist/routers/baseline-guard.mjs.map +1 -1
  197. package/dist/routers/baselines.handlers.mjs +64 -0
  198. package/dist/routers/baselines.handlers.mjs.map +1 -0
  199. package/dist/routers/baselines.mjs +125 -0
  200. package/dist/routers/baselines.mjs.map +1 -0
  201. package/dist/routers/build-cancel.mjs +68 -0
  202. package/dist/routers/build-cancel.mjs.map +1 -0
  203. package/dist/routers/build-changeset.mjs +70 -0
  204. package/dist/routers/build-changeset.mjs.map +1 -0
  205. package/dist/routers/build-reviews.handlers.mjs +86 -0
  206. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  207. package/dist/routers/build-reviews.mjs +74 -0
  208. package/dist/routers/build-reviews.mjs.map +1 -0
  209. package/dist/routers/builds.handlers.mjs +46 -55
  210. package/dist/routers/builds.handlers.mjs.map +1 -1
  211. package/dist/routers/builds.mjs +71 -17
  212. package/dist/routers/builds.mjs.map +1 -1
  213. package/dist/routers/capture-config.mjs +35 -0
  214. package/dist/routers/capture-config.mjs.map +1 -0
  215. package/dist/routers/comments.mjs +41 -15
  216. package/dist/routers/comments.mjs.map +1 -1
  217. package/dist/routers/component-catalog.handlers.mjs +71 -0
  218. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  219. package/dist/routers/component.handlers.mjs +121 -0
  220. package/dist/routers/component.handlers.mjs.map +1 -0
  221. package/dist/routers/connections.mjs +118 -0
  222. package/dist/routers/connections.mjs.map +1 -0
  223. package/dist/routers/design-connect.handlers.mjs +44 -0
  224. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  225. package/dist/routers/design.handlers.mjs +114 -0
  226. package/dist/routers/design.handlers.mjs.map +1 -0
  227. package/dist/routers/health-report.mjs +2 -3
  228. package/dist/routers/health-report.mjs.map +1 -1
  229. package/dist/routers/health.mjs +4 -3
  230. package/dist/routers/health.mjs.map +1 -1
  231. package/dist/routers/helpers.mjs +19 -3
  232. package/dist/routers/helpers.mjs.map +1 -1
  233. package/dist/routers/insights-ui.mjs +64 -0
  234. package/dist/routers/insights-ui.mjs.map +1 -0
  235. package/dist/routers/insights.handlers.mjs +45 -0
  236. package/dist/routers/insights.handlers.mjs.map +1 -0
  237. package/dist/routers/insights.mjs +189 -0
  238. package/dist/routers/insights.mjs.map +1 -0
  239. package/dist/routers/links.handlers.mjs +80 -0
  240. package/dist/routers/links.handlers.mjs.map +1 -0
  241. package/dist/routers/links.mjs +139 -0
  242. package/dist/routers/links.mjs.map +1 -0
  243. package/dist/routers/media.mjs +18 -4
  244. package/dist/routers/media.mjs.map +1 -1
  245. package/dist/routers/members.mjs +2 -1
  246. package/dist/routers/members.mjs.map +1 -1
  247. package/dist/routers/notification-channels.mjs +17 -23
  248. package/dist/routers/notification-channels.mjs.map +1 -1
  249. package/dist/routers/oauth-state.mjs +45 -0
  250. package/dist/routers/oauth-state.mjs.map +1 -0
  251. package/dist/routers/preview-base.mjs +38 -0
  252. package/dist/routers/preview-base.mjs.map +1 -0
  253. package/dist/routers/preview-token.mjs +51 -0
  254. package/dist/routers/preview-token.mjs.map +1 -0
  255. package/dist/routers/profile.mjs +3 -1
  256. package/dist/routers/profile.mjs.map +1 -1
  257. package/dist/routers/projects.mjs +17 -1
  258. package/dist/routers/projects.mjs.map +1 -1
  259. package/dist/routers/publish-upload.mjs +78 -0
  260. package/dist/routers/publish-upload.mjs.map +1 -0
  261. package/dist/routers/published-access.mjs +52 -0
  262. package/dist/routers/published-access.mjs.map +1 -0
  263. package/dist/routers/published-files.mjs +52 -0
  264. package/dist/routers/published-files.mjs.map +1 -0
  265. package/dist/routers/review-forms.mjs +72 -0
  266. package/dist/routers/review-forms.mjs.map +1 -0
  267. package/dist/routers/review-section.handlers.mjs +85 -0
  268. package/dist/routers/review-section.handlers.mjs.map +1 -0
  269. package/dist/routers/review-switch-off.mjs +29 -0
  270. package/dist/routers/review-switch-off.mjs.map +1 -0
  271. package/dist/routers/schemas.mjs +140 -22
  272. package/dist/routers/schemas.mjs.map +1 -1
  273. package/dist/routers/scoped-models.mjs +22 -0
  274. package/dist/routers/scoped-models.mjs.map +1 -0
  275. package/dist/routers/settings-ai.mjs +35 -0
  276. package/dist/routers/settings-ai.mjs.map +1 -0
  277. package/dist/routers/settings-capture.mjs +77 -0
  278. package/dist/routers/settings-capture.mjs.map +1 -0
  279. package/dist/routers/settings-design.mjs +103 -0
  280. package/dist/routers/settings-design.mjs.map +1 -0
  281. package/dist/routers/settings-general.mjs +55 -10
  282. package/dist/routers/settings-general.mjs.map +1 -1
  283. package/dist/routers/settings-git-channel.mjs +77 -0
  284. package/dist/routers/settings-git-channel.mjs.map +1 -0
  285. package/dist/routers/settings-labels.mjs +27 -1
  286. package/dist/routers/settings-labels.mjs.map +1 -1
  287. package/dist/routers/settings-links.mjs +63 -0
  288. package/dist/routers/settings-links.mjs.map +1 -0
  289. package/dist/routers/settings-members.mjs +4 -7
  290. package/dist/routers/settings-members.mjs.map +1 -1
  291. package/dist/routers/settings-notifications.mjs +15 -10
  292. package/dist/routers/settings-notifications.mjs.map +1 -1
  293. package/dist/routers/settings-tokens.mjs +2 -5
  294. package/dist/routers/settings-tokens.mjs.map +1 -1
  295. package/dist/routers/settings-webhooks.mjs +7 -24
  296. package/dist/routers/settings-webhooks.mjs.map +1 -1
  297. package/dist/routers/settings.handlers.mjs +107 -26
  298. package/dist/routers/settings.handlers.mjs.map +1 -1
  299. package/dist/routers/settings.mjs +16 -4
  300. package/dist/routers/settings.mjs.map +1 -1
  301. package/dist/routers/signed-storybook.mjs +53 -0
  302. package/dist/routers/signed-storybook.mjs.map +1 -0
  303. package/dist/routers/snapshot-groups.mjs +63 -0
  304. package/dist/routers/snapshot-groups.mjs.map +1 -0
  305. package/dist/routers/snapshots.mjs +103 -5
  306. package/dist/routers/snapshots.mjs.map +1 -1
  307. package/dist/routers/storybook.mjs +37 -88
  308. package/dist/routers/storybook.mjs.map +1 -1
  309. package/dist/routers/tokens.mjs +2 -5
  310. package/dist/routers/tokens.mjs.map +1 -1
  311. package/dist/routers/ui.mjs +34 -5
  312. package/dist/routers/ui.mjs.map +1 -1
  313. package/dist/routers/webhooks.mjs +5 -11
  314. package/dist/routers/webhooks.mjs.map +1 -1
  315. package/dist/routers/well-known.mjs +11 -4
  316. package/dist/routers/well-known.mjs.map +1 -1
  317. package/dist/routes/api-routes.mjs +93 -0
  318. package/dist/routes/api-routes.mjs.map +1 -0
  319. package/dist/routes/classification.mjs +38 -0
  320. package/dist/routes/classification.mjs.map +1 -0
  321. package/dist/routes/openapi-classes.mjs +22 -0
  322. package/dist/routes/openapi-classes.mjs.map +1 -0
  323. package/dist/routes/page-routes.mjs +104 -0
  324. package/dist/routes/page-routes.mjs.map +1 -0
  325. package/dist/runtime.mjs +18 -13
  326. package/dist/runtime.mjs.map +1 -1
  327. package/dist/serverless.mjs +5 -0
  328. package/dist/store.mjs +19 -1
  329. package/dist/store.mjs.map +1 -1
  330. package/dist/targets.mjs +46 -0
  331. package/dist/targets.mjs.map +1 -0
  332. package/dist/ui/buttons.mjs +16 -3
  333. package/dist/ui/buttons.mjs.map +1 -1
  334. package/dist/ui/design-reference.mjs +97 -0
  335. package/dist/ui/design-reference.mjs.map +1 -0
  336. package/dist/ui/document.mjs +1 -2
  337. package/dist/ui/document.mjs.map +1 -1
  338. package/dist/ui/forms.mjs +6 -5
  339. package/dist/ui/forms.mjs.map +1 -1
  340. package/dist/ui/icons/paths.mjs +1 -0
  341. package/dist/ui/icons/paths.mjs.map +1 -1
  342. package/dist/ui/preview-embed.mjs +82 -0
  343. package/dist/ui/preview-embed.mjs.map +1 -0
  344. package/dist/ui/shell/client-script.mjs +85 -0
  345. package/dist/ui/shell/client-script.mjs.map +1 -1
  346. package/dist/ui/shell/nav.mjs +12 -8
  347. package/dist/ui/shell/nav.mjs.map +1 -1
  348. package/dist/ui/shell/review-script.mjs +8 -0
  349. package/dist/ui/shell/review-script.mjs.map +1 -1
  350. package/dist/ui/shell/sidebar.mjs +2 -2
  351. package/dist/ui/shell/sidebar.mjs.map +1 -1
  352. package/package.json +11 -9
  353. package/dist/asset-paths.mjs +0 -23
  354. package/dist/asset-paths.mjs.map +0 -1
  355. package/dist/assets/mark.svg +0 -1
  356. package/dist/pages/settings-status.mjs +0 -137
  357. package/dist/pages/settings-status.mjs.map +0 -1
  358. package/dist/routers/settings-status.mjs +0 -77
  359. package/dist/routers/settings-status.mjs.map +0 -1
  360. package/dist/routers/status-configs.mjs +0 -108
  361. package/dist/routers/status-configs.mjs.map +0 -1
@@ -0,0 +1,46 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { notifyProjectWith, notifySystemWith } from "./notify.mjs";
5
+ import { BuildModel, ProjectModel } from "@storyshelf/core/models";
6
+ //#region src/capture-alerts.ts
7
+ /**
8
+ * Builds already alerted about, keyed by the build's last update. A sharded build settles one job
9
+ * per shard and every one of them sees the failed build; only the first alerts. A retry rewrites
10
+ * the build, so its next failure is a new key.
11
+ */
12
+ const alerted = /* @__PURE__ */ new Set();
13
+ function alreadyAlerted(build) {
14
+ const key = `${build.id}@${build.updatedAt}`;
15
+ if (alerted.has(key)) return true;
16
+ alerted.add(key);
17
+ return false;
18
+ }
19
+ /** Alert admins when a capture leaves its build failed. */
20
+ async function notifyCaptureFailed(options, config, logger, buildId, publicOrigin) {
21
+ const build = await new BuildModel(options.database).get(buildId);
22
+ if (build?.status !== "failed" || alreadyAlerted(build)) return;
23
+ const deps = {
24
+ db: options.database,
25
+ config,
26
+ ui: options.ui ?? {},
27
+ logger,
28
+ notifiers: options.notifications?.notifiers ?? [],
29
+ notificationsConfig: options.notifications?.config,
30
+ publicOrigin: publicOrigin ? { url: publicOrigin } : void 0
31
+ };
32
+ await notifySystemWith(deps, "sys:capture-failed", {
33
+ buildId,
34
+ projectId: build.projectId,
35
+ gitBranch: build.gitBranch
36
+ });
37
+ const project = await new ProjectModel(options.database).get(build.projectId);
38
+ if (project) await notifyProjectWith(deps, project, "build:failed", {
39
+ buildId,
40
+ status: "failed"
41
+ }, `/builds/${buildId}`);
42
+ }
43
+ //#endregion
44
+ export { alreadyAlerted, notifyCaptureFailed };
45
+
46
+ //# sourceMappingURL=capture-alerts.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-alerts.mjs","names":[],"sources":["../src/capture-alerts.ts"],"sourcesContent":["/** Admin alerts for captures that leave their build failed; the app, not the capture child, sends them. */\nimport type { ShelfConfig, ShelfOptions } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { BuildModel, ProjectModel } from \"@storyshelf/core/models\";\nimport { notifyProjectWith, notifySystemWith } from \"./notify.ts\";\n\n/**\n * Builds already alerted about, keyed by the build's last update. A sharded build settles one job\n * per shard and every one of them sees the failed build; only the first alerts. A retry rewrites\n * the build, so its next failure is a new key.\n */\nconst alerted = new Set<string>();\n\nexport function alreadyAlerted(build: { id: string; updatedAt: string }): boolean {\n const key = `${build.id}@${build.updatedAt}`;\n if (alerted.has(key)) {\n return true;\n }\n alerted.add(key);\n return false;\n}\n\n/** Alert admins when a capture leaves its build failed. */\nexport async function notifyCaptureFailed(\n options: ShelfOptions,\n config: ShelfConfig,\n logger: Logger,\n buildId: string,\n publicOrigin?: () => Promise<string | undefined>,\n): Promise<void> {\n const build = await new BuildModel(options.database).get(buildId);\n if (build?.status !== \"failed\" || alreadyAlerted(build)) {\n return;\n }\n const deps = {\n db: options.database,\n config,\n ui: options.ui ?? {},\n logger,\n notifiers: options.notifications?.notifiers ?? [],\n notificationsConfig: options.notifications?.config,\n publicOrigin: publicOrigin ? { url: publicOrigin } : undefined,\n };\n await notifySystemWith(deps, \"sys:capture-failed\", {\n buildId,\n projectId: build.projectId,\n gitBranch: build.gitBranch,\n });\n // Subscribers of the project's `build:failed` topic hear of a failed capture too.\n const project = await new ProjectModel(options.database).get(build.projectId);\n if (project) {\n await notifyProjectWith(\n deps,\n project,\n \"build:failed\",\n { buildId, status: \"failed\" },\n `/builds/${buildId}`,\n );\n }\n}\n"],"mappings":";;;;;;;;;;;AAWA,MAAM,0BAAU,IAAI,IAAY;AAEhC,SAAgB,eAAe,OAAmD;CAChF,MAAM,MAAM,GAAG,MAAM,GAAG,GAAG,MAAM;CACjC,IAAI,QAAQ,IAAI,GAAG,GACjB,OAAO;CAET,QAAQ,IAAI,GAAG;CACf,OAAO;AACT;;AAGA,eAAsB,oBACpB,SACA,QACA,QACA,SACA,cACe;CACf,MAAM,QAAQ,MAAM,IAAI,WAAW,QAAQ,QAAQ,CAAC,CAAC,IAAI,OAAO;CAChE,IAAI,OAAO,WAAW,YAAY,eAAe,KAAK,GACpD;CAEF,MAAM,OAAO;EACX,IAAI,QAAQ;EACZ;EACA,IAAI,QAAQ,MAAM,CAAC;EACnB;EACA,WAAW,QAAQ,eAAe,aAAa,CAAC;EAChD,qBAAqB,QAAQ,eAAe;EAC5C,cAAc,eAAe,EAAE,KAAK,aAAa,IAAI,KAAA;CACvD;CACA,MAAM,iBAAiB,MAAM,sBAAsB;EACjD;EACA,WAAW,MAAM;EACjB,WAAW,MAAM;CACnB,CAAC;CAED,MAAM,UAAU,MAAM,IAAI,aAAa,QAAQ,QAAQ,CAAC,CAAC,IAAI,MAAM,SAAS;CAC5E,IAAI,SACF,MAAM,kBACJ,MACA,SACA,gBACA;EAAE;EAAS,QAAQ;CAAS,GAC5B,WAAW,SACb;AAEJ"}
@@ -0,0 +1,59 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { DIFF_WORKERS_ENV, IMAGE_ENGINE_ENV } from "@storyshelf/core/diff";
5
+ import { detectResources } from "@storyshelf/core/utils";
6
+ import { CAPTURE_RETRIES_ENV, CAPTURE_SHARD_SIZE_ENV, canForkCaptureChild, createForkedCaptureHandler, decideCaptureProcess, forkCaptureChild } from "@storyshelf/core/capture";
7
+ //#region src/capture-process.ts
8
+ /** Chooses between in-process and forked capture, and builds the forked handler. */
9
+ /**
10
+ * Whether to fork a capture child. `mode: "fork"` forces it (and fails if it cannot); `auto` forks
11
+ * when a `workerEntry` is set and the host can afford a second process.
12
+ *
13
+ * @throws When `mode` is `"fork"` but the runtime cannot fork.
14
+ */
15
+ function shouldForkCapture(testing, logger) {
16
+ if (!testing?.workerEntry || testing.mode === "in-process") return false;
17
+ if (testing.mode === "fork") return requireFork();
18
+ const decision = decideForHost();
19
+ if (decision.mode === "in-process") logger.info({ reason: decision.reason }, "capture workerEntry set; capturing in-process");
20
+ return decision.mode === "fork";
21
+ }
22
+ function requireFork() {
23
+ if (!canForkCaptureChild()) throw new Error("testing.mode \"fork\" is set but child processes are unavailable here");
24
+ return true;
25
+ }
26
+ function decideForHost() {
27
+ const { cpus, memoryBytes } = detectResources();
28
+ return decideCaptureProcess({
29
+ cpus,
30
+ memoryBytes,
31
+ canFork: canForkCaptureChild()
32
+ });
33
+ }
34
+ /** Settings the parent hands to a forked child through its environment. */
35
+ function childEnvOf(deps) {
36
+ const env = { [CAPTURE_RETRIES_ENV]: String(deps.retries) };
37
+ if (deps.shardSize !== void 0) env[CAPTURE_SHARD_SIZE_ENV] = String(deps.shardSize);
38
+ if (deps.imageEngine !== void 0) env[IMAGE_ENGINE_ENV] = deps.imageEngine;
39
+ if (deps.diffThreads !== void 0) env[DIFF_WORKERS_ENV] = String(deps.diffThreads);
40
+ return env;
41
+ }
42
+ /** The `capture` handler that runs jobs in a forked child, with the in-process handler as its fallback. */
43
+ function createForkedHandler(deps) {
44
+ const env = childEnvOf(deps);
45
+ return createForkedCaptureHandler({
46
+ spawn: () => forkCaptureChild({
47
+ entry: deps.workerEntry,
48
+ env
49
+ }),
50
+ fallback: deps.fallback,
51
+ maxRetries: deps.retries,
52
+ onCrashed: deps.onCrashed,
53
+ logger: deps.logger
54
+ });
55
+ }
56
+ //#endregion
57
+ export { childEnvOf, createForkedHandler, shouldForkCapture };
58
+
59
+ //# sourceMappingURL=capture-process.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-process.mjs","names":[],"sources":["../src/capture-process.ts"],"sourcesContent":["/** Chooses between in-process and forked capture, and builds the forked handler. */\nimport {\n canForkCaptureChild,\n CAPTURE_RETRIES_ENV,\n CAPTURE_SHARD_SIZE_ENV,\n createForkedCaptureHandler,\n decideCaptureProcess,\n forkCaptureChild,\n type ForkDecision,\n type ForkedCaptureHandler,\n type KindHandler,\n type ConsumerJob,\n} from \"@storyshelf/core/capture\";\nimport type { ShelfTestingOptions } from \"@storyshelf/core/config\";\nimport { DIFF_WORKERS_ENV, IMAGE_ENGINE_ENV } from \"@storyshelf/core/diff\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { detectResources } from \"@storyshelf/core/utils\";\n\n/**\n * Whether to fork a capture child. `mode: \"fork\"` forces it (and fails if it cannot); `auto` forks\n * when a `workerEntry` is set and the host can afford a second process.\n *\n * @throws When `mode` is `\"fork\"` but the runtime cannot fork.\n */\nexport function shouldForkCapture(\n testing: ShelfTestingOptions | undefined,\n logger: Logger,\n): boolean {\n if (!testing?.workerEntry || testing.mode === \"in-process\") {\n return false;\n }\n if (testing.mode === \"fork\") {\n return requireFork();\n }\n const decision = decideForHost();\n if (decision.mode === \"in-process\") {\n logger.info({ reason: decision.reason }, \"capture workerEntry set; capturing in-process\");\n }\n return decision.mode === \"fork\";\n}\n\nfunction requireFork(): true {\n if (!canForkCaptureChild()) {\n throw new Error('testing.mode \"fork\" is set but child processes are unavailable here');\n }\n return true;\n}\n\nfunction decideForHost(): ForkDecision {\n const { cpus, memoryBytes } = detectResources();\n return decideCaptureProcess({ cpus, memoryBytes, canFork: canForkCaptureChild() });\n}\n\n/** Dependencies for {@link createForkedHandler}. */\nexport interface ForkedHandlerDeps {\n workerEntry: string;\n /** Captures per shard for big builds, handed to the child (`testing.shardSize`). */\n shardSize?: number;\n /** `testing.images.engine`, handed to the child so it diffs with the same codec. */\n imageEngine?: string;\n /** `testing.compare.threads`, handed to the child's diff pool. */\n diffThreads?: number;\n /** `testing.retries`, handed to the child, which stamps it on the shard jobs it queues. */\n retries: number;\n /** Takes over if forking stops being viable. */\n fallback: KindHandler;\n /** A job the child kept killing on its last attempt: write its failure. */\n onCrashed: (job: ConsumerJob, error: string) => Promise<void>;\n logger: Logger;\n}\n\n/** Settings the parent hands to a forked child through its environment. */\nexport function childEnvOf(\n deps: Pick<ForkedHandlerDeps, \"shardSize\" | \"imageEngine\" | \"diffThreads\" | \"retries\">,\n): Record<string, string> {\n const env: Record<string, string> = { [CAPTURE_RETRIES_ENV]: String(deps.retries) };\n if (deps.shardSize !== undefined) {\n env[CAPTURE_SHARD_SIZE_ENV] = String(deps.shardSize);\n }\n if (deps.imageEngine !== undefined) {\n env[IMAGE_ENGINE_ENV] = deps.imageEngine;\n }\n if (deps.diffThreads !== undefined) {\n env[DIFF_WORKERS_ENV] = String(deps.diffThreads);\n }\n return env;\n}\n\n/** The `capture` handler that runs jobs in a forked child, with the in-process handler as its fallback. */\nexport function createForkedHandler(deps: ForkedHandlerDeps): ForkedCaptureHandler {\n const env = childEnvOf(deps);\n return createForkedCaptureHandler({\n spawn: () => forkCaptureChild({ entry: deps.workerEntry, env }),\n fallback: deps.fallback,\n maxRetries: deps.retries,\n onCrashed: deps.onCrashed,\n logger: deps.logger,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,SAAgB,kBACd,SACA,QACS;CACT,IAAI,CAAC,SAAS,eAAe,QAAQ,SAAS,cAC5C,OAAO;CAET,IAAI,QAAQ,SAAS,QACnB,OAAO,YAAY;CAErB,MAAM,WAAW,cAAc;CAC/B,IAAI,SAAS,SAAS,cACpB,OAAO,KAAK,EAAE,QAAQ,SAAS,OAAO,GAAG,+CAA+C;CAE1F,OAAO,SAAS,SAAS;AAC3B;AAEA,SAAS,cAAoB;CAC3B,IAAI,CAAC,oBAAoB,GACvB,MAAM,IAAI,MAAM,uEAAqE;CAEvF,OAAO;AACT;AAEA,SAAS,gBAA8B;CACrC,MAAM,EAAE,MAAM,gBAAgB,gBAAgB;CAC9C,OAAO,qBAAqB;EAAE;EAAM;EAAa,SAAS,oBAAoB;CAAE,CAAC;AACnF;;AAqBA,SAAgB,WACd,MACwB;CACxB,MAAM,MAA8B,GAAG,sBAAsB,OAAO,KAAK,OAAO,EAAE;CAClF,IAAI,KAAK,cAAc,KAAA,GACrB,IAAI,0BAA0B,OAAO,KAAK,SAAS;CAErD,IAAI,KAAK,gBAAgB,KAAA,GACvB,IAAI,oBAAoB,KAAK;CAE/B,IAAI,KAAK,gBAAgB,KAAA,GACvB,IAAI,oBAAoB,OAAO,KAAK,WAAW;CAEjD,OAAO;AACT;;AAGA,SAAgB,oBAAoB,MAA+C;CACjF,MAAM,MAAM,WAAW,IAAI;CAC3B,OAAO,2BAA2B;EAChC,aAAa,iBAAiB;GAAE,OAAO,KAAK;GAAa;EAAI,CAAC;EAC9D,UAAU,KAAK;EACf,YAAY,KAAK;EACjB,WAAW,KAAK;EAChB,QAAQ,KAAK;CACf,CAAC;AACH"}
@@ -2,138 +2,116 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { credentialKeys } from "./credential-keys.mjs";
5
- import { notifySystemWith } from "./notify.mjs";
6
- import { captureMetrics, currentTraceparent } from "@storyshelf/observability";
7
- import { InMemoryCaptureQueue, createDispatchJob, recoverStuckCaptures } from "@storyshelf/core/capture";
8
- import { BuildModel } from "@storyshelf/core/models";
5
+ import { notifyCaptureFailed } from "./capture-alerts.mjs";
6
+ import { createForkedHandler, shouldForkCapture } from "./capture-process.mjs";
7
+ import { DEFAULT_SCRATCH_DIR } from "@storyshelf/core/config";
8
+ import { currentTraceparent } from "@storyshelf/observability";
9
+ import { DEFAULT_RETRIES, captureDispatchDeps, createCaptureHandler, failCrashedCapture, isFinalAttempt } from "@storyshelf/core/capture";
9
10
  //#region src/capture-setup.ts
10
- /** Enqueue a build, continuing the active request trace when present. */
11
- async function enqueueJob(queue, buildId, reqId) {
12
- const traceparent = currentTraceparent();
13
- await queue.enqueue({
14
- buildId,
15
- reqId,
16
- ...traceparent ? { traceparent } : {}
17
- });
18
- }
19
- /** Wrap the dispatch job with duration + outcome metrics. */
20
- async function timedRunJob(runJob, job) {
21
- const instruments = captureMetrics();
22
- const start = performance.now();
23
- try {
24
- await runJob(job);
25
- } catch (error) {
26
- instruments.jobsFailed.add(1);
27
- instruments.jobDuration.record(performance.now() - start);
28
- throw error;
29
- }
30
- instruments.jobsCompleted.add(1);
31
- instruments.jobDuration.record(performance.now() - start);
32
- }
33
- /** Alert admins when an in-process capture leaves its build failed. */
34
- async function notifyCaptureFailed(options, config, logger, buildId) {
35
- const build = await new BuildModel(options.database).get(buildId);
36
- if (build?.status !== "failed") return;
37
- await notifySystemWith({
38
- db: options.database,
39
- config,
40
- ui: options.ui ?? {},
41
- logger,
42
- notifiers: options.notifiers ?? []
43
- }, "sys:capture-failed", {
44
- buildId,
45
- projectId: build.projectId,
46
- gitBranch: build.gitBranch
47
- });
11
+ /** Queue a build, continuing the active request trace when present. */
12
+ function enqueuerFor(queue) {
13
+ return async (buildId, reqId) => {
14
+ const traceparent = currentTraceparent();
15
+ await queue.enqueue({
16
+ buildId,
17
+ reqId,
18
+ ...traceparent ? { traceparent } : {}
19
+ });
20
+ };
48
21
  }
49
- /** Assemble the capture queue and its enqueue hook. */
50
- function setupCaptureQueue(options, config, gitHosts, logger) {
51
- if (!options.captureQueue && !options.captureRunner) return {
22
+ /**
23
+ * Wire capture for `options`: `testing` always has a runner, so this instance consumes the
24
+ * captures it queues. Shard jobs go back onto the same queue the capture came from.
25
+ */
26
+ function setupCapture(options, config, delivery, logger, jobs) {
27
+ const testing = options.testing;
28
+ if (testing === void 0) return {
52
29
  queue: null,
53
- enqueueCapture: void 0
30
+ enqueueCapture: void 0,
31
+ handler: null
54
32
  };
55
- if (options.captureQueue) {
56
- const queue = options.captureQueue;
57
- const enqueueCapture = async (buildId, reqId) => {
58
- await enqueueJob(queue, buildId, reqId);
59
- };
60
- if (!options.captureRunner) return {
61
- queue: options.captureQueue,
62
- enqueueCapture
63
- };
64
- if (!config.scratchDir) return {
65
- queue: options.captureQueue,
66
- enqueueCapture
67
- };
68
- return {
69
- queue: options.captureQueue,
70
- enqueueCapture
71
- };
72
- }
73
- if (!options.captureRunner) return {
74
- queue: null,
75
- enqueueCapture: void 0
33
+ const queue = jobs.view;
34
+ const enqueueCapture = enqueuerFor(queue);
35
+ const handlerOptions = captureHandlerOptions(options, config, delivery, logger, {
36
+ runner: testing.runner,
37
+ enqueue: async (job) => {
38
+ await queue.enqueue(job);
39
+ }
40
+ });
41
+ const retries = options.testing?.retries ?? DEFAULT_RETRIES;
42
+ const dispatch = captureDispatchDeps(handlerOptions, retries);
43
+ const failed = async (job, error) => {
44
+ await failCrashedCapture(dispatch, job, error);
45
+ await notifyCaptureFailed(options, config, logger, job.buildId, delivery.publicOrigin);
46
+ };
47
+ jobs.exhausted.capture = failed;
48
+ const inProcess = createCaptureHandler(handlerOptions);
49
+ return {
50
+ queue,
51
+ enqueueCapture,
52
+ handler: alerting(shouldForkCapture(testing, logger) ? forkedHandler(options, {
53
+ fallback: inProcess,
54
+ retries,
55
+ onCrashed: failed,
56
+ logger
57
+ }) : inProcess, retries, async (buildId) => {
58
+ await notifyCaptureFailed(options, config, logger, buildId, delivery.publicOrigin);
59
+ })
76
60
  };
77
- if (!config.scratchDir) throw new Error("captureRunner is enabled but ShelfConfig.scratchDir is not set");
78
- const jobOptions = {
61
+ }
62
+ function captureHandlerOptions(options, config, delivery, logger, parts) {
63
+ return {
79
64
  db: options.database,
80
- tables: {
81
- projects: options.database.tables.projects,
82
- builds: options.database.tables.builds,
83
- buildLabels: options.database.tables.buildLabels,
84
- snapshots: options.database.tables.snapshots,
85
- baselines: options.database.tables.baselines,
86
- captureAttempts: options.database.tables.captureAttempts,
87
- captureLogs: options.database.tables.captureLogs
88
- },
89
65
  storage: options.storage,
90
- runner: options.captureRunner,
91
- scratchDir: config.scratchDir,
66
+ runner: parts.runner,
67
+ scratchDir: config.scratchDir ?? DEFAULT_SCRATCH_DIR,
92
68
  viewports: config.viewports,
93
69
  logger,
94
- secret: credentialKeys(config)
95
- };
96
- const dispatchDeps = {
97
- db: options.database,
98
- tables: {
99
- projects: options.database.tables.projects,
100
- builds: options.database.tables.builds,
101
- buildLabels: options.database.tables.buildLabels,
102
- snapshots: options.database.tables.snapshots,
103
- projectStatusConfigs: options.database.tables.projectStatusConfigs,
104
- captureAttempts: options.database.tables.captureAttempts,
105
- captureLogs: options.database.tables.captureLogs
106
- },
107
- jobOptions,
108
- gitHosts,
109
70
  secret: credentialKeys(config),
110
- logger
111
- };
112
- const runJob = createDispatchJob(dispatchDeps);
113
- const captureQueue = options.captureQueue ?? new InMemoryCaptureQueue({
114
- concurrency: config.captureConcurrency ?? 2,
115
- logger,
116
- runJob: async (job) => {
117
- await timedRunJob(runJob, job);
118
- await notifyCaptureFailed(options, config, logger, job.buildId);
119
- }
120
- });
121
- const enqueueCapture = async (buildId, reqId) => {
122
- await enqueueJob(captureQueue, buildId, reqId);
71
+ design: options.design,
72
+ shardSize: options.testing?.shardSize,
73
+ notifiers: delivery.notifiers,
74
+ buildState: delivery.buildState,
75
+ publicOrigin: delivery.publicOrigin,
76
+ config,
77
+ enqueue: parts.enqueue,
78
+ maxRetries: options.testing?.retries
123
79
  };
124
- const recoverStuck = async () => await recoverStuckCaptures({
125
- deps: dispatchDeps,
126
- enqueue: async (buildId) => {
127
- await enqueueCapture(buildId);
128
- }
80
+ }
81
+ function forkedHandler(options, parts) {
82
+ const testing = options.testing;
83
+ return createForkedHandler({
84
+ workerEntry: testing?.workerEntry ?? "",
85
+ shardSize: testing?.shardSize,
86
+ imageEngine: testing?.images?.engine,
87
+ diffThreads: testing?.compare?.threads,
88
+ ...parts
129
89
  });
90
+ }
91
+ /**
92
+ * Alert admins when a capture left its build failed: after a run that ended, and after a final
93
+ * attempt that threw (the build may then already be failed by a crash hook). Alerts stay in this
94
+ * process whichever process rendered.
95
+ */
96
+ function alerting(inner, retries, alert) {
130
97
  return {
131
- queue: captureQueue,
132
- enqueueCapture,
133
- recoverStuck
98
+ accepts: (job) => inner.accepts?.(job) !== false,
99
+ run: async (job, context) => {
100
+ try {
101
+ await inner.run(job, context);
102
+ } catch (error) {
103
+ if (isFinalAttempt(job, retries)) await alert(job.buildId);
104
+ throw error;
105
+ }
106
+ await alert(job.buildId);
107
+ },
108
+ cancel: async (job) => {
109
+ await inner.cancel?.(job);
110
+ },
111
+ ...inner.close ? { close: inner.close } : {}
134
112
  };
135
113
  }
136
114
  //#endregion
137
- export { setupCaptureQueue };
115
+ export { setupCapture };
138
116
 
139
117
  //# sourceMappingURL=capture-setup.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"capture-setup.mjs","names":[],"sources":["../src/capture-setup.ts"],"sourcesContent":["import type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\nimport type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport {\n createDispatchJob,\n InMemoryCaptureQueue,\n recoverStuckCaptures,\n} from \"@storyshelf/core/capture\";\nimport type {\n CaptureDispatchJob,\n CaptureJobOptions,\n DispatchDeps,\n RecoveryResult,\n} from \"@storyshelf/core/capture\";\nimport type { ShelfConfig, ShelfOptions } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { captureMetrics, currentTraceparent } from \"@storyshelf/observability\";\nimport { credentialKeys } from \"./credential-keys.ts\";\nimport { notifySystemWith } from \"./notify.ts\";\n\n/**\n * Wiring for the capture queue: the queue instance (if any) and a helper\n * to enqueue a build for rendering. When no `captureRunner` is configured\n * both are null/undefined and builds remain `pending` until a runner is added.\n */\nexport interface QueueWiring {\n queue: CaptureQueue | null;\n enqueueCapture: ((buildId: string, reqId?: string) => Promise<void>) | undefined;\n /**\n * Boot sweep for builds a restart left in `capturing` (requeue once, else fail).\n * Only set for the in-process queue; remote queues redeliver on their own.\n */\n recoverStuck?: () => Promise<RecoveryResult>;\n}\n\n/** Enqueue a build, continuing the active request trace when present. */\nasync function enqueueJob(queue: CaptureQueue, buildId: string, reqId?: string): Promise<void> {\n const traceparent = currentTraceparent();\n await queue.enqueue({ buildId, reqId, ...(traceparent ? { traceparent } : {}) });\n}\n\n/** Wrap the dispatch job with duration + outcome metrics. */\nasync function timedRunJob(\n runJob: (job: CaptureDispatchJob) => Promise<void>,\n job: CaptureDispatchJob,\n): Promise<void> {\n const instruments = captureMetrics();\n const start = performance.now();\n try {\n await runJob(job);\n } catch (error) {\n instruments.jobsFailed.add(1);\n instruments.jobDuration.record(performance.now() - start);\n throw error;\n }\n instruments.jobsCompleted.add(1);\n instruments.jobDuration.record(performance.now() - start);\n}\n\n/** Alert admins when an in-process capture leaves its build failed. */\nasync function notifyCaptureFailed(\n options: ShelfOptions,\n config: ShelfConfig,\n logger: Logger,\n buildId: string,\n): Promise<void> {\n const build = await new BuildModel(options.database).get(buildId);\n if (build?.status !== \"failed\") {\n return;\n }\n await notifySystemWith(\n {\n db: options.database,\n config,\n ui: options.ui ?? {},\n logger,\n notifiers: options.notifiers ?? [],\n },\n \"sys:capture-failed\",\n { buildId, projectId: build.projectId, gitBranch: build.gitBranch },\n );\n}\n\n/** Assemble the capture queue and its enqueue hook. */\nexport function setupCaptureQueue(\n options: ShelfOptions,\n config: ShelfConfig,\n gitHosts: GitHostProvider[],\n logger: Logger,\n): QueueWiring {\n if (!options.captureQueue && !options.captureRunner) {\n return { queue: null, enqueueCapture: undefined };\n }\n if (options.captureQueue) {\n const queue = options.captureQueue;\n const enqueueCapture = async (buildId: string, reqId?: string): Promise<void> => {\n await enqueueJob(queue, buildId, reqId);\n };\n if (!options.captureRunner) {\n return { queue: options.captureQueue, enqueueCapture };\n }\n // Both queue and runner supplied: runner is ignored server-side for remote queues;\n // enqueue goes to the supplied queue and capture is handled by an external worker.\n if (!config.scratchDir) {\n return { queue: options.captureQueue, enqueueCapture };\n }\n // If scratchDir is set with both, still prefer the supplied queue; no in-memory queue needed.\n return { queue: options.captureQueue, enqueueCapture };\n }\n if (!options.captureRunner) {\n return { queue: null, enqueueCapture: undefined };\n }\n if (!config.scratchDir) {\n throw new Error(\"captureRunner is enabled but ShelfConfig.scratchDir is not set\");\n }\n const jobOptions: CaptureJobOptions = {\n db: options.database,\n tables: {\n projects: options.database.tables.projects,\n builds: options.database.tables.builds,\n buildLabels: options.database.tables.buildLabels,\n snapshots: options.database.tables.snapshots,\n baselines: options.database.tables.baselines,\n captureAttempts: options.database.tables.captureAttempts,\n captureLogs: options.database.tables.captureLogs,\n },\n storage: options.storage,\n runner: options.captureRunner,\n scratchDir: config.scratchDir,\n viewports: config.viewports,\n logger,\n secret: credentialKeys(config),\n };\n const dispatchDeps: DispatchDeps = {\n db: options.database,\n tables: {\n projects: options.database.tables.projects,\n builds: options.database.tables.builds,\n buildLabels: options.database.tables.buildLabels,\n snapshots: options.database.tables.snapshots,\n projectStatusConfigs: options.database.tables.projectStatusConfigs,\n captureAttempts: options.database.tables.captureAttempts,\n captureLogs: options.database.tables.captureLogs,\n },\n jobOptions,\n gitHosts,\n secret: credentialKeys(config),\n logger,\n };\n const runJob = createDispatchJob(dispatchDeps);\n const captureQueue =\n options.captureQueue ??\n new InMemoryCaptureQueue({\n concurrency: config.captureConcurrency ?? 2,\n logger,\n runJob: async (job: CaptureDispatchJob): Promise<void> => {\n await timedRunJob(runJob, job);\n // In-process completions only: remote-queue workers own their own\n // alerting (the server never sees those outcomes).\n await notifyCaptureFailed(options, config, logger, job.buildId);\n },\n });\n const enqueueCapture = async (buildId: string, reqId?: string): Promise<void> => {\n await enqueueJob(captureQueue, buildId, reqId);\n };\n const recoverStuck = async (): Promise<RecoveryResult> =>\n await recoverStuckCaptures({\n deps: dispatchDeps,\n enqueue: async (buildId) => {\n await enqueueCapture(buildId);\n },\n });\n return { queue: captureQueue, enqueueCapture, recoverStuck };\n}\n"],"mappings":";;;;;;;;;;AAoCA,eAAe,WAAW,OAAqB,SAAiB,OAA+B;CAC7F,MAAM,cAAc,mBAAmB;CACvC,MAAM,MAAM,QAAQ;EAAE;EAAS;EAAO,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;CAAG,CAAC;AACjF;;AAGA,eAAe,YACb,QACA,KACe;CACf,MAAM,cAAc,eAAe;CACnC,MAAM,QAAQ,YAAY,IAAI;CAC9B,IAAI;EACF,MAAM,OAAO,GAAG;CAClB,SAAS,OAAO;EACd,YAAY,WAAW,IAAI,CAAC;EAC5B,YAAY,YAAY,OAAO,YAAY,IAAI,IAAI,KAAK;EACxD,MAAM;CACR;CACA,YAAY,cAAc,IAAI,CAAC;CAC/B,YAAY,YAAY,OAAO,YAAY,IAAI,IAAI,KAAK;AAC1D;;AAGA,eAAe,oBACb,SACA,QACA,QACA,SACe;CACf,MAAM,QAAQ,MAAM,IAAI,WAAW,QAAQ,QAAQ,CAAC,CAAC,IAAI,OAAO;CAChE,IAAI,OAAO,WAAW,UACpB;CAEF,MAAM,iBACJ;EACE,IAAI,QAAQ;EACZ;EACA,IAAI,QAAQ,MAAM,CAAC;EACnB;EACA,WAAW,QAAQ,aAAa,CAAC;CACnC,GACA,sBACA;EAAE;EAAS,WAAW,MAAM;EAAW,WAAW,MAAM;CAAU,CACpE;AACF;;AAGA,SAAgB,kBACd,SACA,QACA,UACA,QACa;CACb,IAAI,CAAC,QAAQ,gBAAgB,CAAC,QAAQ,eACpC,OAAO;EAAE,OAAO;EAAM,gBAAgB,KAAA;CAAU;CAElD,IAAI,QAAQ,cAAc;EACxB,MAAM,QAAQ,QAAQ;EACtB,MAAM,iBAAiB,OAAO,SAAiB,UAAkC;GAC/E,MAAM,WAAW,OAAO,SAAS,KAAK;EACxC;EACA,IAAI,CAAC,QAAQ,eACX,OAAO;GAAE,OAAO,QAAQ;GAAc;EAAe;EAIvD,IAAI,CAAC,OAAO,YACV,OAAO;GAAE,OAAO,QAAQ;GAAc;EAAe;EAGvD,OAAO;GAAE,OAAO,QAAQ;GAAc;EAAe;CACvD;CACA,IAAI,CAAC,QAAQ,eACX,OAAO;EAAE,OAAO;EAAM,gBAAgB,KAAA;CAAU;CAElD,IAAI,CAAC,OAAO,YACV,MAAM,IAAI,MAAM,gEAAgE;CAElF,MAAM,aAAgC;EACpC,IAAI,QAAQ;EACZ,QAAQ;GACN,UAAU,QAAQ,SAAS,OAAO;GAClC,QAAQ,QAAQ,SAAS,OAAO;GAChC,aAAa,QAAQ,SAAS,OAAO;GACrC,WAAW,QAAQ,SAAS,OAAO;GACnC,WAAW,QAAQ,SAAS,OAAO;GACnC,iBAAiB,QAAQ,SAAS,OAAO;GACzC,aAAa,QAAQ,SAAS,OAAO;EACvC;EACA,SAAS,QAAQ;EACjB,QAAQ,QAAQ;EAChB,YAAY,OAAO;EACnB,WAAW,OAAO;EAClB;EACA,QAAQ,eAAe,MAAM;CAC/B;CACA,MAAM,eAA6B;EACjC,IAAI,QAAQ;EACZ,QAAQ;GACN,UAAU,QAAQ,SAAS,OAAO;GAClC,QAAQ,QAAQ,SAAS,OAAO;GAChC,aAAa,QAAQ,SAAS,OAAO;GACrC,WAAW,QAAQ,SAAS,OAAO;GACnC,sBAAsB,QAAQ,SAAS,OAAO;GAC9C,iBAAiB,QAAQ,SAAS,OAAO;GACzC,aAAa,QAAQ,SAAS,OAAO;EACvC;EACA;EACA;EACA,QAAQ,eAAe,MAAM;EAC7B;CACF;CACA,MAAM,SAAS,kBAAkB,YAAY;CAC7C,MAAM,eACJ,QAAQ,gBACR,IAAI,qBAAqB;EACvB,aAAa,OAAO,sBAAsB;EAC1C;EACA,QAAQ,OAAO,QAA2C;GACxD,MAAM,YAAY,QAAQ,GAAG;GAG7B,MAAM,oBAAoB,SAAS,QAAQ,QAAQ,IAAI,OAAO;EAChE;CACF,CAAC;CACH,MAAM,iBAAiB,OAAO,SAAiB,UAAkC;EAC/E,MAAM,WAAW,cAAc,SAAS,KAAK;CAC/C;CACA,MAAM,eAAe,YACnB,MAAM,qBAAqB;EACzB,MAAM;EACN,SAAS,OAAO,YAAY;GAC1B,MAAM,eAAe,OAAO;EAC9B;CACF,CAAC;CACH,OAAO;EAAE,OAAO;EAAc;EAAgB;CAAa;AAC7D"}
1
+ {"version":3,"file":"capture-setup.mjs","names":[],"sources":["../src/capture-setup.ts"],"sourcesContent":["/**\n * Capture wiring (ADR 0006, decisions 4 to 6): where uploads queue their capture, and the handler\n * of `capture` jobs. Captures queue on the shared database queue. The handler runs them in this process, or in a forked child the parent pushes jobs\n * to (`testing.workerEntry`). The consumer that polls the queue and calls the handler is built\n * in `jobs-setup.ts`.\n */\nimport type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\nimport type { CaptureRunner } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport {\n captureDispatchDeps,\n createCaptureHandler,\n DEFAULT_RETRIES,\n failCrashedCapture,\n isFinalAttempt,\n type BuildStatePublisher,\n type CaptureHandlerOptions,\n type ConsumerJob,\n type KindHandler,\n} from \"@storyshelf/core/capture\";\nimport { DEFAULT_SCRATCH_DIR } from \"@storyshelf/core/config\";\nimport type { ShelfConfig, ShelfOptions } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { currentTraceparent } from \"@storyshelf/observability\";\nimport { notifyCaptureFailed } from \"./capture-alerts.ts\";\nimport { createForkedHandler, shouldForkCapture } from \"./capture-process.ts\";\nimport { credentialKeys } from \"./credential-keys.ts\";\nimport type { JobQueue } from \"./job-queue.ts\";\n\n/** The `capture` handler, with a way to stop a forked child once the consumer has drained. */\nexport interface CaptureHandler extends KindHandler {\n close?: () => Promise<void>;\n}\n\n/** What uploads and the consumer need of capture. */\nexport interface CaptureWiring {\n /** Where captures queue: the database queue, or null without `testing`. */\n queue: CaptureQueue | null;\n /** Queues a capture for a build; undefined without `testing`. */\n enqueueCapture: ((buildId: string, reqId?: string) => Promise<void>) | undefined;\n /** Runs `capture` jobs here; null without `testing`. */\n handler: CaptureHandler | null;\n}\n\n/** What a capture hands its results to. */\ninterface Delivery {\n notifiers: NotifierProvider[];\n buildState: BuildStatePublisher;\n /** The resolved public origin (ADR 0007): status links and targets that need a URL use it. */\n publicOrigin: () => Promise<string | undefined>;\n}\n\n/** Queue a build, continuing the active request trace when present. */\nfunction enqueuerFor(queue: CaptureQueue): (buildId: string, reqId?: string) => Promise<void> {\n return async (buildId, reqId) => {\n const traceparent = currentTraceparent();\n await queue.enqueue({ buildId, reqId, ...(traceparent ? { traceparent } : {}) });\n };\n}\n\n/**\n * Wire capture for `options`: `testing` always has a runner, so this instance consumes the\n * captures it queues. Shard jobs go back onto the same queue the capture came from.\n */\nexport function setupCapture(\n options: ShelfOptions,\n config: ShelfConfig,\n delivery: Delivery,\n logger: Logger,\n jobs: JobQueue,\n): CaptureWiring {\n const testing = options.testing;\n if (testing === undefined) {\n return { queue: null, enqueueCapture: undefined, handler: null };\n }\n const queue = jobs.view;\n const enqueueCapture = enqueuerFor(queue);\n const handlerOptions = captureHandlerOptions(options, config, delivery, logger, {\n runner: testing.runner,\n enqueue: async (job) => {\n await queue.enqueue(job);\n },\n });\n const retries = options.testing?.retries ?? DEFAULT_RETRIES;\n const dispatch = captureDispatchDeps(handlerOptions, retries);\n const failed = async (job: ConsumerJob, error: string): Promise<void> => {\n await failCrashedCapture(dispatch, job, error);\n await notifyCaptureFailed(options, config, logger, job.buildId, delivery.publicOrigin);\n };\n jobs.exhausted.capture = failed;\n const inProcess = createCaptureHandler(handlerOptions);\n const handler = shouldForkCapture(testing, logger)\n ? forkedHandler(options, { fallback: inProcess, retries, onCrashed: failed, logger })\n : inProcess;\n return {\n queue,\n enqueueCapture,\n handler: alerting(handler, retries, async (buildId) => {\n await notifyCaptureFailed(options, config, logger, buildId, delivery.publicOrigin);\n }),\n };\n}\n\nfunction captureHandlerOptions(\n options: ShelfOptions,\n config: ShelfConfig,\n delivery: Delivery,\n logger: Logger,\n parts: { runner: CaptureRunner; enqueue: CaptureHandlerOptions[\"enqueue\"] },\n): CaptureHandlerOptions {\n return {\n db: options.database,\n storage: options.storage,\n runner: parts.runner,\n // `resolveShelfOptions` defaults scratchDir; the fallback covers hand-built configs.\n scratchDir: config.scratchDir ?? DEFAULT_SCRATCH_DIR,\n viewports: config.viewports,\n logger,\n secret: credentialKeys(config),\n design: options.design,\n shardSize: options.testing?.shardSize,\n notifiers: delivery.notifiers,\n buildState: delivery.buildState,\n publicOrigin: delivery.publicOrigin,\n config,\n enqueue: parts.enqueue,\n maxRetries: options.testing?.retries,\n };\n}\n\nfunction forkedHandler(\n options: ShelfOptions,\n parts: {\n fallback: KindHandler;\n retries: number;\n onCrashed: (job: ConsumerJob, error: string) => Promise<void>;\n logger: Logger;\n },\n): CaptureHandler {\n const testing = options.testing;\n return createForkedHandler({\n workerEntry: testing?.workerEntry ?? \"\",\n shardSize: testing?.shardSize,\n imageEngine: testing?.images?.engine,\n diffThreads: testing?.compare?.threads,\n ...parts,\n });\n}\n\n/**\n * Alert admins when a capture left its build failed: after a run that ended, and after a final\n * attempt that threw (the build may then already be failed by a crash hook). Alerts stay in this\n * process whichever process rendered.\n */\nfunction alerting(\n inner: CaptureHandler,\n retries: number,\n alert: (buildId: string) => Promise<void>,\n): CaptureHandler {\n return {\n accepts: (job) => inner.accepts?.(job) !== false,\n run: async (job, context) => {\n try {\n await inner.run(job, context);\n } catch (error) {\n if (isFinalAttempt(job, retries)) {\n await alert(job.buildId);\n }\n throw error;\n }\n await alert(job.buildId);\n },\n cancel: async (job) => {\n await inner.cancel?.(job);\n },\n ...(inner.close ? { close: inner.close } : {}),\n };\n}\n"],"mappings":";;;;;;;;;;;AAqDA,SAAS,YAAY,OAAyE;CAC5F,OAAO,OAAO,SAAS,UAAU;EAC/B,MAAM,cAAc,mBAAmB;EACvC,MAAM,MAAM,QAAQ;GAAE;GAAS;GAAO,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;EAAG,CAAC;CACjF;AACF;;;;;AAMA,SAAgB,aACd,SACA,QACA,UACA,QACA,MACe;CACf,MAAM,UAAU,QAAQ;CACxB,IAAI,YAAY,KAAA,GACd,OAAO;EAAE,OAAO;EAAM,gBAAgB,KAAA;EAAW,SAAS;CAAK;CAEjE,MAAM,QAAQ,KAAK;CACnB,MAAM,iBAAiB,YAAY,KAAK;CACxC,MAAM,iBAAiB,sBAAsB,SAAS,QAAQ,UAAU,QAAQ;EAC9E,QAAQ,QAAQ;EAChB,SAAS,OAAO,QAAQ;GACtB,MAAM,MAAM,QAAQ,GAAG;EACzB;CACF,CAAC;CACD,MAAM,UAAU,QAAQ,SAAS,WAAW;CAC5C,MAAM,WAAW,oBAAoB,gBAAgB,OAAO;CAC5D,MAAM,SAAS,OAAO,KAAkB,UAAiC;EACvE,MAAM,mBAAmB,UAAU,KAAK,KAAK;EAC7C,MAAM,oBAAoB,SAAS,QAAQ,QAAQ,IAAI,SAAS,SAAS,YAAY;CACvF;CACA,KAAK,UAAU,UAAU;CACzB,MAAM,YAAY,qBAAqB,cAAc;CAIrD,OAAO;EACL;EACA;EACA,SAAS,SANK,kBAAkB,SAAS,MAAM,IAC7C,cAAc,SAAS;GAAE,UAAU;GAAW;GAAS,WAAW;GAAQ;EAAO,CAAC,IAClF,WAIyB,SAAS,OAAO,YAAY;GACrD,MAAM,oBAAoB,SAAS,QAAQ,QAAQ,SAAS,SAAS,YAAY;EACnF,CAAC;CACH;AACF;AAEA,SAAS,sBACP,SACA,QACA,UACA,QACA,OACuB;CACvB,OAAO;EACL,IAAI,QAAQ;EACZ,SAAS,QAAQ;EACjB,QAAQ,MAAM;EAEd,YAAY,OAAO,cAAc;EACjC,WAAW,OAAO;EAClB;EACA,QAAQ,eAAe,MAAM;EAC7B,QAAQ,QAAQ;EAChB,WAAW,QAAQ,SAAS;EAC5B,WAAW,SAAS;EACpB,YAAY,SAAS;EACrB,cAAc,SAAS;EACvB;EACA,SAAS,MAAM;EACf,YAAY,QAAQ,SAAS;CAC/B;AACF;AAEA,SAAS,cACP,SACA,OAMgB;CAChB,MAAM,UAAU,QAAQ;CACxB,OAAO,oBAAoB;EACzB,aAAa,SAAS,eAAe;EACrC,WAAW,SAAS;EACpB,aAAa,SAAS,QAAQ;EAC9B,aAAa,SAAS,SAAS;EAC/B,GAAG;CACL,CAAC;AACH;;;;;;AAOA,SAAS,SACP,OACA,SACA,OACgB;CAChB,OAAO;EACL,UAAU,QAAQ,MAAM,UAAU,GAAG,MAAM;EAC3C,KAAK,OAAO,KAAK,YAAY;GAC3B,IAAI;IACF,MAAM,MAAM,IAAI,KAAK,OAAO;GAC9B,SAAS,OAAO;IACd,IAAI,eAAe,KAAK,OAAO,GAC7B,MAAM,MAAM,IAAI,OAAO;IAEzB,MAAM;GACR;GACA,MAAM,MAAM,IAAI,OAAO;EACzB;EACA,QAAQ,OAAO,QAAQ;GACrB,MAAM,MAAM,SAAS,GAAG;EAC1B;EACA,GAAI,MAAM,QAAQ,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;CAC9C;AACF"}
@@ -0,0 +1,34 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { credentialKeys } from "./credential-keys.mjs";
5
+ import { getStore } from "./store.mjs";
6
+ import { ConnectionModel } from "@storyshelf/core/models";
7
+ //#region src/connections.ts
8
+ /**
9
+ * Services a user can connect their own account to. Today only design providers
10
+ * offer OAuth; a future issue-tracker or notifier adapter that exposes
11
+ * `oauth` joins by being listed here — routes, state signing, storage and
12
+ * refresh are shared.
13
+ */
14
+ function connectableServices() {
15
+ return [...getStore().design];
16
+ }
17
+ /** The service addressed by `kind` when the operator configured OAuth for it, else undefined. */
18
+ function findConnectable(kind) {
19
+ return connectableServices().find((service) => service.metadata.kind === kind && service.oauth);
20
+ }
21
+ /** Connection storage bound to the server's credential keys. */
22
+ function connectionModel() {
23
+ const { db, config } = getStore();
24
+ return new ConnectionModel(db, credentialKeys(config));
25
+ }
26
+ /** Connecting needs authentication (a user to own it) and a server secret (to sign the flow). */
27
+ function canConnect() {
28
+ const { authEnabled, user, config } = getStore();
29
+ return authEnabled && Boolean(user) && Boolean(config.secret);
30
+ }
31
+ //#endregion
32
+ export { canConnect, connectableServices, connectionModel, findConnectable };
33
+
34
+ //# sourceMappingURL=connections.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connections.mjs","names":[],"sources":["../src/connections.ts"],"sourcesContent":["import type { OAuthConnectable } from \"@storyshelf/core/adapter/oauth\";\nimport { ConnectionModel } from \"@storyshelf/core/models\";\nimport { credentialKeys } from \"./credential-keys.ts\";\nimport { getStore } from \"./store.ts\";\n\n/**\n * Services a user can connect their own account to. Today only design providers\n * offer OAuth; a future issue-tracker or notifier adapter that exposes\n * `oauth` joins by being listed here — routes, state signing, storage and\n * refresh are shared.\n */\nexport function connectableServices(): OAuthConnectable[] {\n return [...getStore().design];\n}\n\n/** A connectable service whose operator configured OAuth. */\nexport type Connectable = Required<OAuthConnectable>;\n\n/** The service addressed by `kind` when the operator configured OAuth for it, else undefined. */\nexport function findConnectable(kind: string): Connectable | undefined {\n const found = connectableServices().find(\n (service) => service.metadata.kind === kind && service.oauth,\n );\n return found as Connectable | undefined;\n}\n\n/** Connection storage bound to the server's credential keys. */\nexport function connectionModel(): ConnectionModel {\n const { db, config } = getStore();\n return new ConnectionModel(db, credentialKeys(config));\n}\n\n/** Connecting needs authentication (a user to own it) and a server secret (to sign the flow). */\nexport function canConnect(): boolean {\n const { authEnabled, user, config } = getStore();\n return authEnabled && Boolean(user) && Boolean(config.secret);\n}\n"],"mappings":";;;;;;;;;;;;;AAWA,SAAgB,sBAA0C;CACxD,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC,MAAM;AAC9B;;AAMA,SAAgB,gBAAgB,MAAuC;CAIrE,OAHc,oBAAoB,CAAC,CAAC,MACjC,YAAY,QAAQ,SAAS,SAAS,QAAQ,QAAQ,KAE9C;AACb;;AAGA,SAAgB,kBAAmC;CACjD,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,OAAO,IAAI,gBAAgB,IAAI,eAAe,MAAM,CAAC;AACvD;;AAGA,SAAgB,aAAsB;CACpC,MAAM,EAAE,aAAa,MAAM,WAAW,SAAS;CAC/C,OAAO,eAAe,QAAQ,IAAI,KAAK,QAAQ,OAAO,MAAM;AAC9D"}
@@ -0,0 +1,58 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { captureMetrics } from "@storyshelf/observability";
5
+ import { createCaptureConsumer } from "@storyshelf/core/capture";
6
+ //#region src/consumer-setup.ts
7
+ /**
8
+ * The consumer of the app's queues (ADR 0006, decision 5): built when first started, so a host
9
+ * that never starts one never touches the queue, and drained as a unit on teardown.
10
+ */
11
+ /** One consumer from options that are only worked out on the first `start()`. */
12
+ function lazyConsumer(options, closers = []) {
13
+ let built;
14
+ return {
15
+ start: async () => {
16
+ built ??= createCaptureConsumer(options());
17
+ await built.start();
18
+ },
19
+ stop: async () => {
20
+ await built?.stop();
21
+ for (const close of closers) await close();
22
+ },
23
+ isRunning: () => built?.isRunning() ?? false
24
+ };
25
+ }
26
+ /** Several consumers as one: they start together and drain together. */
27
+ function combineConsumers(consumers) {
28
+ const [only, ...rest] = consumers;
29
+ if (only === void 0) return null;
30
+ if (rest.length === 0) return only;
31
+ return {
32
+ start: async () => {
33
+ await Promise.all(consumers.map(async (consumer) => {
34
+ await consumer.start();
35
+ }));
36
+ },
37
+ stop: async () => {
38
+ await Promise.all(consumers.map(async (consumer) => {
39
+ await consumer.stop();
40
+ }));
41
+ },
42
+ isRunning: () => consumers.some((consumer) => consumer.isRunning())
43
+ };
44
+ }
45
+ /** Job counts and durations on the capture instruments, told apart by `job.kind`. */
46
+ function jobMetrics() {
47
+ return { jobFinished: (event) => {
48
+ const instruments = captureMetrics();
49
+ const attributes = { "job.kind": event.kind };
50
+ instruments.jobDuration.record(event.durationMs, attributes);
51
+ if (event.outcome === "completed") instruments.jobsCompleted.add(1, attributes);
52
+ else if (event.outcome === "failed") instruments.jobsFailed.add(1, attributes);
53
+ } };
54
+ }
55
+ //#endregion
56
+ export { combineConsumers, jobMetrics, lazyConsumer };
57
+
58
+ //# sourceMappingURL=consumer-setup.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer-setup.mjs","names":[],"sources":["../src/consumer-setup.ts"],"sourcesContent":["/**\n * The consumer of the app's queues (ADR 0006, decision 5): built when first started, so a host\n * that never starts one never touches the queue, and drained as a unit on teardown.\n */\nimport {\n createCaptureConsumer,\n type CaptureConsumer,\n type CaptureConsumerOptions,\n type ConsumerMetrics,\n} from \"@storyshelf/core/capture\";\nimport { captureMetrics } from \"@storyshelf/observability\";\n\n/** One consumer from options that are only worked out on the first `start()`. */\nexport function lazyConsumer(\n options: () => CaptureConsumerOptions,\n closers: (() => Promise<void>)[] = [],\n): CaptureConsumer {\n let built: CaptureConsumer | undefined;\n return {\n start: async () => {\n built ??= createCaptureConsumer(options());\n await built.start();\n },\n // Stopping one never started is a no-op; the closers (forked child, queue wake) still run.\n stop: async () => {\n await built?.stop();\n for (const close of closers) {\n // oxlint-disable-next-line no-await-in-loop -- close in order, after the drain\n await close();\n }\n },\n isRunning: () => built?.isRunning() ?? false,\n };\n}\n\n/** Several consumers as one: they start together and drain together. */\nexport function combineConsumers(consumers: CaptureConsumer[]): CaptureConsumer | null {\n const [only, ...rest] = consumers;\n if (only === undefined) {\n return null;\n }\n if (rest.length === 0) {\n return only;\n }\n return {\n start: async () => {\n await Promise.all(\n consumers.map(async (consumer) => {\n await consumer.start();\n }),\n );\n },\n stop: async () => {\n await Promise.all(\n consumers.map(async (consumer) => {\n await consumer.stop();\n }),\n );\n },\n isRunning: () => consumers.some((consumer) => consumer.isRunning()),\n };\n}\n\n/** Job counts and durations on the capture instruments, told apart by `job.kind`. */\nexport function jobMetrics(): ConsumerMetrics {\n return {\n jobFinished: (event) => {\n const instruments = captureMetrics();\n const attributes = { \"job.kind\": event.kind };\n instruments.jobDuration.record(event.durationMs, attributes);\n if (event.outcome === \"completed\") {\n instruments.jobsCompleted.add(1, attributes);\n } else if (event.outcome === \"failed\") {\n instruments.jobsFailed.add(1, attributes);\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;AAaA,SAAgB,aACd,SACA,UAAmC,CAAC,GACnB;CACjB,IAAI;CACJ,OAAO;EACL,OAAO,YAAY;GACjB,UAAU,sBAAsB,QAAQ,CAAC;GACzC,MAAM,MAAM,MAAM;EACpB;EAEA,MAAM,YAAY;GAChB,MAAM,OAAO,KAAK;GAClB,KAAK,MAAM,SAAS,SAElB,MAAM,MAAM;EAEhB;EACA,iBAAiB,OAAO,UAAU,KAAK;CACzC;AACF;;AAGA,SAAgB,iBAAiB,WAAsD;CACrF,MAAM,CAAC,MAAM,GAAG,QAAQ;CACxB,IAAI,SAAS,KAAA,GACX,OAAO;CAET,IAAI,KAAK,WAAW,GAClB,OAAO;CAET,OAAO;EACL,OAAO,YAAY;GACjB,MAAM,QAAQ,IACZ,UAAU,IAAI,OAAO,aAAa;IAChC,MAAM,SAAS,MAAM;GACvB,CAAC,CACH;EACF;EACA,MAAM,YAAY;GAChB,MAAM,QAAQ,IACZ,UAAU,IAAI,OAAO,aAAa;IAChC,MAAM,SAAS,KAAK;GACtB,CAAC,CACH;EACF;EACA,iBAAiB,UAAU,MAAM,aAAa,SAAS,UAAU,CAAC;CACpE;AACF;;AAGA,SAAgB,aAA8B;CAC5C,OAAO,EACL,cAAc,UAAU;EACtB,MAAM,cAAc,eAAe;EACnC,MAAM,aAAa,EAAE,YAAY,MAAM,KAAK;EAC5C,YAAY,YAAY,OAAO,MAAM,YAAY,UAAU;EAC3D,IAAI,MAAM,YAAY,aACpB,YAAY,cAAc,IAAI,GAAG,UAAU;OACtC,IAAI,MAAM,YAAY,UAC3B,YAAY,WAAW,IAAI,GAAG,UAAU;CAE5C,EACF;AACF"}
@@ -0,0 +1,46 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { credentialKeys } from "./credential-keys.mjs";
5
+ import { getStore } from "./store.mjs";
6
+ import { notificationOrigin } from "./notifications-pause.mjs";
7
+ import { buildStateFlags } from "./build-state-flags.mjs";
8
+ import { createBuildStatePublisher, requestOpenBuildStates } from "@storyshelf/core/capture";
9
+ //#region src/git-status.ts
10
+ /** Ask for a build's state to be shown on its project's git channels (ADR 0014, decision 5). */
11
+ /**
12
+ * Show the build's current state (commit statuses and the pull-request comment) on every git
13
+ * channel of its project. Called on every transition a request causes: build creation, a review
14
+ * outcome, a retry. Delivery reads the build when it runs, so the call needs no state of its own;
15
+ * it never rejects, so a git host cannot fail the action that triggered it.
16
+ */
17
+ async function requestBuildState(buildId) {
18
+ const store = getStore();
19
+ await (store.buildState ?? inlinePublisher(store)).request(buildId);
20
+ }
21
+ /**
22
+ * An origin just became resolved (an admin confirmed or reported one): requests made while none was
23
+ * were dropped, so ask again for the builds that can still change (ADR 0014, decision 11).
24
+ * Resolves to how many builds were asked; never rejects.
25
+ */
26
+ async function requestOpenBuildStatesNow() {
27
+ const store = getStore();
28
+ return await requestOpenBuildStates(store, { request: requestBuildState });
29
+ }
30
+ /** Hand-built stores have no wiring: deliver inline through the store's own adapters. */
31
+ function inlinePublisher(store) {
32
+ return createBuildStatePublisher({ deps: {
33
+ db: store.db,
34
+ storage: store.storage,
35
+ notifiers: store.notifiers,
36
+ secret: credentialKeys(store.config),
37
+ config: store.config,
38
+ publicOrigin: async () => await notificationOrigin(store),
39
+ flags: (project) => buildStateFlags(store.testing, project),
40
+ logger: store.logger
41
+ } });
42
+ }
43
+ //#endregion
44
+ export { requestBuildState, requestOpenBuildStatesNow };
45
+
46
+ //# sourceMappingURL=git-status.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-status.mjs","names":[],"sources":["../src/git-status.ts"],"sourcesContent":["/** Ask for a build's state to be shown on its project's git channels (ADR 0014, decision 5). */\nimport { createBuildStatePublisher, requestOpenBuildStates } from \"@storyshelf/core/capture\";\nimport { buildStateFlags } from \"./build-state-flags.ts\";\nimport { credentialKeys } from \"./credential-keys.ts\";\nimport { notificationOrigin } from \"./notifications-pause.ts\";\nimport { getStore, type Store } from \"./store.ts\";\n\n/**\n * Show the build's current state (commit statuses and the pull-request comment) on every git\n * channel of its project. Called on every transition a request causes: build creation, a review\n * outcome, a retry. Delivery reads the build when it runs, so the call needs no state of its own;\n * it never rejects, so a git host cannot fail the action that triggered it.\n */\nexport async function requestBuildState(buildId: string): Promise<void> {\n const store = getStore();\n await (store.buildState ?? inlinePublisher(store)).request(buildId);\n}\n\n/**\n * An origin just became resolved (an admin confirmed or reported one): requests made while none was\n * were dropped, so ask again for the builds that can still change (ADR 0014, decision 11).\n * Resolves to how many builds were asked; never rejects.\n */\nexport async function requestOpenBuildStatesNow(): Promise<number> {\n const store = getStore();\n return await requestOpenBuildStates(store, { request: requestBuildState });\n}\n\n/** Hand-built stores have no wiring: deliver inline through the store's own adapters. */\nfunction inlinePublisher(store: Store): ReturnType<typeof createBuildStatePublisher> {\n return createBuildStatePublisher({\n deps: {\n db: store.db,\n storage: store.storage,\n notifiers: store.notifiers,\n secret: credentialKeys(store.config),\n config: store.config,\n publicOrigin: async () => await notificationOrigin(store),\n flags: (project) => buildStateFlags(store.testing, project),\n logger: store.logger,\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAaA,eAAsB,kBAAkB,SAAgC;CACtE,MAAM,QAAQ,SAAS;CACvB,OAAO,MAAM,cAAc,gBAAgB,KAAK,EAAA,CAAG,QAAQ,OAAO;AACpE;;;;;;AAOA,eAAsB,4BAA6C;CACjE,MAAM,QAAQ,SAAS;CACvB,OAAO,MAAM,uBAAuB,OAAO,EAAE,SAAS,kBAAkB,CAAC;AAC3E;;AAGA,SAAS,gBAAgB,OAA4D;CACnF,OAAO,0BAA0B,EAC/B,MAAM;EACJ,IAAI,MAAM;EACV,SAAS,MAAM;EACf,WAAW,MAAM;EACjB,QAAQ,eAAe,MAAM,MAAM;EACnC,QAAQ,MAAM;EACd,cAAc,YAAY,MAAM,mBAAmB,KAAK;EACxD,QAAQ,YAAY,gBAAgB,MAAM,SAAS,OAAO;EAC1D,QAAQ,MAAM;CAChB,EACF,CAAC;AACH"}
@@ -0,0 +1,24 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/hidden-data.ts
5
+ /**
6
+ * Snapshots stored in the database. With `testing` off they are hidden, not deleted (ADR 0006,
7
+ * decision 1): the System page and the startup log say how many.
8
+ */
9
+ async function countHiddenSnapshots(db) {
10
+ return await db.count(db.tables.snapshots);
11
+ }
12
+ /** Log the mode at boot; with `testing` off, also how much captured data it hides. */
13
+ async function logHostingOnlyMode(db, logger) {
14
+ const hiddenSnapshots = await countHiddenSnapshots(db);
15
+ if (hiddenSnapshots > 0) {
16
+ logger.warn({ hiddenSnapshots }, "testing is off: existing snapshots are hidden, not deleted; they reappear when testing is turned on");
17
+ return;
18
+ }
19
+ logger.info("testing is off: hosting Storybooks only (no capture, compare or baselines)");
20
+ }
21
+ //#endregion
22
+ export { countHiddenSnapshots, logHostingOnlyMode };
23
+
24
+ //# sourceMappingURL=hidden-data.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hidden-data.mjs","names":[],"sources":["../src/hidden-data.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { Logger } from \"@storyshelf/core/logger\";\n\n/**\n * Snapshots stored in the database. With `testing` off they are hidden, not deleted (ADR 0006,\n * decision 1): the System page and the startup log say how many.\n */\nexport async function countHiddenSnapshots(db: DatabaseAdapter): Promise<number> {\n return await db.count(db.tables.snapshots);\n}\n\n/** Log the mode at boot; with `testing` off, also how much captured data it hides. */\nexport async function logHostingOnlyMode(db: DatabaseAdapter, logger: Logger): Promise<void> {\n const hiddenSnapshots = await countHiddenSnapshots(db);\n if (hiddenSnapshots > 0) {\n logger.warn(\n { hiddenSnapshots },\n \"testing is off: existing snapshots are hidden, not deleted; they reappear when testing is turned on\",\n );\n return;\n }\n logger.info(\"testing is off: hosting Storybooks only (no capture, compare or baselines)\");\n}\n"],"mappings":";;;;;;;;AAOA,eAAsB,qBAAqB,IAAsC;CAC/E,OAAO,MAAM,GAAG,MAAM,GAAG,OAAO,SAAS;AAC3C;;AAGA,eAAsB,mBAAmB,IAAqB,QAA+B;CAC3F,MAAM,kBAAkB,MAAM,qBAAqB,EAAE;CACrD,IAAI,kBAAkB,GAAG;EACvB,OAAO,KACL,EAAE,gBAAgB,GAClB,qGACF;EACA;CACF;CACA,OAAO,KAAK,4EAA4E;AAC1F"}