@storyshelf/app 0.6.2 → 0.8.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.
- package/README.md +42 -4
- package/dist/app-types.d.mts +2 -6
- package/dist/asset-manifest.mjs +14 -7
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +9 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +9 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +79 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +17 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +74 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +45 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +58 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +96 -119
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +33 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +57 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/credential-keys.mjs +1 -2
- package/dist/credentials.mjs +1 -2
- package/dist/git-status.mjs +45 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +23 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -5
- package/dist/index.mjs +204 -127
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +48 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +20 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +21 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +39 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +102 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +58 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +97 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +27 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +112 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +20 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +59 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +47 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +13 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +50 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +131 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +93 -22
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +5 -4
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/csrf.mjs +1 -2
- package/dist/middleware/rate-limit.mjs +42 -35
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +4 -4
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +50 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +40 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/server-timing.mjs +1 -2
- package/dist/middleware/setup-gate.mjs +1 -2
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +13 -4
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +32 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +3 -4
- package/dist/notify.mjs +79 -41
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +115 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +77 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +132 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +52 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +64 -15
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +105 -78
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +88 -35
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +17 -16
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-default.mjs +53 -0
- package/dist/pages/build-diff-default.mjs.map +1 -0
- package/dist/pages/build-diff-environment.mjs +81 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +23 -6
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +112 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +74 -12
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +29 -14
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-reference.mjs +84 -0
- package/dist/pages/build-diff-reference.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +6 -3
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-stale.mjs +1 -2
- package/dist/pages/build-diff-usage.mjs +62 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +97 -14
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +37 -22
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +73 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +248 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +167 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +14 -28
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +181 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +52 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +125 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/invite.mjs +1 -2
- package/dist/pages/label-detail.mjs +1 -2
- package/dist/pages/library-grid.mjs +104 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +46 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +129 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +113 -220
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/login.mjs +1 -2
- package/dist/pages/notifications-paused.mjs +21 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/passkey-ceremony.mjs +1 -2
- package/dist/pages/profile.mjs +54 -50
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +3 -4
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +52 -23
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-create.mjs +1 -2
- package/dist/pages/project-settings.mjs +52 -14
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +15 -3
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +7 -4
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +115 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +102 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +101 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/root.mjs +1 -2
- package/dist/pages/settings-ai.mjs +68 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +123 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +98 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +178 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +47 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +47 -23
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +44 -4
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +111 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +1 -2
- package/dist/pages/settings-notifications-git.mjs +127 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +80 -44
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +34 -4
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/settings-tokens.mjs +1 -2
- package/dist/pages/settings-webhooks.mjs +1 -2
- package/dist/pages/storybook.mjs +1 -2
- package/dist/pages/target-choices.mjs +38 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +88 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +12 -12
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +47 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +46 -5
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +92 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/admin.mjs +8 -4
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs +10 -4
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/attempts.mjs +1 -2
- package/dist/routers/auth-sync.mjs +1 -2
- package/dist/routers/auth.mjs +2 -3
- package/dist/routers/baseline-guard.mjs +4 -5
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baseline-reference.mjs +72 -0
- package/dist/routers/baseline-reference.mjs.map +1 -0
- package/dist/routers/baselines.handlers.mjs +63 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +124 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +67 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +69 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +85 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +73 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +65 -60
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +77 -19
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-cache.mjs +29 -0
- package/dist/routers/capture-cache.mjs.map +1 -0
- package/dist/routers/capture-config.mjs +34 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +42 -17
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/comparison-image.mjs +29 -0
- package/dist/routers/comparison-image.mjs.map +1 -0
- package/dist/routers/component-catalog.handlers.mjs +70 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +120 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +117 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +43 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +113 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/flash.mjs +1 -2
- package/dist/routers/health-report.mjs +3 -5
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +5 -5
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +20 -5
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/htmx.mjs +1 -2
- package/dist/routers/insights-ui.mjs +63 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +44 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +188 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/labels.mjs +1 -2
- package/dist/routers/links.handlers.mjs +79 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +138 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +30 -7
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +3 -3
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +18 -25
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/notification-subscriptions.mjs +1 -2
- package/dist/routers/oauth-state.mjs +44 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +37 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +50 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +4 -3
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +18 -3
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +85 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +51 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-compression.mjs +109 -0
- package/dist/routers/published-compression.mjs.map +1 -0
- package/dist/routers/published-files.mjs +69 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +71 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-next.mjs +1 -2
- package/dist/routers/review-section.handlers.mjs +84 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +28 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +141 -24
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +21 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +34 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +76 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +102 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +56 -12
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +76 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +28 -3
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +62 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +5 -9
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +16 -12
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +3 -7
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +8 -26
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +108 -28
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +17 -6
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +55 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +62 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +104 -7
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +57 -104
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +3 -7
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +38 -7
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +6 -13
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +12 -6
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +92 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +37 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +21 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +103 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +19 -15
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +4 -0
- package/dist/store.mjs +20 -3
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +45 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/avatar.mjs +1 -2
- package/dist/ui/buttons.mjs +17 -5
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/code-block.mjs +1 -2
- package/dist/ui/compare-stage.mjs +86 -54
- package/dist/ui/compare-stage.mjs.map +1 -1
- package/dist/ui/csrf-field.mjs +1 -2
- package/dist/ui/css.mjs +1 -2
- package/dist/ui/design-reference.mjs +96 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +2 -4
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/dropdown.mjs +1 -2
- package/dist/ui/feedback.mjs +1 -2
- package/dist/ui/filter-input.mjs +1 -2
- package/dist/ui/forms.mjs +7 -7
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/icon.mjs +1 -2
- package/dist/ui/icons/paths.mjs +2 -2
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/icons/sprite.mjs +1 -2
- package/dist/ui/kbd.mjs +1 -2
- package/dist/ui/layout.mjs +1 -2
- package/dist/ui/preview-embed.mjs +81 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/progress.mjs +1 -2
- package/dist/ui/segmented.mjs +1 -2
- package/dist/ui/shell/account.mjs +1 -2
- package/dist/ui/shell/bare.mjs +1 -2
- package/dist/ui/shell/boot-script.mjs +1 -2
- package/dist/ui/shell/client-script.mjs +86 -2
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +13 -10
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +25 -3
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +3 -4
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/dist/ui/shell/topbar.mjs +1 -2
- package/dist/ui/stacks.mjs +1 -2
- package/dist/ui/styles/base.mjs +1 -2
- package/dist/ui/styles/behavior.mjs +1 -2
- package/dist/ui/styles/review-layout.mjs +1 -2
- package/dist/ui/styles/review.mjs +1 -2
- package/dist/ui/styles/tokens.mjs +1 -2
- package/dist/ui/styles.mjs +1 -2
- package/dist/ui/subnav.mjs +1 -2
- package/dist/ui/table.mjs +1 -2
- package/dist/ui/theme.mjs +1 -2
- package/dist/ui/thumbnail.mjs +1 -2
- package/dist/ui/time.mjs +1 -2
- package/package.json +14 -12
- package/dist/asset-paths.mjs +0 -23
- package/dist/asset-paths.mjs.map +0 -1
- package/dist/assets/mark.svg +0 -1
- package/dist/pages/settings-status.mjs +0 -137
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -77
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -108
- package/dist/routers/status-configs.mjs.map +0 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { InsightModel } from "@storyshelf/core/models";
|
|
5
|
+
import { STALE_RUN_MS } from "@storyshelf/core/retention";
|
|
6
|
+
// src/insights/stale.ts
|
|
7
|
+
/** Lazy interruption check: a run stuck past the stale window is failed on read. */
|
|
8
|
+
/** Return the row, first marking it `interrupted` when it is stale and still in flight. */
|
|
9
|
+
async function freshen(deps, row) {
|
|
10
|
+
if (!(row.status === "pending" || row.status === "running") || Date.now() - Date.parse(row.createdAt) < STALE_RUN_MS) return row;
|
|
11
|
+
await new InsightModel(deps.db).markFailed(row.id, "interrupted");
|
|
12
|
+
return {
|
|
13
|
+
...row,
|
|
14
|
+
status: "failed",
|
|
15
|
+
errorCode: "interrupted"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export { freshen };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=stale.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stale.mjs","names":[],"sources":["../../src/insights/stale.ts"],"sourcesContent":["import { InsightModel } from \"@storyshelf/core/models\";\n/** Lazy interruption check: a run stuck past the stale window is failed on read. */\nimport { STALE_RUN_MS } from \"@storyshelf/core/retention\";\nimport type { InsightRow } from \"@storyshelf/core/schema\";\nimport type { InsightDeps } from \"./deps.ts\";\n\n/** Return the row, first marking it `interrupted` when it is stale and still in flight. */\nexport async function freshen(deps: InsightDeps, row: InsightRow): Promise<InsightRow> {\n const inFlight = row.status === \"pending\" || row.status === \"running\";\n if (!inFlight || Date.now() - Date.parse(row.createdAt) < STALE_RUN_MS) {\n return row;\n }\n await new InsightModel(deps.db).markFailed(row.id, \"interrupted\");\n return { ...row, status: \"failed\", errorCode: \"interrupted\" };\n}\n"],"mappings":";;;;;;;;AAOA,eAAsB,QAAQ,MAAmB,KAAsC;CAErF,IAAI,EADa,IAAI,WAAW,aAAa,IAAI,WAAW,cAC3C,KAAK,IAAI,IAAI,KAAK,MAAM,IAAI,SAAS,IAAI,cACxD,OAAO;CAET,MAAM,IAAI,aAAa,KAAK,EAAE,CAAC,CAAC,WAAW,IAAI,IAAI,aAAa;CAChE,OAAO;EAAE,GAAG;EAAK,QAAQ;EAAU,WAAW;CAAc;AAC9D"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { AiUsageModel } from "@storyshelf/core/models";
|
|
5
|
+
import { countedTokens, utcDay } from "@storyshelf/core/insights";
|
|
6
|
+
// src/insights/usage-report.ts
|
|
7
|
+
/** Aggregates for the site-admin AI usage page (counted tokens per day and profile). */
|
|
8
|
+
const DAY_MS = 864e5;
|
|
9
|
+
function addTo(map, key, make, tokens, failed) {
|
|
10
|
+
const current = map.get(key) ?? make(key);
|
|
11
|
+
const next = {
|
|
12
|
+
...current,
|
|
13
|
+
tokens: current.tokens + tokens,
|
|
14
|
+
calls: current.calls + 1
|
|
15
|
+
};
|
|
16
|
+
map.set(key, "failed" in next ? {
|
|
17
|
+
...next,
|
|
18
|
+
failed: Number(next.failed) + Number(failed)
|
|
19
|
+
} : next);
|
|
20
|
+
}
|
|
21
|
+
function aggregate(rows, visionWeight) {
|
|
22
|
+
const days = /* @__PURE__ */ new Map();
|
|
23
|
+
const profiles = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
const tokens = countedTokens(row, row.images, { visionWeight });
|
|
26
|
+
const failed = row.status === "failed";
|
|
27
|
+
addTo(days, row.createdAt.slice(0, 10), (day) => ({
|
|
28
|
+
day,
|
|
29
|
+
tokens: 0,
|
|
30
|
+
calls: 0,
|
|
31
|
+
failed: 0
|
|
32
|
+
}), tokens, failed);
|
|
33
|
+
addTo(profiles, row.profile, (profile) => ({
|
|
34
|
+
profile,
|
|
35
|
+
tokens: 0,
|
|
36
|
+
calls: 0
|
|
37
|
+
}), tokens, failed);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
days: [...days.values()].toSorted((a, b) => b.day.localeCompare(a.day)),
|
|
41
|
+
profiles: [...profiles.values()].toSorted((a, b) => b.tokens - a.tokens)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Summarize the last `windowDays` of usage (UTC days, newest first). */
|
|
45
|
+
async function buildUsageReport(deps, windowDays = 14, now = /* @__PURE__ */ new Date()) {
|
|
46
|
+
const since = (/* @__PURE__ */ new Date(now.getTime() - windowDays * DAY_MS)).toISOString();
|
|
47
|
+
const rows = await new AiUsageModel(deps.db).listSince(since);
|
|
48
|
+
const { visionWeight, dailyTokens } = deps.ai.budget();
|
|
49
|
+
const { days, profiles } = aggregate(rows, visionWeight);
|
|
50
|
+
return {
|
|
51
|
+
days,
|
|
52
|
+
profiles,
|
|
53
|
+
todayTokens: days.find((d) => d.day === utcDay(now))?.tokens ?? 0,
|
|
54
|
+
dailyTokens: dailyTokens ?? null
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export { buildUsageReport };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=usage-report.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-report.mjs","names":[],"sources":["../../src/insights/usage-report.ts"],"sourcesContent":["/** Aggregates for the site-admin AI usage page (counted tokens per day and profile). */\nimport { countedTokens, utcDay } from \"@storyshelf/core/insights\";\nimport { AiUsageModel } from \"@storyshelf/core/models\";\nimport type { AiUsageRow } from \"@storyshelf/core/schema\";\nimport type { InsightDeps } from \"./deps.ts\";\n\n/** One day's totals. */\nexport interface UsageDay {\n day: string;\n tokens: number;\n calls: number;\n failed: number;\n}\n\n/** One profile's totals over the window. */\nexport interface UsageProfile {\n profile: string;\n tokens: number;\n calls: number;\n}\n\n/** What the admin page shows. */\nexport interface UsageReport {\n days: UsageDay[];\n profiles: UsageProfile[];\n todayTokens: number;\n dailyTokens: number | null;\n}\n\nconst DAY_MS = 86_400_000;\n\nfunction addTo<K extends string, V extends { tokens: number; calls: number }>(\n map: Map<string, V>,\n key: K,\n make: (key: K) => V,\n tokens: number,\n failed: boolean,\n): void {\n const current = map.get(key) ?? make(key);\n const next = { ...current, tokens: current.tokens + tokens, calls: current.calls + 1 };\n map.set(key, \"failed\" in next ? { ...next, failed: Number(next.failed) + Number(failed) } : next);\n}\n\nfunction aggregate(\n rows: AiUsageRow[],\n visionWeight: number,\n): { days: UsageDay[]; profiles: UsageProfile[] } {\n const days = new Map<string, UsageDay>();\n const profiles = new Map<string, UsageProfile>();\n for (const row of rows) {\n const tokens = countedTokens(row, row.images, { visionWeight });\n const failed = row.status === \"failed\";\n addTo(\n days,\n row.createdAt.slice(0, 10),\n (day) => ({ day, tokens: 0, calls: 0, failed: 0 }),\n tokens,\n failed,\n );\n addTo(profiles, row.profile, (profile) => ({ profile, tokens: 0, calls: 0 }), tokens, failed);\n }\n return {\n days: [...days.values()].toSorted((a, b) => b.day.localeCompare(a.day)),\n profiles: [...profiles.values()].toSorted((a, b) => b.tokens - a.tokens),\n };\n}\n\n/** Summarize the last `windowDays` of usage (UTC days, newest first). */\nexport async function buildUsageReport(\n deps: InsightDeps,\n windowDays = 14,\n now = new Date(),\n): Promise<UsageReport> {\n const since = new Date(now.getTime() - windowDays * DAY_MS).toISOString();\n const rows = await new AiUsageModel(deps.db).listSince(since);\n const { visionWeight, dailyTokens } = deps.ai.budget();\n const { days, profiles } = aggregate(rows, visionWeight);\n return {\n days,\n profiles,\n todayTokens: days.find((d) => d.day === utcDay(now))?.tokens ?? 0,\n dailyTokens: dailyTokens ?? null,\n };\n}\n"],"mappings":";;;;;;;AA6BA,MAAM,SAAS;AAEf,SAAS,MACP,KACA,KACA,MACA,QACA,QACM;CACN,MAAM,UAAU,IAAI,IAAI,GAAG,KAAK,KAAK,GAAG;CACxC,MAAM,OAAO;EAAE,GAAG;EAAS,QAAQ,QAAQ,SAAS;EAAQ,OAAO,QAAQ,QAAQ;CAAE;CACrF,IAAI,IAAI,KAAK,YAAY,OAAO;EAAE,GAAG;EAAM,QAAQ,OAAO,KAAK,MAAM,IAAI,OAAO,MAAM;CAAE,IAAI,IAAI;AAClG;AAEA,SAAS,UACP,MACA,cACgD;CAChD,MAAM,uBAAO,IAAI,IAAsB;CACvC,MAAM,2BAAW,IAAI,IAA0B;CAC/C,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,SAAS,cAAc,KAAK,IAAI,QAAQ,EAAE,aAAa,CAAC;EAC9D,MAAM,SAAS,IAAI,WAAW;EAC9B,MACE,MACA,IAAI,UAAU,MAAM,GAAG,EAAE,IACxB,SAAS;GAAE;GAAK,QAAQ;GAAG,OAAO;GAAG,QAAQ;EAAE,IAChD,QACA,MACF;EACA,MAAM,UAAU,IAAI,UAAU,aAAa;GAAE;GAAS,QAAQ;GAAG,OAAO;EAAE,IAAI,QAAQ,MAAM;CAC9F;CACA,OAAO;EACL,MAAM,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,UAAU,GAAG,MAAM,EAAE,IAAI,cAAc,EAAE,GAAG,CAAC;EACtE,UAAU,CAAC,GAAG,SAAS,OAAO,CAAC,CAAC,CAAC,UAAU,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;CACzE;AACF;;AAGA,eAAsB,iBACpB,MACA,aAAa,IACb,sBAAM,IAAI,KAAK,GACO;CACtB,MAAM,yBAAQ,IAAI,KAAK,IAAI,QAAQ,IAAI,aAAa,MAAM,EAAA,CAAE,YAAY;CACxE,MAAM,OAAO,MAAM,IAAI,aAAa,KAAK,EAAE,CAAC,CAAC,UAAU,KAAK;CAC5D,MAAM,EAAE,cAAc,gBAAgB,KAAK,GAAG,OAAO;CACrD,MAAM,EAAE,MAAM,aAAa,UAAU,MAAM,YAAY;CACvD,OAAO;EACL;EACA;EACA,aAAa,KAAK,MAAM,MAAM,EAAE,QAAQ,OAAO,GAAG,CAAC,CAAC,EAAE,UAAU;EAChE,aAAa,eAAe;CAC9B;AACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
// src/insights/view.ts
|
|
5
|
+
function parseResult(raw) {
|
|
6
|
+
if (!raw) return {
|
|
7
|
+
output: null,
|
|
8
|
+
meta: null
|
|
9
|
+
};
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
return {
|
|
13
|
+
output: parsed.output ?? null,
|
|
14
|
+
meta: parsed.meta ?? null
|
|
15
|
+
};
|
|
16
|
+
} catch {
|
|
17
|
+
return {
|
|
18
|
+
output: null,
|
|
19
|
+
meta: null
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Convert a stored row to its public view. */
|
|
24
|
+
function toInsightView(row) {
|
|
25
|
+
const { output, meta } = parseResult(row.result);
|
|
26
|
+
return {
|
|
27
|
+
id: row.id,
|
|
28
|
+
kind: row.kind,
|
|
29
|
+
buildId: row.buildId,
|
|
30
|
+
window: row.windowKey,
|
|
31
|
+
status: row.status,
|
|
32
|
+
profile: row.profile,
|
|
33
|
+
model: row.model,
|
|
34
|
+
promptVersion: row.promptVersion,
|
|
35
|
+
verdict: row.verdict,
|
|
36
|
+
summary: row.summary,
|
|
37
|
+
output,
|
|
38
|
+
meta,
|
|
39
|
+
errorCode: row.errorCode,
|
|
40
|
+
createdAt: row.createdAt,
|
|
41
|
+
startedAt: row.startedAt,
|
|
42
|
+
finishedAt: row.finishedAt
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export { toInsightView };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=view.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view.mjs","names":[],"sources":["../../src/insights/view.ts"],"sourcesContent":["/** API/UI view of an insight row (parses the stored envelope defensively). */\nimport type { InsightRow } from \"@storyshelf/core/schema\";\n\n/** Public shape of an insight. */\nexport interface InsightView {\n id: string;\n kind: string;\n buildId: string | null;\n window: string | null;\n status: string;\n profile: string;\n model: string;\n promptVersion: string;\n verdict: string | null;\n summary: string | null;\n output: unknown;\n meta: unknown;\n errorCode: string | null;\n createdAt: string;\n startedAt: string | null;\n finishedAt: string | null;\n}\n\nfunction parseResult(raw: string | null): { output: unknown; meta: unknown } {\n if (!raw) {\n return { output: null, meta: null };\n }\n try {\n const parsed = JSON.parse(raw) as { output?: unknown; meta?: unknown };\n return { output: parsed.output ?? null, meta: parsed.meta ?? null };\n } catch {\n return { output: null, meta: null };\n }\n}\n\n/** Convert a stored row to its public view. */\nexport function toInsightView(row: InsightRow): InsightView {\n const { output, meta } = parseResult(row.result);\n return {\n id: row.id,\n kind: row.kind,\n buildId: row.buildId,\n window: row.windowKey,\n status: row.status,\n profile: row.profile,\n model: row.model,\n promptVersion: row.promptVersion,\n verdict: row.verdict,\n summary: row.summary,\n output,\n meta,\n errorCode: row.errorCode,\n createdAt: row.createdAt,\n startedAt: row.startedAt,\n finishedAt: row.finishedAt,\n };\n}\n"],"mappings":";;;;AAuBA,SAAS,YAAY,KAAwD;CAC3E,IAAI,CAAC,KACH,OAAO;EAAE,QAAQ;EAAM,MAAM;CAAK;CAEpC,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,GAAG;EAC7B,OAAO;GAAE,QAAQ,OAAO,UAAU;GAAM,MAAM,OAAO,QAAQ;EAAK;CACpE,QAAQ;EACN,OAAO;GAAE,QAAQ;GAAM,MAAM;EAAK;CACpC;AACF;;AAGA,SAAgB,cAAc,KAA8B;CAC1D,MAAM,EAAE,QAAQ,SAAS,YAAY,IAAI,MAAM;CAC/C,OAAO;EACL,IAAI,IAAI;EACR,MAAM,IAAI;EACV,SAAS,IAAI;EACb,QAAQ,IAAI;EACZ,QAAQ,IAAI;EACZ,SAAS,IAAI;EACb,OAAO,IAAI;EACX,eAAe,IAAI;EACnB,SAAS,IAAI;EACb,SAAS,IAAI;EACb;EACA;EACA,WAAW,IAAI;EACf,WAAW,IAAI;EACf,WAAW,IAAI;EACf,YAAY,IAAI;CAClB;AACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
// src/job-canceller.ts
|
|
5
|
+
/** Ask each queue that can cancel to cancel the build's jobs; a queue without `cancel` is skipped. */
|
|
6
|
+
function createJobCanceller(queues) {
|
|
7
|
+
return async (buildId) => {
|
|
8
|
+
return (await Promise.all(queues.map(async (queue) => await queue?.cancel?.(buildId) ?? false))).some(Boolean);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export { createJobCanceller };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=job-canceller.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-canceller.mjs","names":[],"sources":["../src/job-canceller.ts"],"sourcesContent":["/**\n * Cancelling a build's background jobs (ADR 0006): every job kind of the build shares the\n * `capture_jobs` table, so one cancel by build id on the database queue covers capture, publish and\n * notify jobs.\n */\nimport type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\n\n/** Cancels the unfinished jobs of a build; resolves true when any job was affected. */\nexport type JobCanceller = (buildId: string) => Promise<boolean>;\n\n/** Ask each queue that can cancel to cancel the build's jobs; a queue without `cancel` is skipped. */\nexport function createJobCanceller(\n queues: readonly (CaptureQueue | null | undefined)[],\n): JobCanceller {\n return async (buildId) => {\n const outcomes = await Promise.all(\n queues.map(async (queue) => (await queue?.cancel?.(buildId)) ?? false),\n );\n return outcomes.some(Boolean);\n };\n}\n"],"mappings":";;;;;AAWA,SAAgB,mBACd,QACc;CACd,OAAO,OAAO,YAAY;EAIxB,QAAO,MAHgB,QAAQ,IAC7B,OAAO,IAAI,OAAO,UAAW,MAAM,OAAO,SAAS,OAAO,KAAM,KAAK,CACvE,EAAA,CACgB,KAAK,OAAO;CAC9B;AACF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { DATABASE_QUEUE_METADATA, createDatabaseCaptureQueue } from "@storyshelf/core/capture";
|
|
5
|
+
// src/job-queue.ts
|
|
6
|
+
/**
|
|
7
|
+
* Create the lazy queue polling `kinds`, with `retries` after a first failed attempt, the lease
|
|
8
|
+
* of `testing.leaseSeconds` and the clock of `testing.queueClock` (production: real time).
|
|
9
|
+
*/
|
|
10
|
+
function createJobQueue(options, kinds, retries) {
|
|
11
|
+
let queue;
|
|
12
|
+
const exhausted = {};
|
|
13
|
+
const get = () => {
|
|
14
|
+
queue ??= createDatabaseCaptureQueue(options.database, {
|
|
15
|
+
kinds: kinds.length > 0 ? kinds : ["capture"],
|
|
16
|
+
retries,
|
|
17
|
+
...options.testing?.leaseSeconds === void 0 ? {} : { leaseMs: Math.round(options.testing.leaseSeconds * 1e3) },
|
|
18
|
+
...options.testing?.queueClock === void 0 ? {} : {
|
|
19
|
+
now: options.testing.queueClock.now,
|
|
20
|
+
timers: options.testing.queueClock.timers
|
|
21
|
+
},
|
|
22
|
+
onExhausted: async (job, error) => {
|
|
23
|
+
await exhausted[job.kind]?.(job, error);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return queue;
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
kinds,
|
|
30
|
+
get,
|
|
31
|
+
built: () => queue,
|
|
32
|
+
view: queueView(get),
|
|
33
|
+
exhausted
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function queueView(get) {
|
|
37
|
+
return {
|
|
38
|
+
metadata: DATABASE_QUEUE_METADATA,
|
|
39
|
+
enqueue: async (job) => {
|
|
40
|
+
await get().enqueue(job);
|
|
41
|
+
},
|
|
42
|
+
status: async (buildId) => await get().status(buildId),
|
|
43
|
+
active: async () => await get().active(),
|
|
44
|
+
recent: async (limit) => await get().recent(limit),
|
|
45
|
+
cancel: async (id) => await get().cancel(id)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export { createJobQueue };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=job-queue.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-queue.mjs","names":[],"sources":["../src/job-queue.ts"],"sourcesContent":["/**\n * The app's one job queue (ADR 0006, decision 4): the database queue, built on first use and\n * shared by every job kind. Capture, publish and notify jobs are enqueued onto it and consumed by\n * one consumer with a handler per kind. Building it lazily keeps hosts that never enqueue or\n * consume (OpenAPI generation, stub-table tests) from touching `capture_jobs`.\n */\nimport type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\nimport {\n createDatabaseCaptureQueue,\n DATABASE_QUEUE_METADATA,\n type DatabaseCaptureQueue,\n type DatabaseJob,\n} from \"@storyshelf/core/capture\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport type { CaptureJobKind } from \"@storyshelf/core/schema\";\n\n/** What a kind does when a job of it failed because its last attempt lost its lease. */\nexport type ExhaustedHandler = (job: DatabaseJob, error: string) => Promise<void>;\n\n/** The shared database queue. */\nexport interface JobQueue {\n /** Job kinds this instance polls; empty when it consumes none. */\n kinds: readonly CaptureJobKind[];\n /** The queue, built on first use. */\n get(): DatabaseCaptureQueue;\n /** The queue if it was ever built. */\n built(): DatabaseCaptureQueue | undefined;\n /** Capture-shaped view for pages and the store; builds the queue only when called. */\n view: CaptureQueue;\n /** Per-kind hooks for exhausted jobs, filled in by the kind's setup. */\n exhausted: Partial<Record<CaptureJobKind, ExhaustedHandler>>;\n}\n\n/**\n * Create the lazy queue polling `kinds`, with `retries` after a first failed attempt, the lease\n * of `testing.leaseSeconds` and the clock of `testing.queueClock` (production: real time).\n */\nexport function createJobQueue(\n options: ShelfOptions,\n kinds: readonly CaptureJobKind[],\n retries: number,\n): JobQueue {\n let queue: DatabaseCaptureQueue | undefined;\n const exhausted: JobQueue[\"exhausted\"] = {};\n const get = (): DatabaseCaptureQueue => {\n queue ??= createDatabaseCaptureQueue(options.database, {\n kinds: kinds.length > 0 ? kinds : [\"capture\"],\n retries,\n ...(options.testing?.leaseSeconds === undefined\n ? {}\n : { leaseMs: Math.round(options.testing.leaseSeconds * 1000) }),\n ...(options.testing?.queueClock === undefined\n ? {}\n : { now: options.testing.queueClock.now, timers: options.testing.queueClock.timers }),\n onExhausted: async (job, error) => {\n await exhausted[job.kind]?.(job, error);\n },\n });\n return queue;\n };\n return { kinds, get, built: () => queue, view: queueView(get), exhausted };\n}\n\nfunction queueView(get: () => DatabaseCaptureQueue): CaptureQueue {\n return {\n metadata: DATABASE_QUEUE_METADATA,\n enqueue: async (job) => {\n await get().enqueue(job);\n },\n status: async (buildId) => await get().status(buildId),\n active: async () => await get().active(),\n recent: async (limit) => await get().recent(limit),\n cancel: async (id) => await get().cancel(id),\n };\n}\n"],"mappings":";;;;;;;;;AAqCA,SAAgB,eACd,SACA,OACA,SACU;CACV,IAAI;CACJ,MAAM,YAAmC,CAAC;CAC1C,MAAM,YAAkC;EACtC,UAAU,2BAA2B,QAAQ,UAAU;GACrD,OAAO,MAAM,SAAS,IAAI,QAAQ,CAAC,SAAS;GAC5C;GACA,GAAI,QAAQ,SAAS,iBAAiB,KAAA,IAClC,CAAC,IACD,EAAE,SAAS,KAAK,MAAM,QAAQ,QAAQ,eAAe,GAAI,EAAE;GAC/D,GAAI,QAAQ,SAAS,eAAe,KAAA,IAChC,CAAC,IACD;IAAE,KAAK,QAAQ,QAAQ,WAAW;IAAK,QAAQ,QAAQ,QAAQ,WAAW;GAAO;GACrF,aAAa,OAAO,KAAK,UAAU;IACjC,MAAM,UAAU,IAAI,KAAK,GAAG,KAAK,KAAK;GACxC;EACF,CAAC;EACD,OAAO;CACT;CACA,OAAO;EAAE;EAAO;EAAK,aAAa;EAAO,MAAM,UAAU,GAAG;EAAG;CAAU;AAC3E;AAEA,SAAS,UAAU,KAA+C;CAChE,OAAO;EACL,UAAU;EACV,SAAS,OAAO,QAAQ;GACtB,MAAM,IAAI,CAAC,CAAC,QAAQ,GAAG;EACzB;EACA,QAAQ,OAAO,YAAY,MAAM,IAAI,CAAC,CAAC,OAAO,OAAO;EACrD,QAAQ,YAAY,MAAM,IAAI,CAAC,CAAC,OAAO;EACvC,QAAQ,OAAO,UAAU,MAAM,IAAI,CAAC,CAAC,OAAO,KAAK;EACjD,QAAQ,OAAO,OAAO,MAAM,IAAI,CAAC,CAAC,OAAO,EAAE;CAC7C;AACF"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { setupBuildState } from "./build-state-setup.mjs";
|
|
5
|
+
import { setupCapture } from "./capture-setup.mjs";
|
|
6
|
+
import { combineConsumers, jobMetrics, lazyConsumer } from "./consumer-setup.mjs";
|
|
7
|
+
import { createJobCanceller } from "./job-canceller.mjs";
|
|
8
|
+
import { createJobQueue } from "./job-queue.mjs";
|
|
9
|
+
import { setupPublishing } from "./publish-setup.mjs";
|
|
10
|
+
import { REVIEW_EVENTS, announceReviewWith, reviewRequestedData } from "./review-events.mjs";
|
|
11
|
+
import { isServerlessHost as isServerlessHost$1 } from "./serverless.mjs";
|
|
12
|
+
import { DEFAULT_SCRATCH_DIR } from "@storyshelf/core/config";
|
|
13
|
+
import { DEFAULT_RETRIES, NOTIFY_MAX_ATTEMPTS } from "@storyshelf/core/capture";
|
|
14
|
+
// src/jobs-setup.ts
|
|
15
|
+
/**
|
|
16
|
+
* Background jobs of the app (ADR 0006): the one database queue, the handlers of its job kinds
|
|
17
|
+
* (`capture` with `testing`, `publish` on a long-running host, `notify` on a queue of its own) and the one consumer that runs
|
|
18
|
+
* them.
|
|
19
|
+
*/
|
|
20
|
+
/** Wire the queue, the handlers and the consumer for `options`. */
|
|
21
|
+
function setupJobs(options, runtime, env = process.env) {
|
|
22
|
+
const { jobs, notifyJobs } = queuesFor(options, isServerlessHost$1(env));
|
|
23
|
+
const buildState = setupBuildState(options, runtime, notifyJobs);
|
|
24
|
+
const capture = setupCapture(options, runtime.config, {
|
|
25
|
+
notifiers: runtime.notifiers,
|
|
26
|
+
buildState: buildState.publisher,
|
|
27
|
+
publicOrigin: async () => await runtime.publicOrigin.url()
|
|
28
|
+
}, runtime.logger, jobs);
|
|
29
|
+
const publishing = setupPublishing(options, runtime.config, runtime.logger, capture.enqueueCapture !== void 0, {
|
|
30
|
+
jobs,
|
|
31
|
+
buildState: buildState.publisher,
|
|
32
|
+
reviewRequested: reviewRequestedFor(options, runtime)
|
|
33
|
+
});
|
|
34
|
+
const wiring = {
|
|
35
|
+
capture,
|
|
36
|
+
publishing,
|
|
37
|
+
buildState,
|
|
38
|
+
notifyJobs
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
captureQueue: capture.queue,
|
|
42
|
+
enqueueCapture: capture.enqueueCapture,
|
|
43
|
+
capturesHere: capture.handler !== null,
|
|
44
|
+
publish: publishing,
|
|
45
|
+
buildState: buildState.publisher,
|
|
46
|
+
cancelBuildJobs: createJobCanceller([jobs.view]),
|
|
47
|
+
consumer: combineConsumers(consumersOf(options, runtime, jobs, wiring))
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The database queue (captures with `testing`, publishing on a long-running host) and,
|
|
52
|
+
* on a long-running host, a queue of its own for `notify` jobs: a status must not wait behind a
|
|
53
|
+
* capture that holds every slot for minutes.
|
|
54
|
+
*/
|
|
55
|
+
function queuesFor(options, serverless) {
|
|
56
|
+
const testing = options.testing;
|
|
57
|
+
const kinds = [...testing?.runner !== void 0 ? ["capture"] : [], ...serverless ? [] : ["publish"]];
|
|
58
|
+
return {
|
|
59
|
+
jobs: createJobQueue(options, kinds, testing?.retries ?? DEFAULT_RETRIES),
|
|
60
|
+
notifyJobs: serverless ? null : createJobQueue(options, ["notify"], NOTIFY_MAX_ATTEMPTS - 1)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Every consumer this instance runs; none on a host that polls nothing. */
|
|
64
|
+
function consumersOf(options, runtime, jobs, wiring) {
|
|
65
|
+
const { capture, publishing, buildState, notifyJobs } = wiring;
|
|
66
|
+
return [...jobs.kinds.length > 0 ? [databaseConsumer(options, runtime, jobs, {
|
|
67
|
+
capture,
|
|
68
|
+
publishing
|
|
69
|
+
})] : [], ...buildState.handler !== null && notifyJobs !== null ? [notifyConsumer(options, runtime, notifyJobs, buildState.handler)] : []];
|
|
70
|
+
}
|
|
71
|
+
/** The consumer of the database queue: capture and publish jobs. */
|
|
72
|
+
function databaseConsumer(options, runtime, jobs, wiring) {
|
|
73
|
+
const { capture, publishing } = wiring;
|
|
74
|
+
const capturesHere = jobs.kinds.includes("capture");
|
|
75
|
+
return lazyConsumer(() => ({
|
|
76
|
+
queue: jobs.get(),
|
|
77
|
+
handlers: {
|
|
78
|
+
...capturesHere && capture.handler ? { capture: capture.handler } : {},
|
|
79
|
+
...publishing.handler ? { publish: publishing.handler } : {}
|
|
80
|
+
},
|
|
81
|
+
config: {
|
|
82
|
+
concurrency: capturesHere ? options.testing?.concurrency : 1,
|
|
83
|
+
maxRetries: options.testing?.retries ?? DEFAULT_RETRIES
|
|
84
|
+
},
|
|
85
|
+
scratchDir: runtime.config.scratchDir ?? DEFAULT_SCRATCH_DIR,
|
|
86
|
+
metrics: jobMetrics(),
|
|
87
|
+
logger: runtime.logger.child({ component: "job-consumer" }),
|
|
88
|
+
...options.testing?.queueClock === void 0 ? {} : {
|
|
89
|
+
now: options.testing.queueClock.now,
|
|
90
|
+
timers: options.testing.queueClock.timers
|
|
91
|
+
}
|
|
92
|
+
}), [async () => {
|
|
93
|
+
if (capturesHere) await capture.handler?.close?.();
|
|
94
|
+
await jobs.built()?.lifecycle.teardown();
|
|
95
|
+
}]);
|
|
96
|
+
}
|
|
97
|
+
/** Deliveries running at once: short HTTP calls to a git host, bounded to stay clear of its rate limits. */
|
|
98
|
+
const NOTIFY_CONCURRENCY = 4;
|
|
99
|
+
/** The consumer of `notify` jobs, on its own queue so no capture or publish job can starve it. */
|
|
100
|
+
function notifyConsumer(options, runtime, queue, handler) {
|
|
101
|
+
return lazyConsumer(() => ({
|
|
102
|
+
queue: queue.get(),
|
|
103
|
+
handlers: { notify: handler },
|
|
104
|
+
config: { concurrency: NOTIFY_CONCURRENCY },
|
|
105
|
+
metrics: jobMetrics(),
|
|
106
|
+
logger: runtime.logger.child({ component: "notify-consumer" }),
|
|
107
|
+
...options.testing?.queueClock === void 0 ? {} : {
|
|
108
|
+
now: options.testing.queueClock.now,
|
|
109
|
+
timers: options.testing.queueClock.timers
|
|
110
|
+
}
|
|
111
|
+
}), [async () => {
|
|
112
|
+
await queue.built()?.lifecycle.teardown();
|
|
113
|
+
}]);
|
|
114
|
+
}
|
|
115
|
+
/** Tells the project's reviewers a published build waits for a verdict (webhook and notifications). */
|
|
116
|
+
function reviewRequestedFor(options, runtime) {
|
|
117
|
+
return async (project, build) => {
|
|
118
|
+
await announceReviewWith({
|
|
119
|
+
db: options.database,
|
|
120
|
+
config: runtime.config,
|
|
121
|
+
ui: runtime.ui,
|
|
122
|
+
logger: runtime.logger,
|
|
123
|
+
notifiers: runtime.notifiers,
|
|
124
|
+
notificationsConfig: options.notifications?.config,
|
|
125
|
+
publicOrigin: runtime.publicOrigin
|
|
126
|
+
}, project, REVIEW_EVENTS.requested, reviewRequestedData(build), build.id);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export { setupJobs };
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=jobs-setup.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs-setup.mjs","names":["isServerlessHost"],"sources":["../src/jobs-setup.ts"],"sourcesContent":["/**\n * Background jobs of the app (ADR 0006): the one database queue, the handlers of its job kinds\n * (`capture` with `testing`, `publish` on a long-running host, `notify` on a queue of its own) and the one consumer that runs\n * them.\n */\nimport {\n DEFAULT_RETRIES,\n NOTIFY_MAX_ATTEMPTS,\n type BuildStatePublisher,\n type CaptureConsumer,\n type KindHandler,\n} from \"@storyshelf/core/capture\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport { DEFAULT_SCRATCH_DIR } from \"@storyshelf/core/config\";\nimport type { Build, CaptureJobKind, Project } from \"@storyshelf/core/schema\";\nimport { setupBuildState, type BuildStateWiring } from \"./build-state-setup.ts\";\nimport { setupCapture, type CaptureWiring } from \"./capture-setup.ts\";\nimport { combineConsumers, jobMetrics, lazyConsumer } from \"./consumer-setup.ts\";\nimport { createJobCanceller, type JobCanceller } from \"./job-canceller.ts\";\nimport { createJobQueue, type JobQueue } from \"./job-queue.ts\";\nimport { setupPublishing, type PublishService, type PublishWiring } from \"./publish-setup.ts\";\nimport { announceReviewWith, REVIEW_EVENTS, reviewRequestedData } from \"./review-events.ts\";\nimport type { ServerRuntime } from \"./runtime.ts\";\nimport { isServerlessHost } from \"./serverless.ts\";\n\n/** What the rest of the app takes from the job wiring. */\nexport interface JobsWiring {\n /** Where captures queue; null without `testing`. */\n captureQueue: CaptureWiring[\"queue\"];\n enqueueCapture: CaptureWiring[\"enqueueCapture\"];\n /** Whether this instance runs the capture jobs (the retry and compute-jobs UI applies). */\n capturesHere: boolean;\n publish: PublishService;\n /** Asks for a build's current state to be shown on its git channels (ADR 0014). */\n buildState: BuildStatePublisher;\n /** Cancels every unfinished job of a build, of any kind (capture, publish, notify). */\n cancelBuildJobs: JobCanceller;\n /** Runs every handler this instance has; null when it consumes nothing. */\n consumer: CaptureConsumer | null;\n}\n\n/** Wire the queue, the handlers and the consumer for `options`. */\nexport function setupJobs(\n options: ShelfOptions,\n runtime: ServerRuntime,\n env: NodeJS.ProcessEnv = process.env,\n): JobsWiring {\n const { jobs, notifyJobs } = queuesFor(options, isServerlessHost(env));\n const buildState = setupBuildState(options, runtime, notifyJobs);\n const capture = setupCapture(\n options,\n runtime.config,\n {\n notifiers: runtime.notifiers,\n buildState: buildState.publisher,\n publicOrigin: async () => await runtime.publicOrigin.url(),\n },\n runtime.logger,\n jobs,\n );\n const publishing = setupPublishing(\n options,\n runtime.config,\n runtime.logger,\n capture.enqueueCapture !== undefined,\n {\n jobs,\n buildState: buildState.publisher,\n reviewRequested: reviewRequestedFor(options, runtime),\n },\n );\n const wiring = { capture, publishing, buildState, notifyJobs };\n return {\n captureQueue: capture.queue,\n enqueueCapture: capture.enqueueCapture,\n capturesHere: capture.handler !== null,\n publish: publishing,\n buildState: buildState.publisher,\n cancelBuildJobs: createJobCanceller([jobs.view]),\n consumer: combineConsumers(consumersOf(options, runtime, jobs, wiring)),\n };\n}\n\n/**\n * The database queue (captures with `testing`, publishing on a long-running host) and,\n * on a long-running host, a queue of its own for `notify` jobs: a status must not wait behind a\n * capture that holds every slot for minutes.\n */\nfunction queuesFor(\n options: ShelfOptions,\n serverless: boolean,\n): { jobs: JobQueue; notifyJobs: JobQueue | null } {\n const testing = options.testing;\n const databaseCaptures = testing?.runner !== undefined;\n const kinds: CaptureJobKind[] = [\n ...(databaseCaptures ? ([\"capture\"] as const) : []),\n ...(serverless ? [] : ([\"publish\"] as const)),\n ];\n return {\n jobs: createJobQueue(options, kinds, testing?.retries ?? DEFAULT_RETRIES),\n notifyJobs: serverless ? null : createJobQueue(options, [\"notify\"], NOTIFY_MAX_ATTEMPTS - 1),\n };\n}\n\n/** Every consumer this instance runs; none on a host that polls nothing. */\nfunction consumersOf(\n options: ShelfOptions,\n runtime: ServerRuntime,\n jobs: JobQueue,\n wiring: {\n capture: CaptureWiring;\n publishing: Pick<PublishWiring, \"handler\">;\n buildState: Pick<BuildStateWiring, \"handler\">;\n notifyJobs: JobQueue | null;\n },\n): CaptureConsumer[] {\n const { capture, publishing, buildState, notifyJobs } = wiring;\n return [\n ...(jobs.kinds.length > 0\n ? [databaseConsumer(options, runtime, jobs, { capture, publishing })]\n : []),\n ...(buildState.handler !== null && notifyJobs !== null\n ? [notifyConsumer(options, runtime, notifyJobs, buildState.handler)]\n : []),\n ];\n}\n\n/** The consumer of the database queue: capture and publish jobs. */\nfunction databaseConsumer(\n options: ShelfOptions,\n runtime: ServerRuntime,\n jobs: JobQueue,\n wiring: {\n capture: CaptureWiring;\n publishing: Pick<PublishWiring, \"handler\">;\n },\n): CaptureConsumer {\n const { capture, publishing } = wiring;\n const capturesHere = jobs.kinds.includes(\"capture\");\n return lazyConsumer(\n () => ({\n queue: jobs.get(),\n handlers: {\n ...(capturesHere && capture.handler ? { capture: capture.handler } : {}),\n ...(publishing.handler ? { publish: publishing.handler } : {}),\n },\n // Publishing alone is one job at a time, so a big zip never crowds out the web server.\n config: {\n concurrency: capturesHere ? options.testing?.concurrency : 1,\n maxRetries: options.testing?.retries ?? DEFAULT_RETRIES,\n },\n scratchDir: runtime.config.scratchDir ?? DEFAULT_SCRATCH_DIR,\n metrics: jobMetrics(),\n logger: runtime.logger.child({ component: \"job-consumer\" }),\n ...(options.testing?.queueClock === undefined\n ? {}\n : { now: options.testing.queueClock.now, timers: options.testing.queueClock.timers }),\n }),\n [\n async () => {\n if (capturesHere) {\n await capture.handler?.close?.();\n }\n await jobs.built()?.lifecycle.teardown();\n },\n ],\n );\n}\n\n/** Deliveries running at once: short HTTP calls to a git host, bounded to stay clear of its rate limits. */\nconst NOTIFY_CONCURRENCY = 4;\n\n/** The consumer of `notify` jobs, on its own queue so no capture or publish job can starve it. */\nfunction notifyConsumer(\n options: ShelfOptions,\n runtime: ServerRuntime,\n queue: JobQueue,\n handler: KindHandler,\n): CaptureConsumer {\n return lazyConsumer(\n () => ({\n queue: queue.get(),\n handlers: { notify: handler },\n config: { concurrency: NOTIFY_CONCURRENCY },\n metrics: jobMetrics(),\n logger: runtime.logger.child({ component: \"notify-consumer\" }),\n ...(options.testing?.queueClock === undefined\n ? {}\n : { now: options.testing.queueClock.now, timers: options.testing.queueClock.timers }),\n }),\n [\n async () => {\n await queue.built()?.lifecycle.teardown();\n },\n ],\n );\n}\n\n/** Tells the project's reviewers a published build waits for a verdict (webhook and notifications). */\nfunction reviewRequestedFor(\n options: ShelfOptions,\n runtime: ServerRuntime,\n): (project: Project, build: Build) => Promise<void> {\n return async (project, build) => {\n await announceReviewWith(\n {\n db: options.database,\n config: runtime.config,\n ui: runtime.ui,\n logger: runtime.logger,\n notifiers: runtime.notifiers,\n notificationsConfig: options.notifications?.config,\n publicOrigin: runtime.publicOrigin,\n },\n project,\n REVIEW_EVENTS.requested,\n reviewRequestedData(build),\n build.id,\n );\n };\n}\n"],"mappings":"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"}
|
package/dist/lifecycle.mjs
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { settleInsightJobs } from "./insights/job.mjs";
|
|
4
5
|
import { startBranchGcTimer } from "./retention-timer.mjs";
|
|
5
|
-
import { AdapterLifecycleError, bindAdapterLoggers, collectSetups, collectTeardowns, runAdapterSetups, runAdapterTeardowns, validateBootAssembly } from "@storyshelf/core/adapter/setup";
|
|
6
|
+
import { AdapterLifecycleError, adapterSourcesOf, bindAdapterLoggers, collectSetups, collectTeardowns, runAdapterSetups, runAdapterTeardowns, validateBootAssembly } from "@storyshelf/core/adapter/setup";
|
|
7
|
+
import { shutdownDiffPool, verifyImageEngine } from "@storyshelf/core/diff";
|
|
6
8
|
import { sanitizeErrorText } from "@storyshelf/core/utils";
|
|
7
|
-
|
|
9
|
+
import { purgeAiData } from "@storyshelf/core/retention";
|
|
10
|
+
// src/lifecycle.ts
|
|
8
11
|
/** Longest teardown waits for background boot tasks before closing adapters anyway. */
|
|
9
12
|
const BACKGROUND_DRAIN_MS = 5e3;
|
|
10
13
|
/** Register a boot task so `teardown()` does not close adapters underneath it. */
|
|
11
14
|
function trackBackground(cell, task) {
|
|
12
15
|
(cell.background ??= []).push(task);
|
|
13
16
|
}
|
|
17
|
+
/** Start the job consumer once adapters are set up; a no-op when none exists or it runs. */
|
|
18
|
+
function startConsumer(cell, logger) {
|
|
19
|
+
const consumer = cell.consumer;
|
|
20
|
+
if (!consumer || consumer.isRunning() || cell.consumerStopped) return;
|
|
21
|
+
cell.consumerRun = consumer.start().catch((error) => {
|
|
22
|
+
logger.error({ err: error }, "job consumer stopped");
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** Stop polling and wait for the jobs in flight to finish (the SIGTERM drain). */
|
|
26
|
+
async function stopConsumer(cell) {
|
|
27
|
+
cell.consumerStopped = true;
|
|
28
|
+
await cell.consumer?.stop();
|
|
29
|
+
await cell.consumerRun;
|
|
30
|
+
}
|
|
14
31
|
/** Wait for tracked boot tasks, bounded so a hung task can never block shutdown. */
|
|
15
32
|
async function drainBackground(cell) {
|
|
16
33
|
const pending = Promise.allSettled(cell.background ?? []);
|
|
@@ -32,23 +49,33 @@ function trackSettlement(cell, promise) {
|
|
|
32
49
|
};
|
|
33
50
|
});
|
|
34
51
|
}
|
|
52
|
+
/** First failing boot check (assembly, auth, AI), or null when all pass. */
|
|
53
|
+
async function firstBootFailure(options, logger) {
|
|
54
|
+
const invalid = validateBootAssembly(adapterSourcesOf(options));
|
|
55
|
+
if (invalid.length > 0) {
|
|
56
|
+
logger.error({ failures: invalid }, "adapter validation failed");
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
failures: invalid
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
for (const [label, run] of [
|
|
63
|
+
["auth", runAuthSetup],
|
|
64
|
+
["ai", runAiSetup],
|
|
65
|
+
["images", runImagesSetup]
|
|
66
|
+
]) {
|
|
67
|
+
const failed = await run(options);
|
|
68
|
+
if (failed) {
|
|
69
|
+
logger.error({ failures: failed.failures }, `${label} setup failed`);
|
|
70
|
+
return failed;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
35
75
|
/** Kick the eager background setup run (resolves; never rejects). */
|
|
36
76
|
function kickSetup(options, ctx, logger, cell) {
|
|
37
77
|
cell.ready = (async () => {
|
|
38
|
-
|
|
39
|
-
if (invalid.length > 0) {
|
|
40
|
-
logger.error({ failures: invalid }, "adapter validation failed");
|
|
41
|
-
return {
|
|
42
|
-
ok: false,
|
|
43
|
-
failures: invalid
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
const authFailed = await runAuthSetup(options);
|
|
47
|
-
if (authFailed) {
|
|
48
|
-
logger.error({ failures: authFailed.failures }, "auth setup failed");
|
|
49
|
-
return authFailed;
|
|
50
|
-
}
|
|
51
|
-
return await runAdapterSetups(collectSetups(options), ctx, logger);
|
|
78
|
+
return await firstBootFailure(options, logger) ?? await runAdapterSetups(collectSetups(adapterSourcesOf(options)), ctx, logger);
|
|
52
79
|
})();
|
|
53
80
|
trackSettlement(cell, cell.ready);
|
|
54
81
|
}
|
|
@@ -80,13 +107,14 @@ function attachLifecycle(app, options, runtime, cell) {
|
|
|
80
107
|
logger: runtime.logger
|
|
81
108
|
};
|
|
82
109
|
bindAdapterLoggers(options, runtime.logger);
|
|
110
|
+
options.ai?.setLogger?.(runtime.logger.child({ component: "ai" }));
|
|
83
111
|
logAuthMode(options, runtime.logger);
|
|
84
112
|
kickSetup(options, setupCtx, runtime.logger, cell);
|
|
85
113
|
const timer = startBranchGcInterval(options, runtime);
|
|
86
114
|
Object.assign(app, { lifecycle: createLifecycle(options, setupCtx, runtime.logger, cell, timer) });
|
|
87
115
|
}
|
|
88
116
|
function startBranchGcInterval(options, runtime) {
|
|
89
|
-
return startBranchGcTimer(options.database, options.storage, runtime.config, runtime.logger);
|
|
117
|
+
return startBranchGcTimer(options.database, options.storage, runtime.config, runtime.logger, Boolean(options.testing));
|
|
90
118
|
}
|
|
91
119
|
/** Run auth boot validation, returning a failure result instead of throwing. */
|
|
92
120
|
async function runAuthSetup(options) {
|
|
@@ -106,9 +134,47 @@ async function runAuthSetup(options) {
|
|
|
106
134
|
};
|
|
107
135
|
}
|
|
108
136
|
}
|
|
137
|
+
/** Run AI boot checks and the stale-run sweep; failure fails readiness like auth. */
|
|
138
|
+
async function runAiSetup(options) {
|
|
139
|
+
if (!options.ai) return null;
|
|
140
|
+
try {
|
|
141
|
+
await options.ai.setup();
|
|
142
|
+
await purgeAiData(options.database);
|
|
143
|
+
return null;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
failures: [{
|
|
149
|
+
category: "ai",
|
|
150
|
+
kind: "ai",
|
|
151
|
+
name: "ai",
|
|
152
|
+
error: sanitizeErrorText(message, 500)
|
|
153
|
+
}]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Load the configured image engine now, so a missing `sharp` fails readiness instead of the first diff. */
|
|
158
|
+
async function runImagesSetup() {
|
|
159
|
+
try {
|
|
160
|
+
await verifyImageEngine();
|
|
161
|
+
return null;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
failures: [{
|
|
167
|
+
category: "images",
|
|
168
|
+
kind: "images",
|
|
169
|
+
name: "images",
|
|
170
|
+
error: sanitizeErrorText(message, 500)
|
|
171
|
+
}]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
109
175
|
/** Validation failure settled on the cell — null when the assembly is sound. */
|
|
110
176
|
function invalidSources(options, cell) {
|
|
111
|
-
const invalid = validateBootAssembly(options);
|
|
177
|
+
const invalid = validateBootAssembly(adapterSourcesOf(options));
|
|
112
178
|
if (invalid.length === 0) return null;
|
|
113
179
|
const result = {
|
|
114
180
|
ok: false,
|
|
@@ -128,15 +194,21 @@ function createLifecycle(options, ctx, logger, cell, timer = null) {
|
|
|
128
194
|
setup: async () => {
|
|
129
195
|
const blocked = invalidSources(options, cell);
|
|
130
196
|
if (blocked) throw new AdapterLifecycleError("setup", blocked.failures);
|
|
131
|
-
const result = await runAdapterSetups(collectSetups(options), ctx, logger);
|
|
197
|
+
const result = await runAdapterSetups(collectSetups(adapterSourcesOf(options)), ctx, logger);
|
|
132
198
|
cell.ready = Promise.resolve(result);
|
|
133
199
|
cell.settled = result;
|
|
134
200
|
if (!result.ok) throw new AdapterLifecycleError("setup", result.failures);
|
|
201
|
+
cell.consumerStopped = false;
|
|
202
|
+
startConsumer(cell, logger);
|
|
135
203
|
},
|
|
136
204
|
teardown: async () => {
|
|
137
205
|
timer?.stop();
|
|
206
|
+
await stopConsumer(cell);
|
|
138
207
|
await drainBackground(cell);
|
|
139
|
-
|
|
208
|
+
await settleInsightJobs();
|
|
209
|
+
await options.ai?.teardown();
|
|
210
|
+
await shutdownDiffPool();
|
|
211
|
+
const result = await runAdapterTeardowns(collectTeardowns(adapterSourcesOf(options)), ctx, logger);
|
|
140
212
|
await options.observability?.shutdown().catch((error) => {
|
|
141
213
|
logger.error({ err: error }, "observability shutdown failed");
|
|
142
214
|
});
|
|
@@ -144,7 +216,6 @@ function createLifecycle(options, ctx, logger, cell, timer = null) {
|
|
|
144
216
|
}
|
|
145
217
|
};
|
|
146
218
|
}
|
|
147
|
-
|
|
148
|
-
export { attachLifecycle, trackBackground };
|
|
219
|
+
export { attachLifecycle, startConsumer, trackBackground };
|
|
149
220
|
|
|
150
221
|
//# sourceMappingURL=lifecycle.mjs.map
|
package/dist/lifecycle.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.mjs","names":[],"sources":["../src/lifecycle.ts"],"sourcesContent":["import type { AdapterSetupContext } from \"@storyshelf/core/adapter/metadata\";\nimport {\n AdapterLifecycleError,\n bindAdapterLoggers,\n collectSetups,\n collectTeardowns,\n runAdapterSetups,\n runAdapterTeardowns,\n validateBootAssembly,\n} from \"@storyshelf/core/adapter/setup\";\nimport type { AdapterSetupResult } from \"@storyshelf/core/adapter/setup\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { sanitizeErrorText } from \"@storyshelf/core/utils\";\nimport type { ShelfRouter, ShelfLifecycle } from \"./app-types.ts\";\nimport { startBranchGcTimer } from \"./retention-timer.ts\";\nimport type { ServerRuntime } from \"./runtime.ts\";\n\n/** Mutable setup-settlement cell shared by the gate, health, and `setup()`. */\nexport interface LifecycleCell {\n ready: Promise<AdapterSetupResult>;\n settled: AdapterSetupResult | null;\n /** Fire-and-forget boot tasks (recovery, credential check) that teardown must outwait. */\n background?: Promise<unknown>[];\n /** Override of {@link BACKGROUND_DRAIN_MS} (tests). */\n drainMs?: number;\n}\n\n/** Longest teardown waits for background boot tasks before closing adapters anyway. */\nconst BACKGROUND_DRAIN_MS = 5000;\n\n/** Register a boot task so `teardown()` does not close adapters underneath it. */\nexport function trackBackground(cell: LifecycleCell, task: Promise<unknown>): void {\n (cell.background ??= []).push(task);\n}\n\n/** Wait for tracked boot tasks, bounded so a hung task can never block shutdown. */\nasync function drainBackground(cell: LifecycleCell): Promise<void> {\n const pending = Promise.allSettled(cell.background ?? []);\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, cell.drainMs ?? BACKGROUND_DRAIN_MS);\n timer.unref();\n });\n await Promise.race([pending, timeout]);\n clearTimeout(timer);\n}\n\nfunction trackSettlement(cell: LifecycleCell, promise: Promise<AdapterSetupResult>): void {\n promise.then(\n (result) => {\n cell.settled = result;\n },\n () => {\n cell.settled = { ok: false, failures: [] };\n },\n );\n}\n\n/** Kick the eager background setup run (resolves; never rejects). */\nfunction kickSetup(\n options: ShelfOptions,\n ctx: AdapterSetupContext,\n logger: Logger,\n cell: LifecycleCell,\n): void {\n // Auth is not an adapter: its one-shot boot validation runs first, then\n // the adapter setups. A failing secret check fails readiness, like others.\n cell.ready = (async (): Promise<AdapterSetupResult> => {\n const invalid = validateBootAssembly(options);\n if (invalid.length > 0) {\n logger.error({ failures: invalid }, \"adapter validation failed\");\n return { ok: false, failures: invalid };\n }\n const authFailed = await runAuthSetup(options);\n if (authFailed) {\n logger.error({ failures: authFailed.failures }, \"auth setup failed\");\n return authFailed;\n }\n return await runAdapterSetups(collectSetups(options), ctx, logger);\n })();\n trackSettlement(cell, cell.ready);\n}\n\n/** Login method count, or null when the engine throws during introspection. */\nfunction loginMethodCount(auth: NonNullable<ShelfOptions[\"auth\"]>): number | null {\n try {\n return auth.loginMethods().length;\n } catch {\n return null;\n }\n}\n\n/** Log which auth mode the server boots with (open servers stay visible). */\nfunction logAuthMode(options: ShelfOptions, logger: Logger): void {\n if (!options.auth) {\n logger.info(\"auth disabled (local default; every route is open)\");\n return;\n }\n const methods = loginMethodCount(options.auth);\n if (methods === null) {\n logger.warn(\"auth loginMethods() threw during boot logging\");\n return;\n }\n logger.info({ loginMethods: methods }, \"auth enabled\");\n}\n\n/** Kick eager setup and attach the `app.lifecycle` namespace (single run). */\nexport function attachLifecycle(\n app: ShelfRouter,\n options: ShelfOptions,\n runtime: ServerRuntime,\n cell: LifecycleCell,\n): void {\n const setupCtx: AdapterSetupContext = { config: runtime.config, logger: runtime.logger };\n bindAdapterLoggers(options, runtime.logger);\n logAuthMode(options, runtime.logger);\n kickSetup(options, setupCtx, runtime.logger, cell);\n const timer = startBranchGcInterval(options, runtime);\n Object.assign(app, {\n lifecycle: createLifecycle(options, setupCtx, runtime.logger, cell, timer),\n });\n}\n\nfunction startBranchGcInterval(\n options: ShelfOptions,\n runtime: ServerRuntime,\n): { stop(): void } | null {\n return startBranchGcTimer(options.database, options.storage, runtime.config, runtime.logger);\n}\n\n/** Run auth boot validation, returning a failure result instead of throwing. */\nasync function runAuthSetup(options: ShelfOptions): Promise<AdapterSetupResult | null> {\n try {\n await options.auth?.setup?.();\n return null;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n ok: false,\n failures: [\n { category: \"auth\", kind: \"auth\", name: \"auth\", error: sanitizeErrorText(message, 500) },\n ],\n };\n }\n}\n/** Validation failure settled on the cell — null when the assembly is sound. */\nfunction invalidSources(options: ShelfOptions, cell: LifecycleCell): AdapterSetupResult | null {\n const invalid = validateBootAssembly(options);\n if (invalid.length === 0) {\n return null;\n }\n const result: AdapterSetupResult = { ok: false, failures: invalid };\n cell.ready = Promise.resolve(result);\n cell.settled = result;\n return result;\n}\n/** Build the `app.lifecycle` namespace closing over one settlement cell. */\nfunction createLifecycle(\n options: ShelfOptions,\n ctx: AdapterSetupContext,\n logger: Logger,\n cell: LifecycleCell,\n timer: { stop(): void } | null = null,\n): ShelfLifecycle {\n return {\n get ready() {\n return cell.ready;\n },\n logger,\n setup: async () => {\n const blocked = invalidSources(options, cell);\n if (blocked) {\n throw new AdapterLifecycleError(\"setup\", blocked.failures);\n }\n const result = await runAdapterSetups(collectSetups(options), ctx, logger);\n cell.ready = Promise.resolve(result);\n cell.settled = result;\n if (!result.ok) {\n throw new AdapterLifecycleError(\"setup\", result.failures);\n }\n },\n teardown: async () => {\n timer?.stop();\n await drainBackground(cell);\n const result = await runAdapterTeardowns(collectTeardowns(options), ctx, logger);\n await options.observability?.shutdown().catch((error: unknown) => {\n logger.error({ err: error }, \"observability shutdown failed\");\n });\n if (!result.ok) {\n throw new AdapterLifecycleError(\"teardown\", result.failures);\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecycle.mjs","names":[],"sources":["../src/lifecycle.ts"],"sourcesContent":["import type { AdapterSetupContext } from \"@storyshelf/core/adapter/metadata\";\nimport {\n AdapterLifecycleError,\n bindAdapterLoggers,\n adapterSourcesOf,\n collectSetups,\n collectTeardowns,\n runAdapterSetups,\n runAdapterTeardowns,\n validateBootAssembly,\n} from \"@storyshelf/core/adapter/setup\";\nimport type { AdapterSetupResult } from \"@storyshelf/core/adapter/setup\";\nimport type { CaptureConsumer } from \"@storyshelf/core/capture\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport { shutdownDiffPool, verifyImageEngine } from \"@storyshelf/core/diff\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { purgeAiData } from \"@storyshelf/core/retention\";\nimport { sanitizeErrorText } from \"@storyshelf/core/utils\";\nimport type { ShelfRouter, ShelfLifecycle } from \"./app-types.ts\";\nimport { settleInsightJobs } from \"./insights/job.ts\";\nimport { startBranchGcTimer } from \"./retention-timer.ts\";\nimport type { ServerRuntime } from \"./runtime.ts\";\n\n/** Mutable setup-settlement cell shared by the gate, health, and `setup()`. */\nexport interface LifecycleCell {\n ready: Promise<AdapterSetupResult>;\n settled: AdapterSetupResult | null;\n /** Fire-and-forget boot tasks (recovery, credential check) that teardown must outwait. */\n background?: Promise<unknown>[];\n /** Override of {@link BACKGROUND_DRAIN_MS} (tests). */\n drainMs?: number;\n /** The job consumer (null on hosts that run none); started once adapters are ready, drained by `teardown()`. */\n consumer?: CaptureConsumer | null;\n /** The running consumer session, settled once it was stopped and drained. */\n consumerRun?: Promise<void>;\n /** Teardown ran: the consumer stays stopped (a late \"adapters are ready\" must not restart it). */\n consumerStopped?: boolean;\n}\n\n/** Longest teardown waits for background boot tasks before closing adapters anyway. */\nconst BACKGROUND_DRAIN_MS = 5000;\n\n/** Register a boot task so `teardown()` does not close adapters underneath it. */\nexport function trackBackground(cell: LifecycleCell, task: Promise<unknown>): void {\n (cell.background ??= []).push(task);\n}\n\n/** Start the job consumer once adapters are set up; a no-op when none exists or it runs. */\nexport function startConsumer(cell: LifecycleCell, logger: Logger): void {\n const consumer = cell.consumer;\n if (!consumer || consumer.isRunning() || cell.consumerStopped) {\n return;\n }\n cell.consumerRun = consumer.start().catch((error: unknown) => {\n logger.error({ err: error }, \"job consumer stopped\");\n });\n}\n\n/** Stop polling and wait for the jobs in flight to finish (the SIGTERM drain). */\nasync function stopConsumer(cell: LifecycleCell): Promise<void> {\n cell.consumerStopped = true;\n await cell.consumer?.stop();\n await cell.consumerRun;\n}\n\n/** Wait for tracked boot tasks, bounded so a hung task can never block shutdown. */\nasync function drainBackground(cell: LifecycleCell): Promise<void> {\n const pending = Promise.allSettled(cell.background ?? []);\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, cell.drainMs ?? BACKGROUND_DRAIN_MS);\n timer.unref();\n });\n await Promise.race([pending, timeout]);\n clearTimeout(timer);\n}\n\nfunction trackSettlement(cell: LifecycleCell, promise: Promise<AdapterSetupResult>): void {\n promise.then(\n (result) => {\n cell.settled = result;\n },\n () => {\n cell.settled = { ok: false, failures: [] };\n },\n );\n}\n\n/** First failing boot check (assembly, auth, AI), or null when all pass. */\nasync function firstBootFailure(\n options: ShelfOptions,\n logger: Logger,\n): Promise<AdapterSetupResult | null> {\n const invalid = validateBootAssembly(adapterSourcesOf(options));\n if (invalid.length > 0) {\n logger.error({ failures: invalid }, \"adapter validation failed\");\n return { ok: false, failures: invalid };\n }\n for (const [label, run] of [\n [\"auth\", runAuthSetup],\n [\"ai\", runAiSetup],\n [\"images\", runImagesSetup],\n ] as const) {\n // eslint-disable-next-line no-await-in-loop -- boot checks run in order and stop at the first failure\n const failed = await run(options);\n if (failed) {\n logger.error({ failures: failed.failures }, `${label} setup failed`);\n return failed;\n }\n }\n return null;\n}\n\n/** Kick the eager background setup run (resolves; never rejects). */\nfunction kickSetup(\n options: ShelfOptions,\n ctx: AdapterSetupContext,\n logger: Logger,\n cell: LifecycleCell,\n): void {\n // Auth is not an adapter: its one-shot boot validation runs first, then\n // the adapter setups. A failing secret check fails readiness, like others.\n cell.ready = (async (): Promise<AdapterSetupResult> => {\n const failed = await firstBootFailure(options, logger);\n return (\n failed ?? (await runAdapterSetups(collectSetups(adapterSourcesOf(options)), ctx, logger))\n );\n })();\n trackSettlement(cell, cell.ready);\n}\n\n/** Login method count, or null when the engine throws during introspection. */\nfunction loginMethodCount(auth: NonNullable<ShelfOptions[\"auth\"]>): number | null {\n try {\n return auth.loginMethods().length;\n } catch {\n return null;\n }\n}\n\n/** Log which auth mode the server boots with (open servers stay visible). */\nfunction logAuthMode(options: ShelfOptions, logger: Logger): void {\n if (!options.auth) {\n logger.info(\"auth disabled (local default; every route is open)\");\n return;\n }\n const methods = loginMethodCount(options.auth);\n if (methods === null) {\n logger.warn(\"auth loginMethods() threw during boot logging\");\n return;\n }\n logger.info({ loginMethods: methods }, \"auth enabled\");\n}\n\n/** Kick eager setup and attach the `app.lifecycle` namespace (single run). */\nexport function attachLifecycle(\n app: ShelfRouter,\n options: ShelfOptions,\n runtime: ServerRuntime,\n cell: LifecycleCell,\n): void {\n const setupCtx: AdapterSetupContext = { config: runtime.config, logger: runtime.logger };\n bindAdapterLoggers(options, runtime.logger);\n options.ai?.setLogger?.(runtime.logger.child({ component: \"ai\" }));\n logAuthMode(options, runtime.logger);\n kickSetup(options, setupCtx, runtime.logger, cell);\n const timer = startBranchGcInterval(options, runtime);\n Object.assign(app, {\n lifecycle: createLifecycle(options, setupCtx, runtime.logger, cell, timer),\n });\n}\n\nfunction startBranchGcInterval(\n options: ShelfOptions,\n runtime: ServerRuntime,\n): { stop(): void } | null {\n return startBranchGcTimer(\n options.database,\n options.storage,\n runtime.config,\n runtime.logger,\n Boolean(options.testing),\n );\n}\n\n/** Run auth boot validation, returning a failure result instead of throwing. */\nasync function runAuthSetup(options: ShelfOptions): Promise<AdapterSetupResult | null> {\n try {\n await options.auth?.setup?.();\n return null;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n ok: false,\n failures: [\n { category: \"auth\", kind: \"auth\", name: \"auth\", error: sanitizeErrorText(message, 500) },\n ],\n };\n }\n}\n/** Run AI boot checks and the stale-run sweep; failure fails readiness like auth. */\nasync function runAiSetup(options: ShelfOptions): Promise<AdapterSetupResult | null> {\n if (!options.ai) {\n return null;\n }\n try {\n await options.ai.setup();\n await purgeAiData(options.database);\n return null;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n ok: false,\n failures: [\n { category: \"ai\", kind: \"ai\", name: \"ai\", error: sanitizeErrorText(message, 500) },\n ],\n };\n }\n}\n/** Load the configured image engine now, so a missing `sharp` fails readiness instead of the first diff. */\nasync function runImagesSetup(): Promise<AdapterSetupResult | null> {\n try {\n await verifyImageEngine();\n return null;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n ok: false,\n failures: [\n {\n category: \"images\",\n kind: \"images\",\n name: \"images\",\n error: sanitizeErrorText(message, 500),\n },\n ],\n };\n }\n}\n/** Validation failure settled on the cell — null when the assembly is sound. */\nfunction invalidSources(options: ShelfOptions, cell: LifecycleCell): AdapterSetupResult | null {\n const invalid = validateBootAssembly(adapterSourcesOf(options));\n if (invalid.length === 0) {\n return null;\n }\n const result: AdapterSetupResult = { ok: false, failures: invalid };\n cell.ready = Promise.resolve(result);\n cell.settled = result;\n return result;\n}\n/** Build the `app.lifecycle` namespace closing over one settlement cell. */\nfunction createLifecycle(\n options: ShelfOptions,\n ctx: AdapterSetupContext,\n logger: Logger,\n cell: LifecycleCell,\n timer: { stop(): void } | null = null,\n): ShelfLifecycle {\n return {\n get ready() {\n return cell.ready;\n },\n logger,\n setup: async () => {\n const blocked = invalidSources(options, cell);\n if (blocked) {\n throw new AdapterLifecycleError(\"setup\", blocked.failures);\n }\n const result = await runAdapterSetups(collectSetups(adapterSourcesOf(options)), ctx, logger);\n cell.ready = Promise.resolve(result);\n cell.settled = result;\n if (!result.ok) {\n throw new AdapterLifecycleError(\"setup\", result.failures);\n }\n cell.consumerStopped = false;\n startConsumer(cell, logger);\n },\n teardown: async () => {\n timer?.stop();\n await stopConsumer(cell);\n await drainBackground(cell);\n await settleInsightJobs();\n await options.ai?.teardown();\n await shutdownDiffPool();\n const result = await runAdapterTeardowns(\n collectTeardowns(adapterSourcesOf(options)),\n ctx,\n logger,\n );\n await options.observability?.shutdown().catch((error: unknown) => {\n logger.error({ err: error }, \"observability shutdown failed\");\n });\n if (!result.ok) {\n throw new AdapterLifecycleError(\"teardown\", result.failures);\n }\n },\n };\n}\n"],"mappings":"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"}
|