@storyshelf/core 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 +49 -32
- package/dist/adapter/capture-queue.d.mts +44 -4
- package/dist/adapter/capture-runner.d.mts +134 -12
- package/dist/adapter/capture-runner.mjs +9 -12
- package/dist/adapter/database.d.mts +89 -10
- package/dist/adapter/database.mjs +4 -3
- package/dist/adapter/database.mjs.map +1 -1
- package/dist/adapter/design.d.mts +83 -0
- package/dist/adapter/design.mjs +27 -0
- package/dist/adapter/design.mjs.map +1 -0
- package/dist/adapter/email-sender.d.mts +0 -2
- package/dist/adapter/metadata.d.mts +3 -12
- package/dist/adapter/notifier.d.mts +6 -3
- package/dist/adapter/notifier.mjs +5 -1
- package/dist/adapter/oauth.d.mts +50 -0
- package/dist/adapter/setup.d.mts +4 -7
- package/dist/adapter/setup.mjs +3 -3
- package/dist/adapter/setup.mjs.map +1 -1
- package/dist/adapter/storage.d.mts +1 -3
- package/dist/adapter/webhook-events.d.mts +0 -2
- package/dist/adapter/webhook-events.mjs +13 -14
- package/dist/adapter/webhook-events.mjs.map +1 -1
- package/dist/adapters/capture-target-offers.d.mts +26 -0
- package/dist/adapters/capture-target-offers.mjs +57 -0
- package/dist/adapters/capture-target-offers.mjs.map +1 -0
- package/dist/adapters/capture-target.d.mts +69 -0
- package/dist/adapters/capture-target.mjs +75 -0
- package/dist/adapters/capture-target.mjs.map +1 -0
- package/dist/adapters/notifier/build-comment.d.mts +22 -0
- package/dist/adapters/notifier/build-comment.mjs +23 -0
- package/dist/adapters/notifier/build-comment.mjs.map +1 -0
- package/dist/adapters/notifier/build-state.d.mts +97 -0
- package/dist/adapters/notifier/build-state.mjs +217 -0
- package/dist/adapters/notifier/build-state.mjs.map +1 -0
- package/dist/adapters/notifier/events.d.mts +4 -4
- package/dist/adapters/notifier/events.mjs +32 -20
- package/dist/adapters/notifier/events.mjs.map +1 -1
- package/dist/adapters/notifier/format.d.mts +0 -2
- package/dist/adapters/notifier/format.mjs +1 -2
- package/dist/adapters/notifier/provider.d.mts +53 -9
- package/dist/adapters/notifier/provider.mjs +11 -0
- package/dist/adapters/notifier/provider.mjs.map +1 -0
- package/dist/adapters/notifier/state.d.mts +22 -0
- package/dist/adapters/notifier/state.mjs +65 -0
- package/dist/adapters/notifier/state.mjs.map +1 -0
- package/dist/adapters/notifier/types.d.mts +51 -4
- package/dist/adapters/remote-browser.d.mts +168 -0
- package/dist/adapters/remote-browser.mjs +201 -0
- package/dist/adapters/remote-browser.mjs.map +1 -0
- package/dist/adapters/remote-lease-attempt.mjs +39 -0
- package/dist/adapters/remote-lease-attempt.mjs.map +1 -0
- package/dist/adapters/remote-lease-error.d.mts +25 -0
- package/dist/adapters/remote-lease-error.mjs +54 -0
- package/dist/adapters/remote-lease-error.mjs.map +1 -0
- package/dist/adapters/remote-lease-held.d.mts +15 -0
- package/dist/adapters/remote-lease-held.mjs +86 -0
- package/dist/adapters/remote-lease-held.mjs.map +1 -0
- package/dist/adapters/remote-lease-shutdown.d.mts +12 -0
- package/dist/adapters/remote-lease-shutdown.mjs +32 -0
- package/dist/adapters/remote-lease-shutdown.mjs.map +1 -0
- package/dist/adapters/remote-lease.d.mts +39 -0
- package/dist/adapters/remote-lease.mjs +175 -0
- package/dist/adapters/remote-lease.mjs.map +1 -0
- package/dist/adapters/remote-limiter.mjs +121 -0
- package/dist/adapters/remote-limiter.mjs.map +1 -0
- package/dist/adapters/remote-pool.d.mts +56 -0
- package/dist/adapters/remote-pool.mjs +195 -0
- package/dist/adapters/remote-pool.mjs.map +1 -0
- package/dist/adapters/render-runs.d.mts +23 -0
- package/dist/adapters/render-runs.mjs +40 -0
- package/dist/adapters/render-runs.mjs.map +1 -0
- package/dist/adapters/setup-runner.d.mts +0 -2
- package/dist/adapters/setup-runner.mjs +3 -6
- package/dist/adapters/setup-runner.mjs.map +1 -1
- package/dist/adapters/sources.d.mts +10 -0
- package/dist/adapters/sources.mjs +19 -0
- package/dist/adapters/sources.mjs.map +1 -0
- package/dist/adapters/validate.d.mts +0 -2
- package/dist/adapters/validate.mjs +27 -20
- package/dist/adapters/validate.mjs.map +1 -1
- package/dist/ai.d.mts +103 -0
- package/dist/ai.mjs +18 -0
- package/dist/ai.mjs.map +1 -0
- package/dist/auth.d.mts +0 -2
- package/dist/auth.mjs +1 -2
- package/dist/brand-packs.d.mts +0 -2
- package/dist/brand-packs.mjs +0 -1
- package/dist/capture/a11y/axe-source.d.mts +16 -0
- package/dist/capture/a11y/axe-source.mjs +36 -0
- package/dist/capture/a11y/axe-source.mjs.map +1 -0
- package/dist/capture/a11y/in-page.d.mts +25 -0
- package/dist/capture/a11y/in-page.mjs +59 -0
- package/dist/capture/a11y/in-page.mjs.map +1 -0
- package/dist/capture/a11y/map.d.mts +22 -0
- package/dist/capture/a11y/map.mjs +47 -0
- package/dist/capture/a11y/map.mjs.map +1 -0
- package/dist/capture/a11y/options.d.mts +21 -0
- package/dist/capture/a11y/options.mjs +74 -0
- package/dist/capture/a11y/options.mjs.map +1 -0
- package/dist/capture/adapter.d.mts +46 -5
- package/dist/capture/adapter.mjs +1 -2
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/affected.d.mts +2 -2
- package/dist/capture/affected.mjs +14 -6
- package/dist/capture/affected.mjs.map +1 -1
- package/dist/capture/attempt-logger.mjs +83 -0
- package/dist/capture/attempt-logger.mjs.map +1 -0
- package/dist/capture/browsers.d.mts +33 -0
- package/dist/capture/browsers.mjs +66 -0
- package/dist/capture/browsers.mjs.map +1 -0
- package/dist/capture/build-state/deliver.d.mts +30 -0
- package/dist/capture/build-state/deliver.mjs +117 -0
- package/dist/capture/build-state/deliver.mjs.map +1 -0
- package/dist/capture/build-state/index.d.mts +8 -0
- package/dist/capture/build-state/notify-job.d.mts +54 -0
- package/dist/capture/build-state/notify-job.mjs +132 -0
- package/dist/capture/build-state/notify-job.mjs.map +1 -0
- package/dist/capture/build-state/publisher.d.mts +21 -0
- package/dist/capture/build-state/publisher.mjs +106 -0
- package/dist/capture/build-state/publisher.mjs.map +1 -0
- package/dist/capture/build-state/resume.d.mts +7 -0
- package/dist/capture/build-state/resume.mjs +46 -0
- package/dist/capture/build-state/resume.mjs.map +1 -0
- package/dist/capture/build-state/types.d.mts +47 -0
- package/dist/capture/catalog/catalogue.d.mts +20 -0
- package/dist/capture/catalog/catalogue.mjs +52 -0
- package/dist/capture/catalog/catalogue.mjs.map +1 -0
- package/dist/capture/catalog/components.d.mts +21 -0
- package/dist/capture/catalog/components.mjs +54 -0
- package/dist/capture/catalog/components.mjs.map +1 -0
- package/dist/capture/catalog/history.d.mts +28 -0
- package/dist/capture/catalog/history.mjs +50 -0
- package/dist/capture/catalog/history.mjs.map +1 -0
- package/dist/capture/catalog/hook.d.mts +12 -0
- package/dist/capture/catalog/hook.mjs +18 -0
- package/dist/capture/catalog/hook.mjs.map +1 -0
- package/dist/capture/catalog/index.d.mts +11 -0
- package/dist/capture/catalog/load.d.mts +24 -0
- package/dist/capture/catalog/load.mjs +48 -0
- package/dist/capture/catalog/load.mjs.map +1 -0
- package/dist/capture/catalog/parse.d.mts +13 -0
- package/dist/capture/catalog/parse.mjs +55 -0
- package/dist/capture/catalog/parse.mjs.map +1 -0
- package/dist/capture/catalog/read.d.mts +15 -0
- package/dist/capture/catalog/read.mjs +39 -0
- package/dist/capture/catalog/read.mjs.map +1 -0
- package/dist/capture/catalog/types.d.mts +31 -0
- package/dist/capture/comparison-reference.d.mts +27 -0
- package/dist/capture/comparison-reference.mjs +75 -0
- package/dist/capture/comparison-reference.mjs.map +1 -0
- package/dist/capture/consumer/capture-handler.d.mts +56 -0
- package/dist/capture/consumer/capture-handler.mjs +117 -0
- package/dist/capture/consumer/capture-handler.mjs.map +1 -0
- package/dist/capture/consumer/idle.d.mts +15 -0
- package/dist/capture/consumer/idle.mjs +24 -0
- package/dist/capture/consumer/idle.mjs.map +1 -0
- package/dist/capture/consumer/index.d.mts +11 -0
- package/dist/capture/consumer/index.mjs +138 -0
- package/dist/capture/consumer/index.mjs.map +1 -0
- package/dist/capture/consumer/loop.mjs +109 -0
- package/dist/capture/consumer/loop.mjs.map +1 -0
- package/dist/capture/consumer/metrics.d.mts +39 -0
- package/dist/capture/consumer/metrics.mjs +37 -0
- package/dist/capture/consumer/metrics.mjs.map +1 -0
- package/dist/capture/consumer/retry.d.mts +31 -0
- package/dist/capture/consumer/retry.mjs +33 -0
- package/dist/capture/consumer/retry.mjs.map +1 -0
- package/dist/capture/consumer/run-job.mjs +194 -0
- package/dist/capture/consumer/run-job.mjs.map +1 -0
- package/dist/capture/consumer/startup.mjs +37 -0
- package/dist/capture/consumer/startup.mjs.map +1 -0
- package/dist/capture/consumer/types.d.mts +95 -0
- package/dist/capture/crash-failure.d.mts +23 -0
- package/dist/capture/crash-failure.mjs +69 -0
- package/dist/capture/crash-failure.mjs.map +1 -0
- package/dist/capture/db-queue/cadence.d.mts +30 -0
- package/dist/capture/db-queue/cadence.mjs +37 -0
- package/dist/capture/db-queue/cadence.mjs.map +1 -0
- package/dist/capture/db-queue/claim.d.mts +7 -0
- package/dist/capture/db-queue/claim.mjs +148 -0
- package/dist/capture/db-queue/claim.mjs.map +1 -0
- package/dist/capture/db-queue/clock.mjs +11 -0
- package/dist/capture/db-queue/clock.mjs.map +1 -0
- package/dist/capture/db-queue/codec.mjs +75 -0
- package/dist/capture/db-queue/codec.mjs.map +1 -0
- package/dist/capture/db-queue/index.d.mts +18 -0
- package/dist/capture/db-queue/index.mjs +89 -0
- package/dist/capture/db-queue/index.mjs.map +1 -0
- package/dist/capture/db-queue/keeper.d.mts +34 -0
- package/dist/capture/db-queue/keeper.mjs +91 -0
- package/dist/capture/db-queue/keeper.mjs.map +1 -0
- package/dist/capture/db-queue/lease.mjs +45 -0
- package/dist/capture/db-queue/lease.mjs.map +1 -0
- package/dist/capture/db-queue/lifecycle.mjs +35 -0
- package/dist/capture/db-queue/lifecycle.mjs.map +1 -0
- package/dist/capture/db-queue/operations.mjs +93 -0
- package/dist/capture/db-queue/operations.mjs.map +1 -0
- package/dist/capture/db-queue/poll-loop.d.mts +28 -0
- package/dist/capture/db-queue/poll-loop.mjs +88 -0
- package/dist/capture/db-queue/poll-loop.mjs.map +1 -0
- package/dist/capture/db-queue/poll.d.mts +8 -0
- package/dist/capture/db-queue/poll.mjs +68 -0
- package/dist/capture/db-queue/poll.mjs.map +1 -0
- package/dist/capture/db-queue/timers.d.mts +10 -0
- package/dist/capture/db-queue/timers.mjs +36 -0
- package/dist/capture/db-queue/timers.mjs.map +1 -0
- package/dist/capture/db-queue/types.d.mts +81 -0
- package/dist/capture/db-queue/types.mjs +11 -0
- package/dist/capture/db-queue/types.mjs.map +1 -0
- package/dist/capture/db-queue/wake.mjs +47 -0
- package/dist/capture/db-queue/wake.mjs.map +1 -0
- package/dist/capture/design.mjs +38 -0
- package/dist/capture/design.mjs.map +1 -0
- package/dist/capture/diff-record.mjs +24 -5
- package/dist/capture/diff-record.mjs.map +1 -1
- package/dist/capture/dispatch.d.mts +13 -10
- package/dist/capture/dispatch.mjs +102 -111
- package/dist/capture/dispatch.mjs.map +1 -1
- package/dist/capture/enrich.mjs +59 -0
- package/dist/capture/enrich.mjs.map +1 -0
- package/dist/capture/failure-sets.mjs +21 -0
- package/dist/capture/failure-sets.mjs.map +1 -0
- package/dist/capture/fingerprint.d.mts +40 -0
- package/dist/capture/fingerprint.mjs +82 -0
- package/dist/capture/fingerprint.mjs.map +1 -0
- package/dist/capture/forked/channel.d.mts +23 -0
- package/dist/capture/forked/child.d.mts +32 -0
- package/dist/capture/forked/child.mjs +201 -0
- package/dist/capture/forked/child.mjs.map +1 -0
- package/dist/capture/forked/crash-tracker.mjs +22 -0
- package/dist/capture/forked/crash-tracker.mjs.map +1 -0
- package/dist/capture/forked/decision.d.mts +24 -0
- package/dist/capture/forked/decision.mjs +26 -0
- package/dist/capture/forked/decision.mjs.map +1 -0
- package/dist/capture/forked/exit.mjs +11 -0
- package/dist/capture/forked/exit.mjs.map +1 -0
- package/dist/capture/forked/fork.d.mts +29 -0
- package/dist/capture/forked/fork.mjs +92 -0
- package/dist/capture/forked/fork.mjs.map +1 -0
- package/dist/capture/forked/forked-capture.d.mts +38 -0
- package/dist/capture/forked/forked-capture.mjs +239 -0
- package/dist/capture/forked/forked-capture.mjs.map +1 -0
- package/dist/capture/forked/lease-book.mjs +45 -0
- package/dist/capture/forked/lease-book.mjs.map +1 -0
- package/dist/capture/forked/protocol.d.mts +36 -0
- package/dist/capture/forked/protocol.mjs +24 -0
- package/dist/capture/forked/protocol.mjs.map +1 -0
- package/dist/capture/forked/runs.mjs +74 -0
- package/dist/capture/forked/runs.mjs.map +1 -0
- package/dist/capture/guards.mjs +1 -2
- package/dist/capture/ignore-regions.d.mts +36 -0
- package/dist/capture/ignore-regions.mjs +65 -0
- package/dist/capture/ignore-regions.mjs.map +1 -0
- package/dist/capture/isolate.mjs +140 -0
- package/dist/capture/isolate.mjs.map +1 -0
- package/dist/capture/job-lease.d.mts +26 -0
- package/dist/capture/job-lease.mjs +56 -0
- package/dist/capture/job-lease.mjs.map +1 -0
- package/dist/capture/matrix.mjs +298 -0
- package/dist/capture/matrix.mjs.map +1 -0
- package/dist/capture/modes.d.mts +23 -0
- package/dist/capture/modes.mjs +106 -0
- package/dist/capture/modes.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +37 -7
- package/dist/capture/orchestrator.mjs +120 -110
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +9 -4
- package/dist/capture/pipeline.mjs +284 -48
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/progress.d.mts +9 -0
- package/dist/capture/progress.mjs +27 -0
- package/dist/capture/progress.mjs.map +1 -0
- package/dist/capture/publish/accept.d.mts +37 -0
- package/dist/capture/publish/accept.mjs +27 -0
- package/dist/capture/publish/accept.mjs.map +1 -0
- package/dist/capture/publish/complete.d.mts +44 -0
- package/dist/capture/publish/complete.mjs +63 -0
- package/dist/capture/publish/complete.mjs.map +1 -0
- package/dist/capture/publish/extract.d.mts +18 -0
- package/dist/capture/publish/extract.mjs +120 -0
- package/dist/capture/publish/extract.mjs.map +1 -0
- package/dist/capture/publish/handler.d.mts +26 -0
- package/dist/capture/publish/handler.mjs +61 -0
- package/dist/capture/publish/handler.mjs.map +1 -0
- package/dist/capture/publish/index.d.mts +11 -0
- package/dist/capture/publish/invalid-zip.d.mts +8 -0
- package/dist/capture/publish/invalid-zip.mjs +11 -0
- package/dist/capture/publish/invalid-zip.mjs.map +1 -0
- package/dist/capture/publish/limits.d.mts +35 -0
- package/dist/capture/publish/limits.mjs +38 -0
- package/dist/capture/publish/limits.mjs.map +1 -0
- package/dist/capture/publish/published.d.mts +13 -0
- package/dist/capture/publish/published.mjs +17 -0
- package/dist/capture/publish/published.mjs.map +1 -0
- package/dist/capture/publish/run.d.mts +31 -0
- package/dist/capture/publish/run.mjs +29 -0
- package/dist/capture/publish/run.mjs.map +1 -0
- package/dist/capture/rediff.d.mts +1 -3
- package/dist/capture/rediff.mjs +23 -9
- package/dist/capture/rediff.mjs.map +1 -1
- package/dist/capture/render.mjs +52 -0
- package/dist/capture/render.mjs.map +1 -0
- package/dist/capture/scratch.d.mts +42 -0
- package/dist/capture/scratch.mjs +136 -0
- package/dist/capture/scratch.mjs.map +1 -0
- package/dist/capture/shard-plan.mjs +127 -0
- package/dist/capture/shard-plan.mjs.map +1 -0
- package/dist/capture/shards/context.mjs +51 -0
- package/dist/capture/shards/context.mjs.map +1 -0
- package/dist/capture/shards/execute.mjs +175 -0
- package/dist/capture/shards/execute.mjs.map +1 -0
- package/dist/capture/shards/fan-out.mjs +125 -0
- package/dist/capture/shards/fan-out.mjs.map +1 -0
- package/dist/capture/shards/history.mjs +34 -0
- package/dist/capture/shards/history.mjs.map +1 -0
- package/dist/capture/shards/reduce.mjs +96 -0
- package/dist/capture/shards/reduce.mjs.map +1 -0
- package/dist/capture/sizing.d.mts +0 -2
- package/dist/capture/sizing.mjs +2 -13
- package/dist/capture/sizing.mjs.map +1 -1
- package/dist/capture/skip-checks.mjs +3 -67
- package/dist/capture/skip-checks.mjs.map +1 -1
- package/dist/capture/statics.d.mts +7 -10
- package/dist/capture/statics.mjs +26 -74
- package/dist/capture/statics.mjs.map +1 -1
- package/dist/capture/storybook-urls.d.mts +38 -0
- package/dist/capture/storybook-urls.mjs +41 -0
- package/dist/capture/storybook-urls.mjs.map +1 -0
- package/dist/capture/storybook.d.mts +12 -6
- package/dist/capture/storybook.mjs +63 -19
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/capture/target.mjs +20 -0
- package/dist/capture/target.mjs.map +1 -0
- package/dist/capture/targets.d.mts +23 -0
- package/dist/capture/targets.mjs +31 -0
- package/dist/capture/targets.mjs.map +1 -0
- package/dist/capture/tests.d.mts +35 -0
- package/dist/capture/tests.mjs +136 -0
- package/dist/capture/tests.mjs.map +1 -0
- package/dist/capture/token-chunks.mjs +91 -0
- package/dist/capture/token-chunks.mjs.map +1 -0
- package/dist/capture.d.mts +63 -7
- package/dist/capture.mjs +51 -3
- package/dist/config.d.mts +136 -14
- package/dist/config.mjs +14 -8
- package/dist/config.mjs.map +1 -1
- package/dist/diff.d.mts +7 -3
- package/dist/diff.mjs +15 -3
- package/dist/diff.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/insights/budget.d.mts +18 -0
- package/dist/insights/budget.mjs +32 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/envelope.d.mts +57 -0
- package/dist/insights/envelope.mjs +59 -0
- package/dist/insights/envelope.mjs.map +1 -0
- package/dist/insights/evidence-health.d.mts +30 -0
- package/dist/insights/evidence-health.mjs +27 -0
- package/dist/insights/evidence-health.mjs.map +1 -0
- package/dist/insights/evidence.d.mts +53 -0
- package/dist/insights/evidence.mjs +63 -0
- package/dist/insights/evidence.mjs.map +1 -0
- package/dist/insights/hash.d.mts +16 -0
- package/dist/insights/hash.mjs +20 -0
- package/dist/insights/hash.mjs.map +1 -0
- package/dist/insights/limits.d.mts +22 -0
- package/dist/insights/limits.mjs +23 -0
- package/dist/insights/limits.mjs.map +1 -0
- package/dist/insights/prompts/v1.d.mts +11 -0
- package/dist/insights/prompts/v1.mjs +33 -0
- package/dist/insights/prompts/v1.mjs.map +1 -0
- package/dist/insights/redact.d.mts +7 -0
- package/dist/insights/redact.mjs +23 -0
- package/dist/insights/redact.mjs.map +1 -0
- package/dist/insights/text.mjs +43 -0
- package/dist/insights/text.mjs.map +1 -0
- package/dist/insights.d.mts +11 -0
- package/dist/insights.mjs +12 -0
- package/dist/logger.d.mts +0 -2
- package/dist/logger.mjs +18 -3
- package/dist/logger.mjs.map +1 -1
- package/dist/models/ai-budget-alert.d.mts +21 -0
- package/dist/models/ai-budget-alert.mjs +37 -0
- package/dist/models/ai-budget-alert.mjs.map +1 -0
- package/dist/models/ai-usage.d.mts +38 -0
- package/dist/models/ai-usage.mjs +60 -0
- package/dist/models/ai-usage.mjs.map +1 -0
- package/dist/models/baseline-approval.d.mts +18 -0
- package/dist/models/baseline-approval.mjs +41 -0
- package/dist/models/baseline-approval.mjs.map +1 -0
- package/dist/models/baseline-archive.d.mts +82 -0
- package/dist/models/baseline-archive.mjs +105 -0
- package/dist/models/baseline-archive.mjs.map +1 -0
- package/dist/models/baseline-atomic.mjs +66 -0
- package/dist/models/baseline-atomic.mjs.map +1 -0
- package/dist/models/baseline-conflict.d.mts +6 -0
- package/dist/models/baseline-conflict.mjs +9 -0
- package/dist/models/baseline-conflict.mjs.map +1 -0
- package/dist/models/baseline-export.d.mts +17 -0
- package/dist/models/baseline-export.mjs +135 -0
- package/dist/models/baseline-export.mjs.map +1 -0
- package/dist/models/baseline-import.mjs +117 -0
- package/dist/models/baseline-import.mjs.map +1 -0
- package/dist/models/baseline-origin.d.mts +16 -0
- package/dist/models/baseline-origin.mjs +35 -0
- package/dist/models/baseline-origin.mjs.map +1 -0
- package/dist/models/baseline-status.d.mts +0 -2
- package/dist/models/baseline-status.mjs +1 -2
- package/dist/models/baseline-transfer.d.mts +48 -0
- package/dist/models/baseline-transfer.mjs +149 -0
- package/dist/models/baseline-transfer.mjs.map +1 -0
- package/dist/models/baseline.d.mts +37 -7
- package/dist/models/baseline.mjs +152 -28
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build-component.d.mts +26 -0
- package/dist/models/build-component.mjs +54 -0
- package/dist/models/build-component.mjs.map +1 -0
- package/dist/models/build-review-status.d.mts +27 -0
- package/dist/models/build-review-status.mjs +36 -0
- package/dist/models/build-review-status.mjs.map +1 -0
- package/dist/models/build-review.d.mts +26 -0
- package/dist/models/build-review.mjs +53 -0
- package/dist/models/build-review.mjs.map +1 -0
- package/dist/models/build.d.mts +25 -5
- package/dist/models/build.mjs +53 -15
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/capture-attempt.d.mts +7 -3
- package/dist/models/capture-attempt.mjs +30 -6
- package/dist/models/capture-attempt.mjs.map +1 -1
- package/dist/models/capture-job.d.mts +104 -0
- package/dist/models/capture-job.mjs +287 -0
- package/dist/models/capture-job.mjs.map +1 -0
- package/dist/models/capture-log.d.mts +1 -3
- package/dist/models/capture-log.mjs +5 -5
- package/dist/models/capture-log.mjs.map +1 -1
- package/dist/models/capture-shard.d.mts +60 -0
- package/dist/models/capture-shard.mjs +189 -0
- package/dist/models/capture-shard.mjs.map +1 -0
- package/dist/models/changeset.d.mts +46 -0
- package/dist/models/changeset.mjs +80 -0
- package/dist/models/changeset.mjs.map +1 -0
- package/dist/models/columns.mjs +18 -0
- package/dist/models/columns.mjs.map +1 -0
- package/dist/models/comment.d.mts +4 -3
- package/dist/models/comment.mjs +8 -6
- package/dist/models/comment.mjs.map +1 -1
- package/dist/models/component.d.mts +41 -0
- package/dist/models/component.mjs +59 -0
- package/dist/models/component.mjs.map +1 -0
- package/dist/models/connections.d.mts +39 -0
- package/dist/models/connections.mjs +111 -0
- package/dist/models/connections.mjs.map +1 -0
- package/dist/models/content-ref.d.mts +1 -3
- package/dist/models/content-ref.mjs +2 -3
- package/dist/models/content-ref.mjs.map +1 -1
- package/dist/models/credentials.d.mts +0 -2
- package/dist/models/credentials.mjs +13 -4
- package/dist/models/credentials.mjs.map +1 -1
- package/dist/models/design-credentials.d.mts +39 -0
- package/dist/models/design-credentials.mjs +85 -0
- package/dist/models/design-credentials.mjs.map +1 -0
- package/dist/models/design-health.d.mts +14 -0
- package/dist/models/design-health.mjs +24 -0
- package/dist/models/design-health.mjs.map +1 -0
- package/dist/models/design-nodes.d.mts +81 -0
- package/dist/models/design-nodes.mjs +221 -0
- package/dist/models/design-nodes.mjs.map +1 -0
- package/dist/models/design-renders.mjs +73 -0
- package/dist/models/design-renders.mjs.map +1 -0
- package/dist/models/design-resolve.d.mts +27 -0
- package/dist/models/design-resolve.mjs +40 -0
- package/dist/models/design-resolve.mjs.map +1 -0
- package/dist/models/design.d.mts +37 -0
- package/dist/models/design.mjs +89 -0
- package/dist/models/design.mjs.map +1 -0
- package/dist/models/inherited-ratio.d.mts +19 -0
- package/dist/models/inherited-ratio.mjs +21 -0
- package/dist/models/inherited-ratio.mjs.map +1 -0
- package/dist/models/insight.d.mts +68 -0
- package/dist/models/insight.mjs +161 -0
- package/dist/models/insight.mjs.map +1 -0
- package/dist/models/label.d.mts +4 -4
- package/dist/models/label.mjs +26 -12
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/member.d.mts +1 -3
- package/dist/models/member.mjs +6 -6
- package/dist/models/member.mjs.map +1 -1
- package/dist/models/notification-channel.d.mts +37 -5
- package/dist/models/notification-channel.mjs +89 -11
- package/dist/models/notification-channel.mjs.map +1 -1
- package/dist/models/notification-subscription.d.mts +1 -4
- package/dist/models/notification-subscription.mjs +8 -11
- package/dist/models/notification-subscription.mjs.map +1 -1
- package/dist/models/project-group-mapping.d.mts +6 -3
- package/dist/models/project-group-mapping.mjs +15 -6
- package/dist/models/project-group-mapping.mjs.map +1 -1
- package/dist/models/project.d.mts +9 -3
- package/dist/models/project.mjs +10 -6
- package/dist/models/project.mjs.map +1 -1
- package/dist/models/public-origin.d.mts +61 -0
- package/dist/models/public-origin.mjs +113 -0
- package/dist/models/public-origin.mjs.map +1 -0
- package/dist/models/review-switch-off.d.mts +12 -0
- package/dist/models/review-switch-off.mjs +25 -0
- package/dist/models/review-switch-off.mjs.map +1 -0
- package/dist/models/site-settings.d.mts +21 -0
- package/dist/models/site-settings.mjs +47 -0
- package/dist/models/site-settings.mjs.map +1 -0
- package/dist/models/snapshot.d.mts +39 -3
- package/dist/models/snapshot.mjs +54 -5
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/token.d.mts +13 -3
- package/dist/models/token.mjs +24 -7
- package/dist/models/token.mjs.map +1 -1
- package/dist/models/usage.d.mts +74 -0
- package/dist/models/usage.mjs +148 -0
- package/dist/models/usage.mjs.map +1 -0
- package/dist/models/user.d.mts +1 -3
- package/dist/models/user.mjs +5 -5
- package/dist/models/user.mjs.map +1 -1
- package/dist/models/webhook.d.mts +15 -3
- package/dist/models/webhook.mjs +34 -6
- package/dist/models/webhook.mjs.map +1 -1
- package/dist/models.d.mts +30 -4
- package/dist/models.mjs +31 -6
- package/dist/options.d.mts +16 -0
- package/dist/options.mjs +88 -0
- package/dist/options.mjs.map +1 -0
- package/dist/paths.d.mts +21 -6
- package/dist/paths.mjs +57 -12
- package/dist/paths.mjs.map +1 -1
- package/dist/retention/ai.d.mts +16 -0
- package/dist/retention/ai.mjs +22 -0
- package/dist/retention/ai.mjs.map +1 -0
- package/dist/retention/pending.mjs +46 -0
- package/dist/retention/pending.mjs.map +1 -0
- package/dist/retention/published-gzip-sweep.mjs +28 -0
- package/dist/retention/published-gzip-sweep.mjs.map +1 -0
- package/dist/retention/screenshot-sweep.d.mts +49 -0
- package/dist/retention/screenshot-sweep.mjs +127 -0
- package/dist/retention/screenshot-sweep.mjs.map +1 -0
- package/dist/retention.d.mts +16 -4
- package/dist/retention.mjs +76 -16
- package/dist/retention.mjs.map +1 -1
- package/dist/schema/baseline.d.mts +8 -2
- package/dist/schema/build-component.d.mts +23 -0
- package/dist/schema/build-review.d.mts +15 -0
- package/dist/schema/build.d.mts +7 -2
- package/dist/schema/capture-attempt.d.mts +0 -2
- package/dist/schema/capture-job.d.mts +58 -0
- package/dist/schema/capture-shard.d.mts +33 -0
- package/dist/schema/comment.d.mts +2 -2
- package/dist/schema/connection.d.mts +22 -0
- package/dist/schema/content-ref.d.mts +0 -2
- package/dist/schema/design.d.mts +56 -0
- package/dist/schema/insight.d.mts +52 -0
- package/dist/schema/label.d.mts +0 -2
- package/dist/schema/member.d.mts +0 -2
- package/dist/schema/notification.d.mts +4 -2
- package/dist/schema/project-group-mapping.d.mts +0 -2
- package/dist/schema/project.d.mts +21 -2
- package/dist/schema/site-setting.d.mts +19 -0
- package/dist/schema/snapshot.d.mts +17 -2
- package/dist/schema/token.d.mts +0 -2
- package/dist/schema/usage.d.mts +28 -0
- package/dist/schema/user.d.mts +0 -2
- package/dist/schema/webhook.d.mts +0 -2
- package/dist/schema.d.mts +12 -6
- package/dist/serverless.d.mts +19 -0
- package/dist/serverless.mjs +36 -0
- package/dist/serverless.mjs.map +1 -0
- package/dist/test-helpers/build-reference-contract.d.mts +14 -0
- package/dist/test-helpers/build-reference-contract.mjs +66 -0
- package/dist/test-helpers/build-reference-contract.mjs.map +1 -0
- package/dist/test-helpers/create-project.d.mts +1 -3
- package/dist/test-helpers/create-project.mjs +1 -2
- package/dist/test-helpers/database-contract.d.mts +0 -2
- package/dist/test-helpers/database-contract.mjs +37 -2
- package/dist/test-helpers/database-contract.mjs.map +1 -1
- package/dist/test-helpers/deferred.d.mts +37 -0
- package/dist/test-helpers/deferred.mjs +60 -0
- package/dist/test-helpers/deferred.mjs.map +1 -0
- package/dist/test-helpers/design-contract.d.mts +19 -0
- package/dist/test-helpers/design-contract.mjs +39 -0
- package/dist/test-helpers/design-contract.mjs.map +1 -0
- package/dist/test-helpers/fake-adapters.d.mts +0 -2
- package/dist/test-helpers/fake-adapters.mjs +1 -2
- package/dist/test-helpers/fake-database.d.mts +0 -2
- package/dist/test-helpers/fake-database.mjs +21 -4
- package/dist/test-helpers/fake-database.mjs.map +1 -1
- package/dist/test-helpers/fake-design.d.mts +41 -0
- package/dist/test-helpers/fake-design.mjs +113 -0
- package/dist/test-helpers/fake-design.mjs.map +1 -0
- package/dist/test-helpers/fake-notifier.d.mts +19 -0
- package/dist/test-helpers/fake-notifier.mjs +43 -0
- package/dist/test-helpers/fake-notifier.mjs.map +1 -0
- package/dist/test-helpers/fake-storage.d.mts +0 -2
- package/dist/test-helpers/fake-storage.mjs +1 -2
- package/dist/test-helpers/fake-tables.mjs +260 -69
- package/dist/test-helpers/fake-tables.mjs.map +1 -1
- package/dist/test-helpers/manual-clock.d.mts +25 -0
- package/dist/test-helpers/manual-clock.mjs +55 -0
- package/dist/test-helpers/manual-clock.mjs.map +1 -0
- package/dist/test-helpers/notifier-contract.d.mts +7 -3
- package/dist/test-helpers/notifier-contract.mjs +33 -5
- package/dist/test-helpers/notifier-contract.mjs.map +1 -1
- package/dist/test-helpers/queue-contract.d.mts +16 -4
- package/dist/test-helpers/queue-contract.mjs +196 -3
- package/dist/test-helpers/queue-contract.mjs.map +1 -1
- package/dist/test-helpers/queue-multi-instance.d.mts +18 -0
- package/dist/test-helpers/queue-multi-instance.mjs +205 -0
- package/dist/test-helpers/queue-multi-instance.mjs.map +1 -0
- package/dist/test-helpers/runner-contract.d.mts +0 -2
- package/dist/test-helpers/runner-contract.mjs +12 -2
- package/dist/test-helpers/runner-contract.mjs.map +1 -1
- package/dist/test-helpers/sql-chunks.mjs +25 -2
- package/dist/test-helpers/sql-chunks.mjs.map +1 -1
- package/dist/test-helpers/storage-contract.d.mts +0 -2
- package/dist/test-helpers/storage-contract.mjs +2 -3
- package/dist/test-helpers.d.mts +10 -4
- package/dist/test-helpers.mjs +9 -3
- package/dist/tracing.mjs +36 -6
- package/dist/tracing.mjs.map +1 -1
- package/dist/types.d.mts +14 -5
- package/dist/types.mjs +22 -6
- package/dist/types.mjs.map +1 -1
- package/dist/urls.d.mts +4 -2
- package/dist/urls.mjs +12 -4
- package/dist/urls.mjs.map +1 -1
- package/dist/utils/adaptive-concurrency.d.mts +64 -0
- package/dist/utils/adaptive-concurrency.mjs +148 -0
- package/dist/utils/adaptive-concurrency.mjs.map +1 -0
- package/dist/utils/connect-retry.d.mts +26 -0
- package/dist/utils/connect-retry.mjs +42 -0
- package/dist/utils/connect-retry.mjs.map +1 -0
- package/dist/utils/encrypt.d.mts +0 -2
- package/dist/utils/encrypt.mjs +1 -2
- package/dist/utils/environment.d.mts +17 -0
- package/dist/utils/environment.mjs +58 -0
- package/dist/utils/environment.mjs.map +1 -0
- package/dist/utils/hash.d.mts +0 -2
- package/dist/utils/hash.mjs +1 -2
- package/dist/utils/http.d.mts +29 -3
- package/dist/utils/http.mjs +100 -19
- package/dist/utils/http.mjs.map +1 -1
- package/dist/utils/map-adaptive.d.mts +13 -0
- package/dist/utils/map-adaptive.mjs +71 -0
- package/dist/utils/map-adaptive.mjs.map +1 -0
- package/dist/utils/map-limit.d.mts +8 -0
- package/dist/utils/map-limit.mjs +31 -0
- package/dist/utils/map-limit.mjs.map +1 -0
- package/dist/utils/mime.d.mts +0 -2
- package/dist/utils/mime.mjs +1 -2
- package/dist/utils/page-limiter.d.mts +35 -0
- package/dist/utils/page-limiter.mjs +55 -0
- package/dist/utils/page-limiter.mjs.map +1 -0
- package/dist/utils/path-security.d.mts +0 -2
- package/dist/utils/path-security.mjs +2 -4
- package/dist/utils/path-security.mjs.map +1 -1
- package/dist/utils/public-origin.d.mts +18 -0
- package/dist/utils/public-origin.mjs +74 -0
- package/dist/utils/public-origin.mjs.map +1 -0
- package/dist/utils/redact.d.mts +0 -2
- package/dist/utils/redact.mjs +4 -2
- package/dist/utils/redact.mjs.map +1 -1
- package/dist/utils/remote-environment.d.mts +76 -0
- package/dist/utils/remote-environment.mjs +163 -0
- package/dist/utils/remote-environment.mjs.map +1 -0
- package/dist/utils/resources.d.mts +62 -0
- package/dist/utils/resources.mjs +121 -0
- package/dist/utils/resources.mjs.map +1 -0
- package/dist/utils/screenshot-store.d.mts +13 -0
- package/dist/utils/screenshot-store.mjs +40 -0
- package/dist/utils/screenshot-store.mjs.map +1 -0
- package/dist/utils/session-gate.d.mts +23 -0
- package/dist/utils/session-gate.mjs +69 -0
- package/dist/utils/session-gate.mjs.map +1 -0
- package/dist/utils/storybook-token.d.mts +89 -0
- package/dist/utils/storybook-token.mjs +135 -0
- package/dist/utils/storybook-token.mjs.map +1 -0
- package/dist/utils/storybook-url.d.mts +13 -0
- package/dist/utils/storybook-url.mjs +20 -0
- package/dist/utils/storybook-url.mjs.map +1 -0
- package/dist/utils/timing.d.mts +0 -2
- package/dist/utils/timing.mjs +2 -3
- package/dist/utils/timing.mjs.map +1 -1
- package/dist/utils/ulid.d.mts +0 -2
- package/dist/utils/ulid.mjs +1 -2
- package/dist/utils/zip-read.mjs +128 -0
- package/dist/utils/zip-read.mjs.map +1 -0
- package/dist/utils/zip-write.d.mts +16 -0
- package/dist/utils/zip-write.mjs +187 -0
- package/dist/utils/zip-write.mjs.map +1 -0
- package/dist/utils.d.mts +17 -3
- package/dist/utils.mjs +18 -4
- package/package.json +35 -26
- package/dist/adapter/capture-runner.mjs.map +0 -1
- package/dist/adapter/git-host/comments.d.mts +0 -41
- package/dist/adapter/git-host/comments.mjs +0 -54
- package/dist/adapter/git-host/comments.mjs.map +0 -1
- package/dist/adapter/git-host/helpers.d.mts +0 -13
- package/dist/adapter/git-host/helpers.mjs +0 -30
- package/dist/adapter/git-host/helpers.mjs.map +0 -1
- package/dist/adapter/git-host.d.mts +0 -42
- package/dist/capture/queue.d.mts +0 -38
- package/dist/capture/queue.mjs +0 -98
- package/dist/capture/queue.mjs.map +0 -1
- package/dist/capture/recovery.d.mts +0 -21
- package/dist/capture/recovery.mjs +0 -83
- package/dist/capture/recovery.mjs.map +0 -1
- package/dist/capture/status-fanout.mjs +0 -72
- package/dist/capture/status-fanout.mjs.map +0 -1
- package/dist/diff/engine.d.mts +0 -16
- package/dist/diff/engine.mjs +0 -87
- package/dist/diff/engine.mjs.map +0 -1
- package/dist/diff/options.d.mts +0 -42
- package/dist/diff/options.mjs +0 -15
- package/dist/diff/options.mjs.map +0 -1
- package/dist/models/status-config.d.mts +0 -38
- package/dist/models/status-config.mjs +0 -63
- package/dist/models/status-config.mjs.map +0 -1
- package/dist/schema/status-config.d.mts +0 -16
- package/dist/test-helpers/git-contract.d.mts +0 -11
- package/dist/test-helpers/git-contract.mjs +0 -34
- package/dist/test-helpers/git-contract.mjs.map +0 -1
- /package/dist/adapter/{git-host.mjs → oauth.mjs} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scratch.mjs","names":[],"sources":["../../src/capture/scratch.ts"],"sourcesContent":["/** Per-build scratch directories: where they live, removing them, and sweeping crash leftovers. */\nimport { readdir, rm, rmdir, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { Logger } from \"../logger.ts\";\n\n/**\n * Subtree of `scratchDir` that holds extracted Storybooks.\n *\n * `scratchDir` is often also the local storage root (dev server, generated servers), where\n * `<project>/builds/<build>/...` holds screenshots and published statics. Keeping scratch in its\n * own subtree means removing it can never touch a storage key.\n */\nexport const SCRATCH_DIRNAME = \".storyshelf-scratch\";\n\n/** Scratch dirs untouched for this long are assumed orphaned by a crash (no capture runs this long). */\nexport const SCRATCH_MAX_AGE_MS = 6 * 60 * 60 * 1000;\n\n/** Directory inside a build's scratch directory that holds one subdirectory per shard. */\nconst SHARDS_DIRNAME = \"shards\";\n\n/** Directory inside a build's scratch directory for the build's own job, apart from its shards. */\nconst JOB_DIRNAME = \"job\";\n\n/** Scratch directory owned by one build: `<scratchDir>/.storyshelf-scratch/<projectId>/<buildId>`. */\nexport function buildScratchDir(scratchDir: string, projectId: string, buildId: string): string {\n return join(scratchDir, SCRATCH_DIRNAME, projectId, buildId);\n}\n\n/** Scratch directory of the build's own job (plan or whole build): `<build>/job`, beside `shards/`. */\nexport function jobScratchDir(scratchDir: string, projectId: string, buildId: string): string {\n return join(buildScratchDir(scratchDir, projectId, buildId), JOB_DIRNAME);\n}\n\n/** Scratch directory of one shard of a build, inside the build's: shards of a build can share a host. */\nexport function shardScratchDir(\n scratchDir: string,\n projectId: string,\n buildId: string,\n shardId: string,\n): string {\n return join(buildScratchDir(scratchDir, projectId, buildId), SHARDS_DIRNAME, shardId);\n}\n\n/**\n * Remove one shard's scratch directory, leaving the build's other shards alone, then the build's\n * directories if that left them empty: whichever shard finishes last tidies up. Best effort.\n */\nexport async function removeShardScratch(\n scratchDir: string,\n projectId: string,\n buildId: string,\n shardId: string,\n logger?: Logger,\n): Promise<void> {\n try {\n await rm(shardScratchDir(scratchDir, projectId, buildId, shardId), {\n recursive: true,\n force: true,\n });\n await pruneBuildScratch(scratchDir, projectId, buildId);\n } catch (error) {\n logger?.warn({ err: error, buildId, shardId }, \"failed to remove shard scratch directory\");\n }\n}\n\n/**\n * Remove the scratch of the build's own job (the plan, or the whole build when it is not sharded)\n * and the build directory too if nothing else is left in it.\n *\n * Shards live in their own `shards/` directory beside `job/`, so this can never reach one: the plan\n * hands its shards to the queue before it cleans up, and a worker may already be extracting.\n */\nexport async function removeJobScratch(\n scratchDir: string,\n projectId: string,\n buildId: string,\n logger?: Logger,\n): Promise<void> {\n try {\n await rm(jobScratchDir(scratchDir, projectId, buildId), { recursive: true, force: true });\n await pruneBuildScratch(scratchDir, projectId, buildId);\n } catch (error) {\n logger?.warn({ err: error, buildId }, \"failed to remove job scratch directory\");\n }\n}\n\n/**\n * Remove the build's scratch directories that are empty, and nothing that is not. A shard that is\n * still running (a redelivered duplicate of one that already finished) keeps its directory; its own\n * cleanup, or the boot sweep, removes it later. `rmdir` fails on a non-empty directory, which is\n * the point.\n */\nexport async function pruneBuildScratch(\n scratchDir: string,\n projectId: string,\n buildId: string,\n): Promise<void> {\n const buildDir = buildScratchDir(scratchDir, projectId, buildId);\n await rmdir(join(buildDir, SHARDS_DIRNAME)).catch(ignore);\n await rmdir(buildDir).catch(ignore);\n}\n\n/** Remove a build's scratch directory. Best effort: a failure is logged, never thrown. */\nexport async function removeBuildScratch(\n scratchDir: string,\n projectId: string,\n buildId: string,\n logger?: Logger,\n): Promise<void> {\n try {\n await rm(buildScratchDir(scratchDir, projectId, buildId), { recursive: true, force: true });\n } catch (error) {\n logger?.warn({ err: error, buildId }, \"failed to remove build scratch directory\");\n }\n}\n\n/** Options for {@link sweepOrphanedScratch}. */\nexport interface ScratchSweepOptions {\n /** Remove build scratch dirs not modified for this long (default {@link SCRATCH_MAX_AGE_MS}). */\n maxAgeMs?: number;\n /** Clock override for tests. */\n now?: number;\n logger?: Logger;\n}\n\n/**\n * Boot-time sweep for scratch dirs a crash left behind. Age-gated rather than wholesale so a\n * second process sharing the same `scratchDir` (server plus worker) keeps its live captures.\n *\n * @returns How many build scratch directories were removed.\n */\nexport async function sweepOrphanedScratch(\n scratchDir: string,\n options: ScratchSweepOptions = {},\n): Promise<number> {\n const root = join(scratchDir, SCRATCH_DIRNAME);\n const cutoff = (options.now ?? Date.now()) - (options.maxAgeMs ?? SCRATCH_MAX_AGE_MS);\n const projects = await readdir(root).catch(() => [] as string[]);\n const removed = await Promise.all(\n projects.map(async (projectId) => await sweepProject(join(root, projectId), cutoff)),\n );\n const total = removed.reduce((sum, count) => sum + count, 0);\n if (total > 0) {\n options.logger?.info({ removed: total }, \"removed orphaned capture scratch directories\");\n }\n return total;\n}\n\nasync function sweepProject(projectDir: string, cutoff: number): Promise<number> {\n const builds = await readdir(projectDir).catch(() => [] as string[]);\n const results = await Promise.all(\n builds.map(async (buildId) => await sweepBuild(join(projectDir, buildId), cutoff)),\n );\n // Drop the project dir once empty; `rmdir` semantics via a non-recursive remove that fails if not.\n if ((await readdir(projectDir).catch(() => [\"x\"])).length === 0) {\n await rm(projectDir, { recursive: true, force: true }).catch(ignore);\n }\n return results.filter(Boolean).length;\n}\n\nasync function sweepBuild(buildDir: string, cutoff: number): Promise<boolean> {\n try {\n if ((await stat(buildDir)).mtimeMs >= cutoff) {\n return false;\n }\n await rm(buildDir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n}\n\nfunction ignore(): void {}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
// src/capture/shard-plan.ts
|
|
5
|
+
/**
|
|
6
|
+
* Split the work into shards of about `size` captures, keeping a story's engines together so
|
|
7
|
+
* a newly enabled engine can still bootstrap from its counterpart in the same shard.
|
|
8
|
+
*
|
|
9
|
+
* Returns `[]` when sharding is off or the build is too small to be worth it (up to twice the
|
|
10
|
+
* shard size runs as one shard, exactly as before).
|
|
11
|
+
*
|
|
12
|
+
* Without timings the slices are contiguous runs of stories in their original order. Given
|
|
13
|
+
* `weightOf`, the stories are packed longest-first into shards of equal capture capacity and the
|
|
14
|
+
* shards come back heaviest first, so the slow work starts early and no shard straggles.
|
|
15
|
+
*
|
|
16
|
+
* @param work - Per-engine render lists, primary engine first.
|
|
17
|
+
* @param capturesOf - Captures one story produces on one engine (its viewport count).
|
|
18
|
+
* @param size - Target captures per shard; undefined or below 1 turns sharding off.
|
|
19
|
+
* @param weightOf - What a story cost last time, in milliseconds; unknown for a story returns undefined.
|
|
20
|
+
*/
|
|
21
|
+
function planShards(work, capturesOf, size, weightOf) {
|
|
22
|
+
if (!size || !worthSharding(work, capturesOf, size)) return [];
|
|
23
|
+
const costs = storyCosts(work, capturesOf);
|
|
24
|
+
const weights = weightOf ? weighted(costs, weightOf) : void 0;
|
|
25
|
+
return (weights ? packLongestFirst(costs, weights, size) : sliceByCost(costs, size)).map((ids) => restrictTo(work, new Set(ids)));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether {@link planShards} would split this work at all: sharding is on (`size` of at least 1)
|
|
29
|
+
* and the work is more than twice the shard size. Lets the plan stage skip work only an ordering needs.
|
|
30
|
+
*/
|
|
31
|
+
function worthSharding(work, capturesOf, size) {
|
|
32
|
+
return size !== void 0 && size >= 1 && totalCaptures(work, capturesOf) > size * 2;
|
|
33
|
+
}
|
|
34
|
+
/** Total captures the work produces. */
|
|
35
|
+
function totalCaptures(work, capturesOf) {
|
|
36
|
+
return [...storyCosts(work, capturesOf).values()].reduce((sum, cost) => sum + cost, 0);
|
|
37
|
+
}
|
|
38
|
+
/** Captures per story id, summed over the engines that render it, in first-seen order. */
|
|
39
|
+
function storyCosts(work, capturesOf) {
|
|
40
|
+
const costs = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const engine of work) for (const story of engine.stories) costs.set(story.id, (costs.get(story.id) ?? 0) + capturesOf(story));
|
|
42
|
+
return costs;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* What each story is expected to cost in milliseconds. A story nothing is known about is priced at
|
|
46
|
+
* the average cost of one capture, so a new story is treated as ordinary rather than free.
|
|
47
|
+
*/
|
|
48
|
+
function weighted(costs, weightOf) {
|
|
49
|
+
const known = [...costs.keys()].flatMap((id) => {
|
|
50
|
+
const weight = weightOf(id);
|
|
51
|
+
return typeof weight === "number" && weight > 0 ? [[id, weight]] : [];
|
|
52
|
+
});
|
|
53
|
+
if (known.length === 0) return;
|
|
54
|
+
const knownCaptures = known.reduce((sum, [id]) => sum + (costs.get(id) ?? 0), 0);
|
|
55
|
+
const perCapture = known.reduce((sum, [, weight]) => sum + weight, 0) / Math.max(knownCaptures, 1);
|
|
56
|
+
const weights = new Map(known);
|
|
57
|
+
for (const [id, captures] of costs) if (!weights.has(id)) weights.set(id, perCapture * captures);
|
|
58
|
+
return weights;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Longest-processing-time packing: the dearest stories are placed first, each into the shard that
|
|
62
|
+
* has the least work so far and still has room for its captures. Shards come back heaviest first.
|
|
63
|
+
*/
|
|
64
|
+
function packLongestFirst(costs, weights, size) {
|
|
65
|
+
const count = Math.max(1, Math.ceil(sumOf(costs) / size));
|
|
66
|
+
const bins = Array.from({ length: count }, newBin);
|
|
67
|
+
for (const id of byWeightDescending(costs, weights)) {
|
|
68
|
+
const captures = costs.get(id) ?? 0;
|
|
69
|
+
const bin = binFor(bins, captures, size);
|
|
70
|
+
bin.ids.push(id);
|
|
71
|
+
bin.captures += captures;
|
|
72
|
+
bin.weight += weights.get(id) ?? 0;
|
|
73
|
+
}
|
|
74
|
+
return bins.filter((bin) => bin.ids.length > 0).toSorted((left, right) => right.weight - left.weight).map((bin) => bin.ids);
|
|
75
|
+
}
|
|
76
|
+
/** Story ids dearest first; equal cost keeps the order they were planned in. */
|
|
77
|
+
function byWeightDescending(costs, weights) {
|
|
78
|
+
return [...costs.keys()].toSorted((left, right) => (weights.get(right) ?? 0) - (weights.get(left) ?? 0));
|
|
79
|
+
}
|
|
80
|
+
function newBin() {
|
|
81
|
+
return {
|
|
82
|
+
ids: [],
|
|
83
|
+
captures: 0,
|
|
84
|
+
weight: 0
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The shard with the least work that still has room for `captures`. When none has room the last
|
|
89
|
+
* stories did not fit anywhere, so a shard is added: one more beats overfilling one, just as an
|
|
90
|
+
* oversized story gets a shard of its own when there are no timings.
|
|
91
|
+
*/
|
|
92
|
+
function binFor(bins, captures, size) {
|
|
93
|
+
const [lightest] = bins.filter((bin) => bin.captures + captures <= size).toSorted((left, right) => left.weight - right.weight);
|
|
94
|
+
if (lightest) return lightest;
|
|
95
|
+
const fresh = newBin();
|
|
96
|
+
bins.push(fresh);
|
|
97
|
+
return fresh;
|
|
98
|
+
}
|
|
99
|
+
function sumOf(costs) {
|
|
100
|
+
return [...costs.values()].reduce((sum, cost) => sum + cost, 0);
|
|
101
|
+
}
|
|
102
|
+
/** Contiguous runs of ids whose cost stays within `size` (a single oversized story gets its own). */
|
|
103
|
+
function sliceByCost(costs, size) {
|
|
104
|
+
const slices = [];
|
|
105
|
+
for (const id of costs.keys()) {
|
|
106
|
+
const cost = costs.get(id) ?? 0;
|
|
107
|
+
const last = slices.at(-1);
|
|
108
|
+
if (last && last.used + cost <= size) {
|
|
109
|
+
last.ids.push(id);
|
|
110
|
+
last.used += cost;
|
|
111
|
+
} else slices.push({
|
|
112
|
+
ids: [id],
|
|
113
|
+
used: cost
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return slices.map((slice) => slice.ids);
|
|
117
|
+
}
|
|
118
|
+
/** The work restricted to some stories; engines left with nothing to render are dropped. */
|
|
119
|
+
function restrictTo(work, ids) {
|
|
120
|
+
return work.map((engine) => ({
|
|
121
|
+
browser: engine.browser,
|
|
122
|
+
stories: engine.stories.filter((story) => ids.has(story.id))
|
|
123
|
+
})).filter((engine) => engine.stories.length > 0);
|
|
124
|
+
}
|
|
125
|
+
export { planShards, totalCaptures, worthSharding };
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=shard-plan.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shard-plan.mjs","names":[],"sources":["../../src/capture/shard-plan.ts"],"sourcesContent":["/** Splitting a build's render work into shards (ADR 0005 phase 5). Pure: no I/O, no clock. */\nimport type { CaptureTarget } from \"../adapters/capture-runner.ts\";\nimport type { StoryEntry, Viewport } from \"./adapter.ts\";\n\n/** What one engine has to render this build (after affected-capture partitioning). */\nexport interface EngineWork {\n browser: CaptureTarget;\n stories: StoryEntry[];\n}\n\n/** One render shard: a slice of stories, with the engines each of them is rendered on. */\nexport interface ShardPayload {\n /** Engines of the slice, primary first; each lists only the stories it renders. */\n browsers: EngineWork[];\n /** Viewports the stories resolve against. */\n viewports: Viewport[];\n /** Engines with no baseline yet when the plan was made (fixed at plan time, not per shard). */\n fresh: string[];\n}\n\n/** Settings of the plan itself, kept on the plan row. */\nexport interface PlanPayload {\n browsers: string[];\n fresh: string[];\n /** Captures per shard the plan aimed for. */\n shardSize: number;\n /** Captures (story x viewport x engine) the plan covers. */\n total: number;\n}\n\n/** Outcome of one render shard, kept on its row. */\nexport interface ShardResult {\n failedStoryIds: string[];\n flakyStoryIds: string[];\n a11yStoryIds: string[];\n captured: number;\n durationMs: number;\n}\n\n/** Outcome of the plan stage: every story of the build, and stories whose inherited snapshot failed. */\nexport interface PlanResult {\n storyIds: string[];\n failedStoryIds: string[];\n}\n\n/**\n * Split the work into shards of about `size` captures, keeping a story's engines together so\n * a newly enabled engine can still bootstrap from its counterpart in the same shard.\n *\n * Returns `[]` when sharding is off or the build is too small to be worth it (up to twice the\n * shard size runs as one shard, exactly as before).\n *\n * Without timings the slices are contiguous runs of stories in their original order. Given\n * `weightOf`, the stories are packed longest-first into shards of equal capture capacity and the\n * shards come back heaviest first, so the slow work starts early and no shard straggles.\n *\n * @param work - Per-engine render lists, primary engine first.\n * @param capturesOf - Captures one story produces on one engine (its viewport count).\n * @param size - Target captures per shard; undefined or below 1 turns sharding off.\n * @param weightOf - What a story cost last time, in milliseconds; unknown for a story returns undefined.\n */\nexport function planShards(\n work: readonly EngineWork[],\n capturesOf: (story: StoryEntry) => number,\n size: number | undefined,\n weightOf?: (storyId: string) => number | undefined,\n): EngineWork[][] {\n if (!size || !worthSharding(work, capturesOf, size)) {\n return [];\n }\n const costs = storyCosts(work, capturesOf);\n const weights = weightOf ? weighted(costs, weightOf) : undefined;\n const slices = weights ? packLongestFirst(costs, weights, size) : sliceByCost(costs, size);\n return slices.map((ids) => restrictTo(work, new Set(ids)));\n}\n\n/**\n * Whether {@link planShards} would split this work at all: sharding is on (`size` of at least 1)\n * and the work is more than twice the shard size. Lets the plan stage skip work only an ordering needs.\n */\nexport function worthSharding(\n work: readonly EngineWork[],\n capturesOf: (story: StoryEntry) => number,\n size: number | undefined,\n): boolean {\n return size !== undefined && size >= 1 && totalCaptures(work, capturesOf) > size * 2;\n}\n\n/** Total captures the work produces. */\nexport function totalCaptures(\n work: readonly EngineWork[],\n capturesOf: (story: StoryEntry) => number,\n): number {\n return [...storyCosts(work, capturesOf).values()].reduce((sum, cost) => sum + cost, 0);\n}\n\n/** Captures per story id, summed over the engines that render it, in first-seen order. */\nfunction storyCosts(\n work: readonly EngineWork[],\n capturesOf: (story: StoryEntry) => number,\n): Map<string, number> {\n const costs = new Map<string, number>();\n for (const engine of work) {\n for (const story of engine.stories) {\n costs.set(story.id, (costs.get(story.id) ?? 0) + capturesOf(story));\n }\n }\n return costs;\n}\n\n/**\n * What each story is expected to cost in milliseconds. A story nothing is known about is priced at\n * the average cost of one capture, so a new story is treated as ordinary rather than free.\n */\nfunction weighted(\n costs: Map<string, number>,\n weightOf: (storyId: string) => number | undefined,\n): Map<string, number> | undefined {\n const known = [...costs.keys()].flatMap((id) => {\n const weight = weightOf(id);\n return typeof weight === \"number\" && weight > 0 ? [[id, weight] as const] : [];\n });\n if (known.length === 0) {\n return undefined;\n }\n const knownCaptures = known.reduce((sum, [id]) => sum + (costs.get(id) ?? 0), 0);\n const perCapture =\n known.reduce((sum, [, weight]) => sum + weight, 0) / Math.max(knownCaptures, 1);\n const weights = new Map(known);\n for (const [id, captures] of costs) {\n if (!weights.has(id)) {\n weights.set(id, perCapture * captures);\n }\n }\n return weights;\n}\n\n/**\n * Longest-processing-time packing: the dearest stories are placed first, each into the shard that\n * has the least work so far and still has room for its captures. Shards come back heaviest first.\n */\nfunction packLongestFirst(\n costs: Map<string, number>,\n weights: Map<string, number>,\n size: number,\n): string[][] {\n const count = Math.max(1, Math.ceil(sumOf(costs) / size));\n const bins: Bin[] = Array.from({ length: count }, newBin);\n for (const id of byWeightDescending(costs, weights)) {\n const captures = costs.get(id) ?? 0;\n const bin = binFor(bins, captures, size);\n bin.ids.push(id);\n bin.captures += captures;\n bin.weight += weights.get(id) ?? 0;\n }\n return bins\n .filter((bin) => bin.ids.length > 0)\n .toSorted((left, right) => right.weight - left.weight)\n .map((bin) => bin.ids);\n}\n\n/** Story ids dearest first; equal cost keeps the order they were planned in. */\nfunction byWeightDescending(costs: Map<string, number>, weights: Map<string, number>): string[] {\n return [...costs.keys()].toSorted(\n (left, right) => (weights.get(right) ?? 0) - (weights.get(left) ?? 0),\n );\n}\n\n/** One shard being filled: the stories in it, their captures and what they are expected to cost. */\ninterface Bin {\n ids: string[];\n captures: number;\n weight: number;\n}\n\nfunction newBin(): Bin {\n return { ids: [], captures: 0, weight: 0 };\n}\n\n/**\n * The shard with the least work that still has room for `captures`. When none has room the last\n * stories did not fit anywhere, so a shard is added: one more beats overfilling one, just as an\n * oversized story gets a shard of its own when there are no timings.\n */\nfunction binFor(bins: Bin[], captures: number, size: number): Bin {\n const withRoom = bins.filter((bin) => bin.captures + captures <= size);\n const [lightest] = withRoom.toSorted((left, right) => left.weight - right.weight);\n if (lightest) {\n return lightest;\n }\n const fresh = newBin();\n bins.push(fresh);\n return fresh;\n}\n\nfunction sumOf(costs: Map<string, number>): number {\n return [...costs.values()].reduce((sum, cost) => sum + cost, 0);\n}\n\n/** Contiguous runs of ids whose cost stays within `size` (a single oversized story gets its own). */\nfunction sliceByCost(costs: Map<string, number>, size: number): string[][] {\n const slices: { ids: string[]; used: number }[] = [];\n for (const id of costs.keys()) {\n const cost = costs.get(id) ?? 0;\n const last = slices.at(-1);\n if (last && last.used + cost <= size) {\n last.ids.push(id);\n last.used += cost;\n } else {\n slices.push({ ids: [id], used: cost });\n }\n }\n return slices.map((slice) => slice.ids);\n}\n\n/** The work restricted to some stories; engines left with nothing to render are dropped. */\nfunction restrictTo(work: readonly EngineWork[], ids: ReadonlySet<string>): EngineWork[] {\n return work\n .map((engine) => ({\n browser: engine.browser,\n stories: engine.stories.filter((story) => ids.has(story.id)),\n }))\n .filter((engine) => engine.stories.length > 0);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { storybookUrlSource } from "../storybook-urls.mjs";
|
|
5
|
+
// src/capture/shards/context.ts
|
|
6
|
+
/** A capture context for the persist and finalize steps of one stage. */
|
|
7
|
+
function contextOf(target, parts) {
|
|
8
|
+
const { options } = target;
|
|
9
|
+
return {
|
|
10
|
+
db: options.db,
|
|
11
|
+
tables: options.tables,
|
|
12
|
+
storage: options.storage,
|
|
13
|
+
project: target.project,
|
|
14
|
+
build: target.build,
|
|
15
|
+
viewports: parts.viewports,
|
|
16
|
+
browsers: parts.browsers,
|
|
17
|
+
captures: parts.captures ?? [],
|
|
18
|
+
inherited: parts.inherited ?? [],
|
|
19
|
+
...parts.freshBrowsers ? { freshBrowsers: parts.freshBrowsers } : {},
|
|
20
|
+
progress: parts.progress,
|
|
21
|
+
...options.logger ? { logger: options.logger } : {},
|
|
22
|
+
...options.secret ? { secret: options.secret } : {}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** The matrix input for rendering `stories` from `storybookDir` on `browsers`. */
|
|
26
|
+
function matrixInputOf(target, parts) {
|
|
27
|
+
const { options } = target;
|
|
28
|
+
return {
|
|
29
|
+
db: options.db,
|
|
30
|
+
storage: options.storage,
|
|
31
|
+
tables: options.tables,
|
|
32
|
+
runner: options.runner,
|
|
33
|
+
build: target.build,
|
|
34
|
+
project: target.project,
|
|
35
|
+
stories: parts.stories,
|
|
36
|
+
viewports: parts.viewports,
|
|
37
|
+
storybookDir: parts.storybookDir,
|
|
38
|
+
browsers: parts.browsers,
|
|
39
|
+
affectedPaths: parts.affectedPaths,
|
|
40
|
+
renderTimeoutMs: options.renderTimeoutMs,
|
|
41
|
+
storybookUrl: storybookUrlSource(options, target),
|
|
42
|
+
logger: parts.logger ?? options.logger,
|
|
43
|
+
recordLog: parts.recordLog,
|
|
44
|
+
progress: parts.progress,
|
|
45
|
+
runId: parts.runId,
|
|
46
|
+
shardIndex: parts.shardIndex
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export { contextOf, matrixInputOf };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=context.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.mjs","names":[],"sources":["../../../src/capture/shards/context.ts"],"sourcesContent":["/** What every stage of a sharded capture works against. */\nimport type { Logger } from \"pino\";\nimport type { CaptureTarget } from \"../../adapters/capture-runner.ts\";\nimport type { Build } from \"../../schema/build.ts\";\nimport type { Project } from \"../../schema/project.ts\";\nimport type { Viewport } from \"../adapter.ts\";\nimport type { MatrixInput } from \"../matrix.ts\";\nimport type { CaptureJobOptions } from \"../orchestrator.ts\";\nimport type { CaptureContext } from \"../pipeline.ts\";\nimport type { CaptureProgress } from \"../progress.ts\";\nimport { storybookUrlSource } from \"../storybook-urls.ts\";\n\n/** The build being captured and the adapters to capture it with. */\nexport interface ShardTarget {\n options: CaptureJobOptions;\n build: Build;\n project: Project;\n}\n\n/** A capture context for the persist and finalize steps of one stage. */\nexport function contextOf(\n target: ShardTarget,\n parts: {\n viewports: Viewport[];\n browsers: CaptureTarget[];\n captures?: CaptureContext[\"captures\"];\n inherited?: NonNullable<CaptureContext[\"inherited\"]>;\n freshBrowsers?: ReadonlySet<string>;\n progress?: CaptureProgress | undefined;\n },\n): CaptureContext {\n const { options } = target;\n return {\n db: options.db,\n tables: options.tables,\n storage: options.storage,\n project: target.project,\n build: target.build,\n viewports: parts.viewports,\n browsers: parts.browsers,\n captures: parts.captures ?? [],\n inherited: parts.inherited ?? [],\n ...(parts.freshBrowsers ? { freshBrowsers: parts.freshBrowsers } : {}),\n progress: parts.progress,\n ...(options.logger ? { logger: options.logger } : {}),\n ...(options.secret ? { secret: options.secret } : {}),\n };\n}\n\n/** The matrix input for rendering `stories` from `storybookDir` on `browsers`. */\nexport function matrixInputOf(\n target: ShardTarget,\n parts: {\n stories: MatrixInput[\"stories\"];\n viewports: Viewport[];\n browsers: CaptureTarget[];\n storybookDir: string;\n affectedPaths: string[] | null;\n logger?: Logger | undefined;\n recordLog?: MatrixInput[\"recordLog\"];\n progress?: CaptureProgress | undefined;\n runId?: string | undefined;\n shardIndex?: number | undefined;\n },\n): MatrixInput {\n const { options } = target;\n return {\n db: options.db,\n storage: options.storage,\n tables: options.tables,\n runner: options.runner,\n build: target.build,\n project: target.project,\n stories: parts.stories,\n viewports: parts.viewports,\n storybookDir: parts.storybookDir,\n browsers: parts.browsers,\n affectedPaths: parts.affectedPaths,\n renderTimeoutMs: options.renderTimeoutMs,\n storybookUrl: storybookUrlSource(options, target),\n logger: parts.logger ?? options.logger,\n recordLog: parts.recordLog,\n progress: parts.progress,\n runId: parts.runId,\n shardIndex: parts.shardIndex,\n };\n}\n"],"mappings":";;;;;;AAoBA,SAAgB,UACd,QACA,OAQgB;CAChB,MAAM,EAAE,YAAY;CACpB,OAAO;EACL,IAAI,QAAQ;EACZ,QAAQ,QAAQ;EAChB,SAAS,QAAQ;EACjB,SAAS,OAAO;EAChB,OAAO,OAAO;EACd,WAAW,MAAM;EACjB,UAAU,MAAM;EAChB,UAAU,MAAM,YAAY,CAAC;EAC7B,WAAW,MAAM,aAAa,CAAC;EAC/B,GAAI,MAAM,gBAAgB,EAAE,eAAe,MAAM,cAAc,IAAI,CAAC;EACpE,UAAU,MAAM;EAChB,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;EACnD,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;CACrD;AACF;;AAGA,SAAgB,cACd,QACA,OAYa;CACb,MAAM,EAAE,YAAY;CACpB,OAAO;EACL,IAAI,QAAQ;EACZ,SAAS,QAAQ;EACjB,QAAQ,QAAQ;EAChB,QAAQ,QAAQ;EAChB,OAAO,OAAO;EACd,SAAS,OAAO;EAChB,SAAS,MAAM;EACf,WAAW,MAAM;EACjB,cAAc,MAAM;EACpB,UAAU,MAAM;EAChB,eAAe,MAAM;EACrB,iBAAiB,QAAQ;EACzB,cAAc,mBAAmB,SAAS,MAAM;EAChD,QAAQ,MAAM,UAAU,QAAQ;EAChC,WAAW,MAAM;EACjB,UAAU,MAAM;EAChB,OAAO,MAAM;EACb,YAAY,MAAM;CACpB;AACF"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { classifyFailures } from "../failure-sets.mjs";
|
|
5
|
+
import { renderWork } from "../matrix.mjs";
|
|
6
|
+
import { SnapshotModel } from "../../models/snapshot.mjs";
|
|
7
|
+
import { persistRendered } from "../pipeline.mjs";
|
|
8
|
+
import { removeShardScratch } from "../scratch.mjs";
|
|
9
|
+
import { CaptureShardModel } from "../../models/capture-shard.mjs";
|
|
10
|
+
import { contextOf, matrixInputOf } from "./context.mjs";
|
|
11
|
+
import { extractStorybookToScratch } from "../statics.mjs";
|
|
12
|
+
import { loadTarget } from "../target.mjs";
|
|
13
|
+
import { reduceBuild } from "./reduce.mjs";
|
|
14
|
+
// src/capture/shards/execute.ts
|
|
15
|
+
/** The render stage: one shard of a sharded capture, run by whichever worker picked up its job. */
|
|
16
|
+
/**
|
|
17
|
+
* Render and persist one shard, record its outcome, and, if it was the last one, finalize the build.
|
|
18
|
+
*
|
|
19
|
+
* Safe to run twice: a shard already done is not rendered again, and a rerun of an interrupted one
|
|
20
|
+
* starts from a clean slate for its stories. A redelivered shard that is already done still tries
|
|
21
|
+
* to finalize the build, in case the run that finished it died or failed before it could.
|
|
22
|
+
*/
|
|
23
|
+
async function executeCaptureShard(input, options) {
|
|
24
|
+
const picked = await pickShard(input, options);
|
|
25
|
+
if ("status" in picked) return picked;
|
|
26
|
+
const final = input.final !== false;
|
|
27
|
+
if (picked.shard.status === "done") return await reduceFinished(picked.target, input, final);
|
|
28
|
+
await runShard(picked.target, picked.shard, input, final);
|
|
29
|
+
return await reduceRendered(picked.target, input, final);
|
|
30
|
+
}
|
|
31
|
+
/** The shard is done: if it was the last one, this run finalizes the build. */
|
|
32
|
+
async function reduceRendered(target, input, final) {
|
|
33
|
+
const summary = await reduceBuild(target, final);
|
|
34
|
+
if (!summary) return {
|
|
35
|
+
status: "rendered",
|
|
36
|
+
finalized: false
|
|
37
|
+
};
|
|
38
|
+
await input.onFinalized?.(summary);
|
|
39
|
+
return {
|
|
40
|
+
status: "rendered",
|
|
41
|
+
finalized: true,
|
|
42
|
+
summary
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** A redelivered shard that an earlier run finished: finalize the build if nobody has. */
|
|
46
|
+
async function reduceFinished(target, input, final) {
|
|
47
|
+
const summary = await reduceBuild(target, final);
|
|
48
|
+
if (!summary) return {
|
|
49
|
+
status: "skipped",
|
|
50
|
+
reason: "shard already finished"
|
|
51
|
+
};
|
|
52
|
+
await input.onFinalized?.(summary);
|
|
53
|
+
return {
|
|
54
|
+
status: "reduced",
|
|
55
|
+
summary
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Run the shard; a failure is recorded on its row (and, if final, reduces the build) then rethrown. */
|
|
59
|
+
async function runShard(target, shard, input, final) {
|
|
60
|
+
const shards = new CaptureShardModel(target.options.db);
|
|
61
|
+
await shards.markRunning(shard.id);
|
|
62
|
+
input.progress?.();
|
|
63
|
+
try {
|
|
64
|
+
await renderShard(target, shard, input.progress);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
await recordFailure(shards, shard, error, final);
|
|
67
|
+
if (final) await finalizeAfterFailure(target, input, error);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The shard failed for good: if it was the last one open, it ends the build, and only then does
|
|
73
|
+
* this run report it. A failure here is logged, not thrown: the shard's own error is the news.
|
|
74
|
+
*/
|
|
75
|
+
async function finalizeAfterFailure(target, input, error) {
|
|
76
|
+
try {
|
|
77
|
+
const summary = await reduceBuild(target, true);
|
|
78
|
+
if (summary) await input.onFinalized?.(summary, error);
|
|
79
|
+
} catch (reduceError) {
|
|
80
|
+
target.options.logger?.error({
|
|
81
|
+
err: reduceError,
|
|
82
|
+
buildId: target.build.id
|
|
83
|
+
}, "finalizing after a failed shard failed");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The shard to run and what to run it against, or why there is nothing to do. */
|
|
87
|
+
async function pickShard(input, options) {
|
|
88
|
+
const shard = await new CaptureShardModel(options.db).get(input.shard);
|
|
89
|
+
if (shard?.kind !== "render") return {
|
|
90
|
+
status: "skipped",
|
|
91
|
+
reason: "shard no longer exists"
|
|
92
|
+
};
|
|
93
|
+
const { build, project } = await loadTarget(options, input.buildId);
|
|
94
|
+
if (build.status !== "capturing") return {
|
|
95
|
+
status: "skipped",
|
|
96
|
+
reason: shard.status === "done" ? "shard already finished" : `build is ${build.status}`
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
target: {
|
|
100
|
+
options,
|
|
101
|
+
build,
|
|
102
|
+
project
|
|
103
|
+
},
|
|
104
|
+
shard
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function recordFailure(shards, shard, error, final) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
await (final ? shards.fail(shard.id, message) : shards.retry(shard.id, message));
|
|
110
|
+
}
|
|
111
|
+
async function renderShard(target, shard, progress) {
|
|
112
|
+
const { options, build, project } = target;
|
|
113
|
+
const payload = JSON.parse(shard.payload ?? "{}");
|
|
114
|
+
const started = performance.now();
|
|
115
|
+
const storybookDir = await extractStorybookToScratch(options.storage, options.scratchDir, project.id, build.id, shard.id, options.limits);
|
|
116
|
+
progress?.();
|
|
117
|
+
try {
|
|
118
|
+
const result = await renderAndPersist(target, payload, {
|
|
119
|
+
storybookDir,
|
|
120
|
+
runId: shard.id,
|
|
121
|
+
shardIndex: shard.ordinal,
|
|
122
|
+
progress
|
|
123
|
+
});
|
|
124
|
+
const durationMs = Math.round(performance.now() - started);
|
|
125
|
+
await new CaptureShardModel(options.db).complete(shard.id, JSON.stringify({
|
|
126
|
+
...result,
|
|
127
|
+
durationMs
|
|
128
|
+
}));
|
|
129
|
+
} finally {
|
|
130
|
+
await removeShardScratch(options.scratchDir, project.id, build.id, shard.id, options.logger);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function renderAndPersist(target, payload, run) {
|
|
134
|
+
const stories = payload.browsers.flatMap((engine) => engine.stories);
|
|
135
|
+
const browsers = payload.browsers.map((engine) => engine.browser);
|
|
136
|
+
const rendered = await renderWork(matrixInputOf(target, {
|
|
137
|
+
stories,
|
|
138
|
+
viewports: payload.viewports,
|
|
139
|
+
browsers,
|
|
140
|
+
storybookDir: run.storybookDir,
|
|
141
|
+
affectedPaths: null,
|
|
142
|
+
progress: run.progress,
|
|
143
|
+
runId: run.runId,
|
|
144
|
+
shardIndex: run.shardIndex
|
|
145
|
+
}), payload.browsers);
|
|
146
|
+
const failures = classifyFailures(stories, rendered.failures);
|
|
147
|
+
await clearEarlierRun(target, payload);
|
|
148
|
+
const persisted = await persistRendered(contextOf(target, {
|
|
149
|
+
viewports: payload.viewports,
|
|
150
|
+
browsers,
|
|
151
|
+
captures: rendered.captures,
|
|
152
|
+
freshBrowsers: new Set(payload.fresh),
|
|
153
|
+
progress: run.progress
|
|
154
|
+
}), failures.blocking, failures.flaky, failures.a11y);
|
|
155
|
+
return {
|
|
156
|
+
failedStoryIds: [...persisted.failed],
|
|
157
|
+
flakyStoryIds: [...persisted.flaky],
|
|
158
|
+
a11yStoryIds: [...persisted.a11y],
|
|
159
|
+
captured: rendered.captures.length
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Drop what an interrupted earlier run of this shard persisted: the rendered rows of the
|
|
164
|
+
* (story, engine) pairs it is about to write. The plan's inherited rows stay, including those of
|
|
165
|
+
* these stories on engines or viewports this shard does not render.
|
|
166
|
+
*/
|
|
167
|
+
async function clearEarlierRun(target, payload) {
|
|
168
|
+
await new SnapshotModel(target.options.db, target.options.tables).removeRendered(target.build.id, payload.browsers.map((engine) => ({
|
|
169
|
+
browser: engine.browser,
|
|
170
|
+
storyIds: new Set(engine.stories.map((story) => story.id))
|
|
171
|
+
})));
|
|
172
|
+
}
|
|
173
|
+
export { executeCaptureShard };
|
|
174
|
+
|
|
175
|
+
//# sourceMappingURL=execute.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.mjs","names":[],"sources":["../../../src/capture/shards/execute.ts"],"sourcesContent":["/** The render stage: one shard of a sharded capture, run by whichever worker picked up its job. */\nimport { CaptureShardModel } from \"../../models/capture-shard.ts\";\nimport { SnapshotModel } from \"../../models/snapshot.ts\";\nimport type { CaptureShard } from \"../../schema/capture-shard.ts\";\nimport { classifyFailures } from \"../failure-sets.ts\";\nimport { renderWork } from \"../matrix.ts\";\nimport type { CaptureJobOptions } from \"../orchestrator.ts\";\nimport { persistRendered } from \"../pipeline.ts\";\nimport type { CaptureProgress } from \"../progress.ts\";\nimport { removeShardScratch } from \"../scratch.ts\";\nimport type { ShardPayload, ShardResult } from \"../shard-plan.ts\";\nimport { extractStorybookToScratch } from \"../statics.ts\";\nimport { loadTarget } from \"../target.ts\";\nimport { contextOf, matrixInputOf, type ShardTarget } from \"./context.ts\";\nimport { reduceBuild, type FinalSummary } from \"./reduce.ts\";\n\n/** A shard job being run. */\nexport interface ShardRunInput {\n buildId: string;\n shard: string;\n reqId?: string | undefined;\n /**\n * Whether a failure now is the last word. A queue that redelivers failed jobs passes false until\n * its retries are used up, so a shard that will be retried does not end the build.\n */\n final?: boolean | undefined;\n /** Reports render and persist progress to the job's lease (see `leaseProgress`). */\n progress?: CaptureProgress | undefined;\n /**\n * Called once by the run that finalized the build, and by no other, with the summary and, when\n * the shard that ended the build failed, its error. The caller closes the plan's attempt here.\n */\n onFinalized?: ((summary: FinalSummary, error?: unknown) => Promise<void>) | undefined;\n}\n\n/** How a shard run ended. */\nexport type ShardOutcome =\n | { status: \"skipped\"; reason: string }\n | { status: \"rendered\"; finalized: false }\n | { status: \"rendered\"; finalized: true; summary: FinalSummary }\n /** The shard was already done, but the build was not: this run finalized it. */\n | { status: \"reduced\"; summary: FinalSummary };\n\n/**\n * Render and persist one shard, record its outcome, and, if it was the last one, finalize the build.\n *\n * Safe to run twice: a shard already done is not rendered again, and a rerun of an interrupted one\n * starts from a clean slate for its stories. A redelivered shard that is already done still tries\n * to finalize the build, in case the run that finished it died or failed before it could.\n */\nexport async function executeCaptureShard(\n input: ShardRunInput,\n options: CaptureJobOptions,\n): Promise<ShardOutcome> {\n const picked = await pickShard(input, options);\n if (\"status\" in picked) {\n return picked;\n }\n const final = input.final !== false;\n if (picked.shard.status === \"done\") {\n return await reduceFinished(picked.target, input, final);\n }\n await runShard(picked.target, picked.shard, input, final);\n return await reduceRendered(picked.target, input, final);\n}\n\n/** The shard is done: if it was the last one, this run finalizes the build. */\nasync function reduceRendered(\n target: ShardTarget,\n input: ShardRunInput,\n final: boolean,\n): Promise<ShardOutcome> {\n const summary = await reduceBuild(target, final);\n if (!summary) {\n return { status: \"rendered\", finalized: false };\n }\n await input.onFinalized?.(summary);\n return { status: \"rendered\", finalized: true, summary };\n}\n\n/** A redelivered shard that an earlier run finished: finalize the build if nobody has. */\nasync function reduceFinished(\n target: ShardTarget,\n input: ShardRunInput,\n final: boolean,\n): Promise<ShardOutcome> {\n const summary = await reduceBuild(target, final);\n if (!summary) {\n return { status: \"skipped\", reason: \"shard already finished\" };\n }\n await input.onFinalized?.(summary);\n return { status: \"reduced\", summary };\n}\n\n/** Run the shard; a failure is recorded on its row (and, if final, reduces the build) then rethrown. */\nasync function runShard(\n target: ShardTarget,\n shard: CaptureShard,\n input: ShardRunInput,\n final: boolean,\n): Promise<void> {\n const shards = new CaptureShardModel(target.options.db);\n await shards.markRunning(shard.id);\n input.progress?.();\n try {\n await renderShard(target, shard, input.progress);\n } catch (error) {\n await recordFailure(shards, shard, error, final);\n if (final) {\n await finalizeAfterFailure(target, input, error);\n }\n throw error;\n }\n}\n\n/**\n * The shard failed for good: if it was the last one open, it ends the build, and only then does\n * this run report it. A failure here is logged, not thrown: the shard's own error is the news.\n */\nasync function finalizeAfterFailure(\n target: ShardTarget,\n input: ShardRunInput,\n error: unknown,\n): Promise<void> {\n try {\n const summary = await reduceBuild(target, true);\n if (summary) {\n await input.onFinalized?.(summary, error);\n }\n } catch (reduceError) {\n target.options.logger?.error(\n { err: reduceError, buildId: target.build.id },\n \"finalizing after a failed shard failed\",\n );\n }\n}\n\n/** The shard to run and what to run it against, or why there is nothing to do. */\nasync function pickShard(\n input: ShardRunInput,\n options: CaptureJobOptions,\n): Promise<\n { target: ShardTarget; shard: CaptureShard } | Extract<ShardOutcome, { status: \"skipped\" }>\n> {\n const shard = await new CaptureShardModel(options.db).get(input.shard);\n if (shard?.kind !== \"render\") {\n return { status: \"skipped\", reason: \"shard no longer exists\" };\n }\n const { build, project } = await loadTarget(options, input.buildId);\n if (build.status !== \"capturing\") {\n const reason = shard.status === \"done\" ? \"shard already finished\" : `build is ${build.status}`;\n return { status: \"skipped\", reason };\n }\n return { target: { options, build, project }, shard };\n}\n\nasync function recordFailure(\n shards: CaptureShardModel,\n shard: CaptureShard,\n error: unknown,\n final: boolean,\n): Promise<void> {\n const message = error instanceof Error ? error.message : String(error);\n await (final ? shards.fail(shard.id, message) : shards.retry(shard.id, message));\n}\n\nasync function renderShard(\n target: ShardTarget,\n shard: CaptureShard,\n progress: CaptureProgress | undefined,\n): Promise<void> {\n const { options, build, project } = target;\n const payload = JSON.parse(shard.payload ?? \"{}\") as ShardPayload;\n const started = performance.now();\n const storybookDir = await extractStorybookToScratch(\n options.storage,\n options.scratchDir,\n project.id,\n build.id,\n shard.id,\n options.limits,\n );\n progress?.();\n try {\n const result = await renderAndPersist(target, payload, {\n storybookDir,\n runId: shard.id,\n shardIndex: shard.ordinal,\n progress,\n });\n const durationMs = Math.round(performance.now() - started);\n await new CaptureShardModel(options.db).complete(\n shard.id,\n JSON.stringify({ ...result, durationMs } satisfies ShardResult),\n );\n } finally {\n await removeShardScratch(options.scratchDir, project.id, build.id, shard.id, options.logger);\n }\n}\n\nasync function renderAndPersist(\n target: ShardTarget,\n payload: ShardPayload,\n run: {\n storybookDir: string;\n runId: string;\n shardIndex: number;\n progress: CaptureProgress | undefined;\n },\n): Promise<Omit<ShardResult, \"durationMs\">> {\n const stories = payload.browsers.flatMap((engine) => engine.stories);\n const browsers = payload.browsers.map((engine) => engine.browser);\n const rendered = await renderWork(\n matrixInputOf(target, {\n stories,\n viewports: payload.viewports,\n browsers,\n storybookDir: run.storybookDir,\n affectedPaths: null,\n progress: run.progress,\n runId: run.runId,\n shardIndex: run.shardIndex,\n }),\n payload.browsers,\n );\n const failures = classifyFailures(stories, rendered.failures);\n await clearEarlierRun(target, payload);\n const persisted = await persistRendered(\n contextOf(target, {\n viewports: payload.viewports,\n browsers,\n captures: rendered.captures,\n freshBrowsers: new Set(payload.fresh),\n progress: run.progress,\n }),\n failures.blocking,\n failures.flaky,\n failures.a11y,\n );\n return {\n failedStoryIds: [...persisted.failed],\n flakyStoryIds: [...persisted.flaky],\n a11yStoryIds: [...persisted.a11y],\n captured: rendered.captures.length,\n };\n}\n\n/**\n * Drop what an interrupted earlier run of this shard persisted: the rendered rows of the\n * (story, engine) pairs it is about to write. The plan's inherited rows stay, including those of\n * these stories on engines or viewports this shard does not render.\n */\nasync function clearEarlierRun(target: ShardTarget, payload: ShardPayload): Promise<void> {\n await new SnapshotModel(target.options.db, target.options.tables).removeRendered(\n target.build.id,\n payload.browsers.map((engine) => ({\n browser: engine.browser,\n storyIds: new Set(engine.stories.map((story) => story.id)),\n })),\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { CaptureAttemptModel, emitAttemptLog } from "../../models/capture-attempt.mjs";
|
|
5
|
+
import { resolveStoryViewports } from "../storybook.mjs";
|
|
6
|
+
import { affectedPathsFor } from "../affected.mjs";
|
|
7
|
+
import { partitionBrowsers } from "../matrix.mjs";
|
|
8
|
+
import { SnapshotModel } from "../../models/snapshot.mjs";
|
|
9
|
+
import { freshBrowsersOf, persistInherited } from "../pipeline.mjs";
|
|
10
|
+
import { CaptureShardModel } from "../../models/capture-shard.mjs";
|
|
11
|
+
import { planShards, worthSharding } from "../shard-plan.mjs";
|
|
12
|
+
import { contextOf, matrixInputOf } from "./context.mjs";
|
|
13
|
+
import { storyDurations } from "./history.mjs";
|
|
14
|
+
// src/capture/shards/fan-out.ts
|
|
15
|
+
/**
|
|
16
|
+
* The plan stage. When sharding is on and the build is big enough, decide what to render, record
|
|
17
|
+
* the plan and its shards, and hand the shards to the queue; otherwise return null and the caller
|
|
18
|
+
* renders the build in one piece as it always has.
|
|
19
|
+
*
|
|
20
|
+
* Inherited snapshots (affected capture) are written here, since nothing renders for them.
|
|
21
|
+
*/
|
|
22
|
+
async function fanOutCapture(args) {
|
|
23
|
+
const { options } = args;
|
|
24
|
+
if (!options.shardSize || !options.enqueue) return null;
|
|
25
|
+
const matrix = matrixInputOf(args, {
|
|
26
|
+
stories: args.stories,
|
|
27
|
+
viewports: args.viewports,
|
|
28
|
+
browsers: args.browsers,
|
|
29
|
+
storybookDir: "",
|
|
30
|
+
affectedPaths: affectedPathsFor(args.build),
|
|
31
|
+
logger: args.logger,
|
|
32
|
+
recordLog: args.input.recordLog,
|
|
33
|
+
progress: args.input.progress
|
|
34
|
+
});
|
|
35
|
+
const partition = await partitionBrowsers(matrix);
|
|
36
|
+
const capturesOf = (story) => resolveStoryViewports(story, args.viewports).length;
|
|
37
|
+
return worthSharding(partition.work, capturesOf, options.shardSize) ? await planAndCommit(args, partition, capturesOf) : null;
|
|
38
|
+
}
|
|
39
|
+
async function planAndCommit(args, partition, capturesOf) {
|
|
40
|
+
const durations = await storyDurations(args);
|
|
41
|
+
const slices = planShards(partition.work, capturesOf, args.options.shardSize, (storyId) => durations.get(storyId));
|
|
42
|
+
return slices.length === 0 ? null : await commitPlan({
|
|
43
|
+
...args,
|
|
44
|
+
timed: durations.size > 0
|
|
45
|
+
}, slices, partition);
|
|
46
|
+
}
|
|
47
|
+
async function commitPlan(args, slices, partition) {
|
|
48
|
+
await supersedeEarlierRuns(args);
|
|
49
|
+
const ctx = contextOf(args, {
|
|
50
|
+
viewports: args.viewports,
|
|
51
|
+
browsers: args.browsers,
|
|
52
|
+
inherited: partition.inherited,
|
|
53
|
+
progress: args.input.progress
|
|
54
|
+
});
|
|
55
|
+
const failedInherits = await persistInherited(ctx);
|
|
56
|
+
const storyIds = /* @__PURE__ */ new Set([...partition.work.flatMap((engine) => engine.stories.map((story) => story.id)), ...partition.inherited.map((inherited) => inherited.story.id)]);
|
|
57
|
+
const fresh = [...await freshBrowsersOf(ctx)];
|
|
58
|
+
const failed = /* @__PURE__ */ new Set([...failedInherits, ...args.testFailed ?? []]);
|
|
59
|
+
const rows = await recordPlan(args, slices, {
|
|
60
|
+
fresh,
|
|
61
|
+
storyIds,
|
|
62
|
+
failed
|
|
63
|
+
});
|
|
64
|
+
await handOut(args, rows);
|
|
65
|
+
await emitAttemptLog(args.input.recordLog, args.logger, "info", "capture planned", {
|
|
66
|
+
shards: rows.length,
|
|
67
|
+
stories: storyIds.size,
|
|
68
|
+
ordering: args.timed ? "longest-first" : "by-capture-count"
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
storyCount: storyIds.size,
|
|
72
|
+
failedCount: failed.size,
|
|
73
|
+
shards: rows.length
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A new plan of a build (recovery requeue, retry, a redelivered plan job) starts clean: the
|
|
78
|
+
* snapshots an earlier plan or its shards wrote are dropped, so inherited rows are not doubled,
|
|
79
|
+
* and the attempts it left open are closed, since only the newest plan's attempt is ever closed.
|
|
80
|
+
*/
|
|
81
|
+
async function supersedeEarlierRuns(args) {
|
|
82
|
+
const { db, tables } = args.options;
|
|
83
|
+
await new SnapshotModel(db, tables).removeForBuild(args.build.id);
|
|
84
|
+
await new CaptureAttemptModel(db, tables).closeOthers(args.build.id, args.input.attempt?.id ?? null, "superseded by a new capture plan");
|
|
85
|
+
}
|
|
86
|
+
async function recordPlan(args, slices, plan) {
|
|
87
|
+
const shards = new CaptureShardModel(args.options.db);
|
|
88
|
+
await shards.clearBuild(args.build.id);
|
|
89
|
+
const settings = {
|
|
90
|
+
browsers: args.browsers,
|
|
91
|
+
fresh: plan.fresh,
|
|
92
|
+
shardSize: args.options.shardSize ?? 0,
|
|
93
|
+
total: slices.length
|
|
94
|
+
};
|
|
95
|
+
const result = {
|
|
96
|
+
storyIds: [...plan.storyIds],
|
|
97
|
+
failedStoryIds: [...plan.failed]
|
|
98
|
+
};
|
|
99
|
+
return await shards.createPlan(args.project.id, args.build.id, {
|
|
100
|
+
attemptId: args.input.attempt?.id ?? null,
|
|
101
|
+
payload: JSON.stringify(settings),
|
|
102
|
+
result: JSON.stringify(result)
|
|
103
|
+
}, slices.map((browsers) => ({ payload: shardPayload(args, browsers, plan.fresh) })));
|
|
104
|
+
}
|
|
105
|
+
function shardPayload(args, browsers, fresh) {
|
|
106
|
+
const payload = {
|
|
107
|
+
browsers,
|
|
108
|
+
viewports: args.viewports,
|
|
109
|
+
fresh
|
|
110
|
+
};
|
|
111
|
+
return JSON.stringify(payload);
|
|
112
|
+
}
|
|
113
|
+
/** Enqueue every shard in order; the caller marks the build failed if the queue refuses one. */
|
|
114
|
+
async function handOut(args, rows) {
|
|
115
|
+
const { enqueue } = args.options;
|
|
116
|
+
for (const row of rows) await enqueue?.({
|
|
117
|
+
buildId: args.build.id,
|
|
118
|
+
shard: row.id,
|
|
119
|
+
reqId: args.input.reqId,
|
|
120
|
+
traceparent: args.input.traceparent
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
export { fanOutCapture };
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=fan-out.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fan-out.mjs","names":[],"sources":["../../../src/capture/shards/fan-out.ts"],"sourcesContent":["/** The plan stage: decide what to render, record it as shards, and hand the shards to the queue. */\nimport type { Logger } from \"pino\";\nimport type { CaptureTarget } from \"../../adapters/capture-runner.ts\";\nimport { CaptureAttemptModel, emitAttemptLog } from \"../../models/capture-attempt.ts\";\nimport { CaptureShardModel } from \"../../models/capture-shard.ts\";\nimport { SnapshotModel } from \"../../models/snapshot.ts\";\nimport type { CaptureShard } from \"../../schema/capture-shard.ts\";\nimport type { Viewport } from \"../adapter.ts\";\nimport { affectedPathsFor, type InheritedStory } from \"../affected.ts\";\nimport { partitionBrowsers, type MatrixInput } from \"../matrix.ts\";\nimport type { CaptureJobInput, CaptureSummary } from \"../orchestrator.ts\";\nimport { freshBrowsersOf, persistInherited } from \"../pipeline.ts\";\nimport {\n planShards,\n worthSharding,\n type EngineWork,\n type PlanPayload,\n type PlanResult,\n type ShardPayload,\n} from \"../shard-plan.ts\";\nimport { resolveStoryViewports } from \"../storybook.ts\";\nimport { contextOf, matrixInputOf, type ShardTarget } from \"./context.ts\";\nimport { storyDurations } from \"./history.ts\";\n\n/** Inputs for {@link fanOutCapture}: the plan stage's view of the build. */\nexport interface FanOutInput extends ShardTarget {\n input: CaptureJobInput;\n stories: MatrixInput[\"stories\"];\n viewports: Viewport[];\n browsers: CaptureTarget[];\n logger: Logger | undefined;\n /** Test entries that failed in the plan stage (ADR 0004, decision 10): they fail the build. */\n testFailed?: ReadonlySet<string> | undefined;\n /** Set by the plan stage: whether past timings were available to order the shards by. */\n timed?: boolean;\n}\n\n/**\n * The plan stage. When sharding is on and the build is big enough, decide what to render, record\n * the plan and its shards, and hand the shards to the queue; otherwise return null and the caller\n * renders the build in one piece as it always has.\n *\n * Inherited snapshots (affected capture) are written here, since nothing renders for them.\n */\nexport async function fanOutCapture(args: FanOutInput): Promise<CaptureSummary | null> {\n const { options } = args;\n if (!options.shardSize || !options.enqueue) {\n return null;\n }\n const matrix = matrixInputOf(args, {\n stories: args.stories,\n viewports: args.viewports,\n browsers: args.browsers,\n storybookDir: \"\",\n affectedPaths: affectedPathsFor(args.build),\n logger: args.logger,\n recordLog: args.input.recordLog,\n progress: args.input.progress,\n });\n const partition = await partitionBrowsers(matrix);\n const capturesOf = (story: MatrixInput[\"stories\"][number]): number =>\n resolveStoryViewports(story, args.viewports).length;\n // Past timings only order the shards: not worth a lookup for a build that runs in one piece.\n return worthSharding(partition.work, capturesOf, options.shardSize)\n ? await planAndCommit(args, partition, capturesOf)\n : null;\n}\n\nasync function planAndCommit(\n args: FanOutInput,\n partition: { work: EngineWork[]; inherited: InheritedStory[] },\n capturesOf: (story: MatrixInput[\"stories\"][number]) => number,\n): Promise<CaptureSummary | null> {\n const durations = await storyDurations(args);\n const slices = planShards(partition.work, capturesOf, args.options.shardSize, (storyId) =>\n durations.get(storyId),\n );\n return slices.length === 0\n ? null\n : await commitPlan({ ...args, timed: durations.size > 0 }, slices, partition);\n}\n\nasync function commitPlan(\n args: FanOutInput,\n slices: EngineWork[][],\n partition: { work: EngineWork[]; inherited: InheritedStory[] },\n): Promise<CaptureSummary> {\n await supersedeEarlierRuns(args);\n const ctx = contextOf(args, {\n viewports: args.viewports,\n browsers: args.browsers,\n inherited: partition.inherited,\n progress: args.input.progress,\n });\n const failedInherits = await persistInherited(ctx);\n const storyIds = new Set([\n ...partition.work.flatMap((engine) => engine.stories.map((story) => story.id)),\n ...partition.inherited.map((inherited) => inherited.story.id),\n ]);\n const fresh = [...(await freshBrowsersOf(ctx))];\n const failed = new Set([...failedInherits, ...(args.testFailed ?? [])]);\n const rows = await recordPlan(args, slices, { fresh, storyIds, failed });\n await handOut(args, rows);\n await emitAttemptLog(args.input.recordLog, args.logger, \"info\", \"capture planned\", {\n shards: rows.length,\n stories: storyIds.size,\n ordering: args.timed ? \"longest-first\" : \"by-capture-count\",\n });\n return { storyCount: storyIds.size, failedCount: failed.size, shards: rows.length };\n}\n\n/**\n * A new plan of a build (recovery requeue, retry, a redelivered plan job) starts clean: the\n * snapshots an earlier plan or its shards wrote are dropped, so inherited rows are not doubled,\n * and the attempts it left open are closed, since only the newest plan's attempt is ever closed.\n */\nasync function supersedeEarlierRuns(args: FanOutInput): Promise<void> {\n const { db, tables } = args.options;\n await new SnapshotModel(db, tables).removeForBuild(args.build.id);\n await new CaptureAttemptModel(db, tables).closeOthers(\n args.build.id,\n args.input.attempt?.id ?? null,\n \"superseded by a new capture plan\",\n );\n}\n\nasync function recordPlan(\n args: FanOutInput,\n slices: EngineWork[][],\n plan: { fresh: string[]; storyIds: Set<string>; failed: ReadonlySet<string> },\n): Promise<CaptureShard[]> {\n const shards = new CaptureShardModel(args.options.db);\n await shards.clearBuild(args.build.id);\n const settings: PlanPayload = {\n browsers: args.browsers,\n fresh: plan.fresh,\n shardSize: args.options.shardSize ?? 0,\n total: slices.length,\n };\n const result: PlanResult = { storyIds: [...plan.storyIds], failedStoryIds: [...plan.failed] };\n return await shards.createPlan(\n args.project.id,\n args.build.id,\n {\n attemptId: args.input.attempt?.id ?? null,\n payload: JSON.stringify(settings),\n result: JSON.stringify(result),\n },\n slices.map((browsers) => ({ payload: shardPayload(args, browsers, plan.fresh) })),\n );\n}\n\nfunction shardPayload(args: FanOutInput, browsers: EngineWork[], fresh: string[]): string {\n const payload: ShardPayload = { browsers, viewports: args.viewports, fresh };\n return JSON.stringify(payload);\n}\n\n/** Enqueue every shard in order; the caller marks the build failed if the queue refuses one. */\nasync function handOut(args: FanOutInput, rows: CaptureShard[]): Promise<void> {\n const { enqueue } = args.options;\n for (const row of rows) {\n // oxlint-disable-next-line no-await-in-loop -- shards are queued in plan order\n await enqueue?.({\n buildId: args.build.id,\n shard: row.id,\n reqId: args.input.reqId,\n traceparent: args.input.traceparent,\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { BuildModel } from "../../models/build.mjs";
|
|
5
|
+
import { SnapshotModel } from "../../models/snapshot.mjs";
|
|
6
|
+
// src/capture/shards/history.ts
|
|
7
|
+
/** What the project's recent builds say a story costs, so a plan can order its work by it. */
|
|
8
|
+
/**
|
|
9
|
+
* Milliseconds each story cost, taken from the newest build of this project whose snapshots carry
|
|
10
|
+
* timings. Older builds are a fair guide: story render cost moves with the story, not the build.
|
|
11
|
+
*
|
|
12
|
+
* Returns an empty map when nothing has been timed yet — a fresh project, a runner that reports no
|
|
13
|
+
* durations, or a retention purge that took the timed builds with it — and the plan then falls back
|
|
14
|
+
* to slicing by capture count alone.
|
|
15
|
+
*/
|
|
16
|
+
async function storyDurations(target) {
|
|
17
|
+
const { options, project, build } = target;
|
|
18
|
+
const builds = new BuildModel(options.db, options.tables);
|
|
19
|
+
const snapshots = new SnapshotModel(options.db, options.tables);
|
|
20
|
+
const recent = (await builds.list(project.id, { limit: BUILDS_SCANNED + 1 })).filter((candidate) => candidate.id !== build.id).slice(0, BUILDS_SCANNED);
|
|
21
|
+
for (const candidate of recent) {
|
|
22
|
+
const timings = await snapshots.durationsByStory(candidate.id);
|
|
23
|
+
if (timings.size > 0) return timings;
|
|
24
|
+
}
|
|
25
|
+
return /* @__PURE__ */ new Map();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Recent builds to look through for timings. More than a couple is wasted work: a build that
|
|
29
|
+
* rendered nothing (everything inherited) carries none, but several in a row do not happen.
|
|
30
|
+
*/
|
|
31
|
+
const BUILDS_SCANNED = 3;
|
|
32
|
+
export { storyDurations };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=history.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.mjs","names":[],"sources":["../../../src/capture/shards/history.ts"],"sourcesContent":["/** What the project's recent builds say a story costs, so a plan can order its work by it. */\nimport { BuildModel } from \"../../models/build.ts\";\nimport { SnapshotModel } from \"../../models/snapshot.ts\";\nimport type { ShardTarget } from \"./context.ts\";\n\n/**\n * Milliseconds each story cost, taken from the newest build of this project whose snapshots carry\n * timings. Older builds are a fair guide: story render cost moves with the story, not the build.\n *\n * Returns an empty map when nothing has been timed yet — a fresh project, a runner that reports no\n * durations, or a retention purge that took the timed builds with it — and the plan then falls back\n * to slicing by capture count alone.\n */\nexport async function storyDurations(target: ShardTarget): Promise<Map<string, number>> {\n const { options, project, build } = target;\n const builds = new BuildModel(options.db, options.tables);\n const snapshots = new SnapshotModel(options.db, options.tables);\n // One extra in case the build being planned is among the newest.\n const recent = (await builds.list(project.id, { limit: BUILDS_SCANNED + 1 }))\n .filter((candidate) => candidate.id !== build.id)\n .slice(0, BUILDS_SCANNED);\n for (const candidate of recent) {\n // oxlint-disable-next-line no-await-in-loop -- stops at the first build that carries timings\n const timings = await snapshots.durationsByStory(candidate.id);\n if (timings.size > 0) {\n return timings;\n }\n }\n return new Map();\n}\n\n/**\n * Recent builds to look through for timings. More than a couple is wasted work: a build that\n * rendered nothing (everything inherited) carries none, but several in a row do not happen.\n */\nconst BUILDS_SCANNED = 3;\n"],"mappings":";;;;;;;;;;;;;;;AAaA,eAAsB,eAAe,QAAmD;CACtF,MAAM,EAAE,SAAS,SAAS,UAAU;CACpC,MAAM,SAAS,IAAI,WAAW,QAAQ,IAAI,QAAQ,MAAM;CACxD,MAAM,YAAY,IAAI,cAAc,QAAQ,IAAI,QAAQ,MAAM;CAE9D,MAAM,UAAU,MAAM,OAAO,KAAK,QAAQ,IAAI,EAAE,OAAO,iBAAiB,EAAE,CAAC,EAAA,CACxE,QAAQ,cAAc,UAAU,OAAO,MAAM,EAAE,CAAC,CAChD,MAAM,GAAG,cAAc;CAC1B,KAAK,MAAM,aAAa,QAAQ;EAE9B,MAAM,UAAU,MAAM,UAAU,iBAAiB,UAAU,EAAE;EAC7D,IAAI,QAAQ,OAAO,GACjB,OAAO;CAEX;CACA,uBAAO,IAAI,IAAI;AACjB;;;;;AAMA,MAAM,iBAAiB"}
|