@storyshelf/core 0.7.0 → 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/dist/adapter/capture-queue.d.mts +0 -2
- package/dist/adapter/capture-runner.d.mts +9 -4
- package/dist/adapter/capture-runner.mjs +6 -2
- package/dist/adapter/database.d.mts +63 -3
- package/dist/adapter/database.mjs +4 -3
- package/dist/adapter/database.mjs.map +1 -1
- package/dist/adapter/design.d.mts +0 -2
- package/dist/adapter/design.mjs +1 -2
- package/dist/adapter/email-sender.d.mts +0 -2
- package/dist/adapter/metadata.d.mts +0 -2
- package/dist/adapter/oauth.d.mts +0 -2
- package/dist/adapter/setup.d.mts +0 -2
- package/dist/adapter/setup.mjs +1 -2
- package/dist/adapter/storage.d.mts +1 -3
- package/dist/adapter/webhook-events.d.mts +0 -2
- package/dist/adapter/webhook-events.mjs +12 -13
- package/dist/adapter/webhook-events.mjs.map +1 -1
- package/dist/adapters/capture-target-offers.d.mts +0 -2
- package/dist/adapters/capture-target-offers.mjs +1 -2
- package/dist/adapters/capture-target.d.mts +0 -2
- package/dist/adapters/capture-target.mjs +1 -2
- package/dist/adapters/notifier/build-comment.d.mts +0 -2
- package/dist/adapters/notifier/build-comment.mjs +1 -2
- package/dist/adapters/notifier/build-state.d.mts +0 -2
- package/dist/adapters/notifier/build-state.mjs +1 -2
- package/dist/adapters/notifier/events.d.mts +0 -2
- package/dist/adapters/notifier/events.mjs +1 -2
- package/dist/adapters/notifier/format.d.mts +0 -2
- package/dist/adapters/notifier/format.mjs +1 -2
- package/dist/adapters/notifier/provider.d.mts +0 -2
- package/dist/adapters/notifier/provider.mjs +1 -2
- package/dist/adapters/notifier/state.d.mts +0 -2
- package/dist/adapters/notifier/state.mjs +1 -2
- package/dist/adapters/notifier/types.d.mts +0 -2
- package/dist/adapters/remote-browser.d.mts +112 -31
- package/dist/adapters/remote-browser.mjs +132 -34
- package/dist/adapters/remote-browser.mjs.map +1 -1
- 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 +0 -2
- package/dist/adapters/render-runs.mjs +1 -2
- package/dist/adapters/setup-runner.d.mts +0 -2
- package/dist/adapters/setup-runner.mjs +1 -2
- package/dist/adapters/sources.d.mts +0 -2
- package/dist/adapters/sources.mjs +1 -2
- package/dist/adapters/validate.d.mts +0 -2
- package/dist/adapters/validate.mjs +2 -2
- package/dist/adapters/validate.mjs.map +1 -1
- package/dist/ai.d.mts +0 -2
- package/dist/ai.mjs +1 -2
- 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 +0 -2
- package/dist/capture/a11y/axe-source.mjs +1 -2
- package/dist/capture/a11y/in-page.d.mts +1 -3
- package/dist/capture/a11y/in-page.mjs +3 -4
- package/dist/capture/a11y/in-page.mjs.map +1 -1
- package/dist/capture/a11y/map.d.mts +0 -2
- package/dist/capture/a11y/map.mjs +1 -2
- package/dist/capture/a11y/options.d.mts +0 -2
- package/dist/capture/a11y/options.mjs +1 -2
- package/dist/capture/adapter.d.mts +5 -2
- package/dist/capture/adapter.mjs +1 -2
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/affected.d.mts +0 -2
- package/dist/capture/affected.mjs +1 -2
- package/dist/capture/attempt-logger.mjs +1 -2
- package/dist/capture/browsers.d.mts +0 -2
- package/dist/capture/browsers.mjs +1 -2
- package/dist/capture/build-state/deliver.d.mts +0 -2
- package/dist/capture/build-state/deliver.mjs +1 -2
- package/dist/capture/build-state/notify-job.d.mts +0 -2
- package/dist/capture/build-state/notify-job.mjs +1 -2
- package/dist/capture/build-state/publisher.d.mts +0 -2
- package/dist/capture/build-state/publisher.mjs +1 -2
- package/dist/capture/build-state/resume.d.mts +0 -2
- package/dist/capture/build-state/resume.mjs +1 -2
- package/dist/capture/build-state/types.d.mts +1 -3
- package/dist/capture/catalog/catalogue.d.mts +0 -2
- package/dist/capture/catalog/catalogue.mjs +1 -2
- package/dist/capture/catalog/components.d.mts +0 -2
- package/dist/capture/catalog/components.mjs +1 -2
- package/dist/capture/catalog/history.d.mts +0 -2
- package/dist/capture/catalog/history.mjs +1 -2
- package/dist/capture/catalog/hook.d.mts +0 -2
- package/dist/capture/catalog/hook.mjs +1 -2
- package/dist/capture/catalog/index.d.mts +2 -2
- package/dist/capture/catalog/load.d.mts +0 -2
- package/dist/capture/catalog/load.mjs +1 -2
- package/dist/capture/catalog/parse.d.mts +0 -2
- package/dist/capture/catalog/parse.mjs +1 -2
- package/dist/capture/catalog/read.d.mts +6 -3
- package/dist/capture/catalog/read.mjs +14 -8
- package/dist/capture/catalog/read.mjs.map +1 -1
- package/dist/capture/catalog/types.d.mts +0 -2
- 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 +1 -3
- package/dist/capture/consumer/capture-handler.mjs +1 -2
- package/dist/capture/consumer/idle.d.mts +0 -2
- package/dist/capture/consumer/idle.mjs +1 -2
- package/dist/capture/consumer/index.d.mts +0 -2
- package/dist/capture/consumer/index.mjs +1 -2
- package/dist/capture/consumer/loop.mjs +1 -2
- package/dist/capture/consumer/metrics.d.mts +0 -2
- package/dist/capture/consumer/metrics.mjs +1 -2
- package/dist/capture/consumer/retry.d.mts +0 -2
- package/dist/capture/consumer/retry.mjs +1 -2
- package/dist/capture/consumer/run-job.mjs +1 -2
- package/dist/capture/consumer/startup.mjs +1 -2
- package/dist/capture/consumer/types.d.mts +0 -2
- package/dist/capture/crash-failure.d.mts +0 -2
- package/dist/capture/crash-failure.mjs +1 -2
- package/dist/capture/db-queue/cadence.d.mts +0 -2
- package/dist/capture/db-queue/cadence.mjs +1 -2
- package/dist/capture/db-queue/claim.d.mts +0 -2
- package/dist/capture/db-queue/claim.mjs +20 -4
- package/dist/capture/db-queue/claim.mjs.map +1 -1
- package/dist/capture/db-queue/clock.mjs +1 -2
- package/dist/capture/db-queue/codec.mjs +1 -2
- package/dist/capture/db-queue/index.d.mts +0 -2
- package/dist/capture/db-queue/index.mjs +3 -3
- package/dist/capture/db-queue/index.mjs.map +1 -1
- package/dist/capture/db-queue/keeper.d.mts +0 -2
- package/dist/capture/db-queue/keeper.mjs +1 -2
- package/dist/capture/db-queue/lease.mjs +1 -2
- package/dist/capture/db-queue/lifecycle.mjs +1 -2
- package/dist/capture/db-queue/operations.mjs +1 -2
- package/dist/capture/db-queue/poll-loop.d.mts +0 -2
- package/dist/capture/db-queue/poll-loop.mjs +1 -2
- package/dist/capture/db-queue/poll.d.mts +0 -2
- package/dist/capture/db-queue/poll.mjs +1 -2
- package/dist/capture/db-queue/timers.d.mts +0 -2
- package/dist/capture/db-queue/timers.mjs +1 -2
- package/dist/capture/db-queue/types.d.mts +0 -2
- package/dist/capture/db-queue/types.mjs +1 -2
- package/dist/capture/db-queue/types.mjs.map +1 -1
- package/dist/capture/db-queue/wake.mjs +1 -2
- package/dist/capture/design.mjs +1 -2
- package/dist/capture/diff-record.mjs +1 -2
- package/dist/capture/dispatch.d.mts +1 -3
- package/dist/capture/dispatch.mjs +17 -5
- package/dist/capture/dispatch.mjs.map +1 -1
- package/dist/capture/enrich.mjs +1 -2
- package/dist/capture/failure-sets.mjs +1 -2
- package/dist/capture/fingerprint.d.mts +0 -2
- package/dist/capture/fingerprint.mjs +1 -2
- package/dist/capture/forked/channel.d.mts +0 -2
- package/dist/capture/forked/child.d.mts +0 -2
- package/dist/capture/forked/child.mjs +1 -2
- package/dist/capture/forked/crash-tracker.mjs +1 -2
- package/dist/capture/forked/decision.d.mts +0 -2
- package/dist/capture/forked/decision.mjs +1 -2
- package/dist/capture/forked/exit.mjs +1 -2
- package/dist/capture/forked/fork.d.mts +0 -2
- package/dist/capture/forked/fork.mjs +1 -2
- package/dist/capture/forked/forked-capture.d.mts +0 -2
- package/dist/capture/forked/forked-capture.mjs +1 -2
- package/dist/capture/forked/lease-book.mjs +1 -2
- package/dist/capture/forked/protocol.d.mts +0 -2
- package/dist/capture/forked/protocol.mjs +1 -2
- package/dist/capture/forked/runs.mjs +1 -2
- 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 +1 -2
- package/dist/capture/job-lease.d.mts +0 -2
- package/dist/capture/job-lease.mjs +1 -2
- package/dist/capture/matrix.mjs +4 -4
- package/dist/capture/matrix.mjs.map +1 -1
- package/dist/capture/modes.d.mts +0 -2
- package/dist/capture/modes.mjs +2 -3
- package/dist/capture/modes.mjs.map +1 -1
- package/dist/capture/orchestrator.d.mts +0 -2
- package/dist/capture/orchestrator.mjs +2 -3
- package/dist/capture/pipeline.d.mts +0 -2
- package/dist/capture/pipeline.mjs +37 -6
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/progress.d.mts +0 -2
- package/dist/capture/progress.mjs +1 -2
- package/dist/capture/publish/accept.d.mts +0 -2
- package/dist/capture/publish/accept.mjs +1 -2
- package/dist/capture/publish/complete.d.mts +1 -3
- package/dist/capture/publish/complete.mjs +1 -2
- package/dist/capture/publish/extract.d.mts +0 -2
- package/dist/capture/publish/extract.mjs +6 -5
- package/dist/capture/publish/extract.mjs.map +1 -1
- package/dist/capture/publish/handler.d.mts +1 -3
- package/dist/capture/publish/handler.mjs +1 -2
- package/dist/capture/publish/index.d.mts +2 -1
- 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 +0 -2
- package/dist/capture/publish/limits.mjs +1 -2
- package/dist/capture/publish/published.d.mts +0 -2
- package/dist/capture/publish/published.mjs +1 -2
- package/dist/capture/publish/run.d.mts +1 -3
- package/dist/capture/publish/run.mjs +1 -2
- package/dist/capture/rediff.d.mts +1 -3
- package/dist/capture/rediff.mjs +19 -6
- package/dist/capture/rediff.mjs.map +1 -1
- package/dist/capture/render.mjs +1 -2
- package/dist/capture/render.mjs.map +1 -1
- package/dist/capture/scratch.d.mts +0 -2
- package/dist/capture/scratch.mjs +2 -4
- package/dist/capture/scratch.mjs.map +1 -1
- package/dist/capture/shard-plan.mjs +1 -2
- package/dist/capture/shards/context.mjs +3 -3
- package/dist/capture/shards/context.mjs.map +1 -1
- package/dist/capture/shards/execute.mjs +4 -3
- package/dist/capture/shards/execute.mjs.map +1 -1
- package/dist/capture/shards/fan-out.mjs +1 -2
- package/dist/capture/shards/history.mjs +1 -2
- package/dist/capture/shards/reduce.mjs +1 -2
- package/dist/capture/sizing.d.mts +0 -2
- package/dist/capture/sizing.mjs +1 -2
- package/dist/capture/skip-checks.mjs +1 -2
- package/dist/capture/statics.d.mts +0 -2
- package/dist/capture/statics.mjs +2 -4
- package/dist/capture/statics.mjs.map +1 -1
- package/dist/capture/storybook-urls.d.mts +0 -2
- package/dist/capture/storybook-urls.mjs +3 -3
- package/dist/capture/storybook-urls.mjs.map +1 -1
- package/dist/capture/storybook.d.mts +0 -2
- package/dist/capture/storybook.mjs +8 -4
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/capture/target.mjs +1 -2
- package/dist/capture/targets.d.mts +0 -2
- package/dist/capture/targets.mjs +1 -2
- package/dist/capture/tests.d.mts +0 -2
- package/dist/capture/tests.mjs +13 -3
- package/dist/capture/tests.mjs.map +1 -1
- package/dist/capture/token-chunks.mjs +24 -7
- package/dist/capture/token-chunks.mjs.map +1 -1
- package/dist/capture.d.mts +5 -2
- package/dist/capture.mjs +5 -2
- package/dist/config.d.mts +16 -3
- package/dist/config.mjs +6 -3
- package/dist/config.mjs.map +1 -1
- package/dist/diff.d.mts +0 -2
- package/dist/diff.mjs +1 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/insights/budget.d.mts +0 -2
- package/dist/insights/budget.mjs +1 -2
- package/dist/insights/envelope.d.mts +0 -2
- package/dist/insights/envelope.mjs +1 -2
- package/dist/insights/evidence-health.d.mts +0 -2
- package/dist/insights/evidence-health.mjs +1 -2
- package/dist/insights/evidence.d.mts +0 -2
- package/dist/insights/evidence.mjs +1 -2
- package/dist/insights/hash.d.mts +0 -2
- package/dist/insights/hash.mjs +1 -2
- package/dist/insights/limits.d.mts +0 -2
- package/dist/insights/limits.mjs +1 -2
- package/dist/insights/prompts/v1.d.mts +0 -2
- package/dist/insights/prompts/v1.mjs +1 -2
- package/dist/insights/redact.d.mts +0 -2
- package/dist/insights/redact.mjs +1 -2
- package/dist/insights/text.mjs +1 -2
- package/dist/logger.d.mts +0 -2
- package/dist/logger.mjs +1 -2
- package/dist/models/ai-budget-alert.d.mts +0 -2
- package/dist/models/ai-budget-alert.mjs +1 -2
- package/dist/models/ai-usage.d.mts +0 -2
- package/dist/models/ai-usage.mjs +1 -2
- 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 +0 -2
- package/dist/models/baseline-archive.mjs +1 -2
- 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 +0 -2
- package/dist/models/baseline-export.mjs +1 -2
- package/dist/models/baseline-import.mjs +1 -2
- 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 +1 -2
- package/dist/models/baseline-transfer.mjs +30 -2
- package/dist/models/baseline-transfer.mjs.map +1 -1
- package/dist/models/baseline.d.mts +5 -2
- package/dist/models/baseline.mjs +91 -12
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build-component.d.mts +0 -2
- package/dist/models/build-component.mjs +1 -2
- package/dist/models/build-review-status.d.mts +0 -2
- package/dist/models/build-review-status.mjs +1 -2
- package/dist/models/build-review.d.mts +0 -2
- package/dist/models/build-review.mjs +1 -2
- package/dist/models/build.d.mts +1 -3
- package/dist/models/build.mjs +14 -15
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/capture-attempt.d.mts +0 -2
- package/dist/models/capture-attempt.mjs +1 -2
- package/dist/models/capture-job.d.mts +0 -2
- package/dist/models/capture-job.mjs +1 -2
- package/dist/models/capture-log.d.mts +0 -2
- package/dist/models/capture-log.mjs +1 -2
- package/dist/models/capture-shard.d.mts +0 -2
- package/dist/models/capture-shard.mjs +1 -2
- package/dist/models/changeset.d.mts +0 -2
- package/dist/models/changeset.mjs +1 -2
- package/dist/models/columns.mjs +1 -2
- package/dist/models/comment.d.mts +0 -2
- package/dist/models/comment.mjs +1 -2
- package/dist/models/component.d.mts +0 -2
- package/dist/models/component.mjs +1 -2
- package/dist/models/connections.d.mts +0 -2
- package/dist/models/connections.mjs +1 -2
- package/dist/models/content-ref.d.mts +0 -2
- package/dist/models/content-ref.mjs +1 -2
- package/dist/models/credentials.d.mts +0 -2
- package/dist/models/credentials.mjs +1 -2
- package/dist/models/design-credentials.d.mts +0 -2
- package/dist/models/design-credentials.mjs +1 -2
- package/dist/models/design-health.d.mts +0 -2
- package/dist/models/design-health.mjs +1 -2
- package/dist/models/design-nodes.d.mts +2 -4
- package/dist/models/design-nodes.mjs +1 -2
- package/dist/models/design-renders.mjs +1 -2
- package/dist/models/design-resolve.d.mts +0 -2
- package/dist/models/design-resolve.mjs +1 -2
- package/dist/models/design.d.mts +0 -2
- package/dist/models/design.mjs +1 -2
- package/dist/models/inherited-ratio.d.mts +0 -2
- package/dist/models/inherited-ratio.mjs +1 -2
- package/dist/models/insight.d.mts +0 -2
- package/dist/models/insight.mjs +1 -2
- package/dist/models/label.d.mts +0 -2
- package/dist/models/label.mjs +1 -2
- package/dist/models/member.d.mts +0 -2
- package/dist/models/member.mjs +1 -2
- package/dist/models/notification-channel.d.mts +0 -2
- package/dist/models/notification-channel.mjs +1 -2
- package/dist/models/notification-subscription.d.mts +0 -2
- package/dist/models/notification-subscription.mjs +1 -2
- package/dist/models/project-group-mapping.d.mts +0 -2
- package/dist/models/project-group-mapping.mjs +1 -2
- package/dist/models/project.d.mts +0 -2
- package/dist/models/project.mjs +1 -2
- package/dist/models/public-origin.d.mts +0 -2
- package/dist/models/public-origin.mjs +1 -2
- package/dist/models/review-switch-off.d.mts +0 -2
- package/dist/models/review-switch-off.mjs +1 -2
- package/dist/models/site-settings.d.mts +0 -2
- package/dist/models/site-settings.mjs +1 -2
- package/dist/models/snapshot.d.mts +3 -2
- package/dist/models/snapshot.mjs +4 -2
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/token.d.mts +0 -2
- package/dist/models/token.mjs +1 -2
- package/dist/models/usage.d.mts +0 -2
- package/dist/models/usage.mjs +1 -2
- package/dist/models/user.d.mts +0 -2
- package/dist/models/user.mjs +1 -2
- package/dist/models/webhook.d.mts +0 -2
- package/dist/models/webhook.mjs +1 -2
- package/dist/models.d.mts +5 -2
- package/dist/models.mjs +5 -2
- package/dist/options.d.mts +0 -2
- package/dist/options.mjs +2 -4
- package/dist/options.mjs.map +1 -1
- package/dist/paths.d.mts +3 -3
- package/dist/paths.mjs +7 -3
- package/dist/paths.mjs.map +1 -1
- package/dist/retention/ai.d.mts +1 -3
- package/dist/retention/ai.mjs +0 -1
- package/dist/retention/pending.mjs +1 -2
- 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 +0 -2
- package/dist/retention/screenshot-sweep.mjs +1 -2
- package/dist/retention.d.mts +0 -2
- package/dist/retention.mjs +5 -2
- package/dist/retention.mjs.map +1 -1
- package/dist/schema/baseline.d.mts +2 -2
- package/dist/schema/build-component.d.mts +0 -2
- package/dist/schema/build-review.d.mts +0 -2
- package/dist/schema/build.d.mts +0 -2
- package/dist/schema/capture-attempt.d.mts +0 -2
- package/dist/schema/capture-job.d.mts +0 -2
- package/dist/schema/capture-shard.d.mts +0 -2
- package/dist/schema/comment.d.mts +0 -2
- package/dist/schema/connection.d.mts +0 -2
- package/dist/schema/content-ref.d.mts +0 -2
- package/dist/schema/design.d.mts +0 -2
- package/dist/schema/insight.d.mts +0 -2
- package/dist/schema/label.d.mts +0 -2
- package/dist/schema/member.d.mts +0 -2
- package/dist/schema/notification.d.mts +0 -2
- package/dist/schema/project-group-mapping.d.mts +0 -2
- package/dist/schema/project.d.mts +0 -2
- package/dist/schema/site-setting.d.mts +0 -2
- package/dist/schema/snapshot.d.mts +5 -2
- package/dist/schema/token.d.mts +0 -2
- package/dist/schema/usage.d.mts +0 -2
- package/dist/schema/user.d.mts +0 -2
- package/dist/schema/webhook.d.mts +0 -2
- package/dist/schema.d.mts +4 -6
- package/dist/serverless.d.mts +0 -2
- package/dist/serverless.mjs +1 -2
- 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 +0 -2
- 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 +0 -2
- package/dist/test-helpers/deferred.mjs +1 -2
- package/dist/test-helpers/design-contract.d.mts +0 -2
- package/dist/test-helpers/design-contract.mjs +1 -2
- 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 +7 -2
- package/dist/test-helpers/fake-database.mjs.map +1 -1
- package/dist/test-helpers/fake-design.d.mts +0 -2
- package/dist/test-helpers/fake-design.mjs +1 -2
- package/dist/test-helpers/fake-notifier.d.mts +0 -2
- package/dist/test-helpers/fake-notifier.mjs +1 -2
- 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 +5 -2
- package/dist/test-helpers/fake-tables.mjs.map +1 -1
- package/dist/test-helpers/manual-clock.d.mts +0 -2
- package/dist/test-helpers/manual-clock.mjs +1 -2
- package/dist/test-helpers/notifier-contract.d.mts +0 -2
- package/dist/test-helpers/notifier-contract.mjs +1 -2
- package/dist/test-helpers/queue-contract.d.mts +0 -2
- package/dist/test-helpers/queue-contract.mjs +1 -2
- package/dist/test-helpers/queue-multi-instance.d.mts +0 -2
- package/dist/test-helpers/queue-multi-instance.mjs +1 -2
- package/dist/test-helpers/runner-contract.d.mts +0 -2
- package/dist/test-helpers/runner-contract.mjs +1 -2
- package/dist/test-helpers/sql-chunks.mjs +2 -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 +1 -2
- package/dist/test-helpers.d.mts +2 -1
- package/dist/test-helpers.mjs +2 -1
- package/dist/tracing.mjs +36 -6
- package/dist/tracing.mjs.map +1 -1
- package/dist/types.d.mts +0 -2
- package/dist/types.mjs +1 -2
- package/dist/urls.d.mts +0 -2
- package/dist/urls.mjs +1 -2
- package/dist/utils/adaptive-concurrency.d.mts +0 -2
- package/dist/utils/adaptive-concurrency.mjs +1 -2
- package/dist/utils/connect-retry.d.mts +2 -2
- package/dist/utils/connect-retry.mjs +2 -3
- package/dist/utils/connect-retry.mjs.map +1 -1
- package/dist/utils/encrypt.d.mts +0 -2
- package/dist/utils/encrypt.mjs +1 -2
- package/dist/utils/environment.d.mts +0 -2
- package/dist/utils/environment.mjs +1 -2
- package/dist/utils/hash.d.mts +0 -2
- package/dist/utils/hash.mjs +1 -2
- package/dist/utils/http.d.mts +13 -3
- package/dist/utils/http.mjs +79 -17
- package/dist/utils/http.mjs.map +1 -1
- package/dist/utils/map-adaptive.d.mts +0 -2
- package/dist/utils/map-adaptive.mjs +1 -2
- package/dist/utils/map-limit.d.mts +0 -2
- package/dist/utils/map-limit.mjs +1 -2
- package/dist/utils/mime.d.mts +0 -2
- package/dist/utils/mime.mjs +1 -2
- package/dist/utils/page-limiter.d.mts +0 -2
- package/dist/utils/page-limiter.mjs +1 -2
- 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 +0 -2
- package/dist/utils/public-origin.mjs +1 -2
- package/dist/utils/redact.d.mts +0 -2
- package/dist/utils/redact.mjs +1 -2
- package/dist/utils/remote-environment.d.mts +40 -3
- package/dist/utils/remote-environment.mjs +52 -3
- package/dist/utils/remote-environment.mjs.map +1 -1
- package/dist/utils/resources.d.mts +0 -2
- package/dist/utils/resources.mjs +1 -2
- package/dist/utils/screenshot-store.d.mts +0 -2
- package/dist/utils/screenshot-store.mjs +1 -2
- package/dist/utils/session-gate.d.mts +7 -6
- package/dist/utils/session-gate.mjs +28 -12
- package/dist/utils/session-gate.mjs.map +1 -1
- package/dist/utils/storybook-token.d.mts +11 -3
- package/dist/utils/storybook-token.mjs +10 -9
- package/dist/utils/storybook-token.mjs.map +1 -1
- package/dist/utils/storybook-url.d.mts +0 -2
- package/dist/utils/storybook-url.mjs +1 -2
- package/dist/utils/timing.d.mts +0 -2
- package/dist/utils/timing.mjs +1 -2
- package/dist/utils/ulid.d.mts +0 -2
- package/dist/utils/ulid.mjs +1 -2
- package/dist/utils/zip-read.mjs +1 -2
- package/dist/utils/zip-write.d.mts +0 -2
- package/dist/utils/zip-write.mjs +1 -2
- package/dist/utils.d.mts +5 -5
- package/dist/utils.mjs +5 -5
- package/package.json +6 -6
package/dist/capture/rediff.mjs
CHANGED
|
@@ -2,13 +2,14 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { diffPath } from "../paths.mjs";
|
|
5
|
-
import { BaselineModel } from "../models/baseline.mjs";
|
|
6
5
|
import { NO_BASELINE } from "../models/baseline-status.mjs";
|
|
6
|
+
import { BaselineModel } from "../models/baseline.mjs";
|
|
7
7
|
import { SnapshotModel } from "../models/snapshot.mjs";
|
|
8
8
|
import { mapLimit } from "../utils/map-limit.mjs";
|
|
9
|
+
import { freezeComparison, isComparisonImagePath, parseComparisonReference, sourceSnapshotId } from "./comparison-reference.mjs";
|
|
9
10
|
import { diffStoredScreenshots, writeDiffOverlay } from "./diff-record.mjs";
|
|
10
11
|
import { diffStatus } from "./pipeline.mjs";
|
|
11
|
-
|
|
12
|
+
// src/capture/rediff.ts
|
|
12
13
|
/** Build statuses a re-diff may run in; earlier or failed builds have no complete snapshot set. */
|
|
13
14
|
const REDIFFABLE_BUILD_STATUSES = /* @__PURE__ */ new Set([
|
|
14
15
|
"reviewing",
|
|
@@ -45,6 +46,7 @@ async function rediffSnapshot(deps, project, build, row) {
|
|
|
45
46
|
const baseline = await new BaselineModel(deps.db).resolve(project.id, row.storyId, row.viewportName, build.gitBranch, project.gitDefaultBranch, row.browser);
|
|
46
47
|
const patch = baseline ? await diffPatch(deps, project, build, row, baseline) : noBaselinePatch();
|
|
47
48
|
await new SnapshotModel(deps.db).update(row.id, patch);
|
|
49
|
+
if (row.comparisonImagePath && row.comparisonImagePath !== patch.comparisonImagePath && isComparisonImagePath(row.comparisonImagePath, project.id, build.id)) await deps.storage.delete(row.comparisonImagePath).catch(() => {});
|
|
48
50
|
return patch.status === row.status ? "same" : "changed";
|
|
49
51
|
}
|
|
50
52
|
function noBaselinePatch() {
|
|
@@ -55,11 +57,22 @@ function noBaselinePatch() {
|
|
|
55
57
|
diffRatio: null,
|
|
56
58
|
diffPassed: null,
|
|
57
59
|
baselineId: null,
|
|
58
|
-
baselineVersion: NO_BASELINE
|
|
60
|
+
baselineVersion: NO_BASELINE,
|
|
61
|
+
comparisonImagePath: null,
|
|
62
|
+
comparisonMetadata: null
|
|
59
63
|
};
|
|
60
64
|
}
|
|
61
65
|
async function diffPatch(deps, project, build, row, baseline) {
|
|
62
|
-
const
|
|
66
|
+
const comparison = await freezeComparison(deps.storage, project.id, build.id, baseline, {
|
|
67
|
+
defaultBranch: project.gitDefaultBranch,
|
|
68
|
+
mode: row.mode ?? null,
|
|
69
|
+
target: row.browser ?? "chromium",
|
|
70
|
+
width: row.viewportWidth ?? null,
|
|
71
|
+
height: row.viewportHeight ?? null,
|
|
72
|
+
captureFingerprint: parseComparisonReference(row.comparisonMetadata)?.captureFingerprint ?? null,
|
|
73
|
+
sourceSnapshotId: await sourceSnapshotId(deps.db, baseline)
|
|
74
|
+
});
|
|
75
|
+
const result = await diffStoredScreenshots(deps.storage, project, baseline.screenshotPath === row.screenshotPath ? row.screenshotPath : comparison.comparisonImagePath, row.screenshotPath);
|
|
63
76
|
const target = diffPath(project.id, build.id, row.storyId, row.viewportName, row.browser);
|
|
64
77
|
const overlay = await writeDiffOverlay(deps.storage, target, result, result.passed);
|
|
65
78
|
return {
|
|
@@ -69,10 +82,10 @@ async function diffPatch(deps, project, build, row, baseline) {
|
|
|
69
82
|
diffRatio: result.diffRatio,
|
|
70
83
|
diffPassed: result.passed,
|
|
71
84
|
baselineId: baseline.id,
|
|
72
|
-
baselineVersion: baseline.updatedAt
|
|
85
|
+
baselineVersion: baseline.updatedAt,
|
|
86
|
+
...comparison
|
|
73
87
|
};
|
|
74
88
|
}
|
|
75
|
-
//#endregion
|
|
76
89
|
export { REDIFFABLE_BUILD_STATUSES, rediffBuild };
|
|
77
90
|
|
|
78
91
|
//# sourceMappingURL=rediff.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rediff.mjs","names":[],"sources":["../../src/capture/rediff.ts"],"sourcesContent":["/** Recompute a build's diffs against the baselines that apply now, without re-rendering. */\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { NO_BASELINE } from \"../models/baseline-status.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport type { Baseline } from \"../schema/baseline.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport type { Snapshot } from \"../schema/snapshot.ts\";\nimport { mapLimit } from \"../utils/map-limit.ts\";\nimport { diffPath } from \"../utils/paths.ts\";\nimport { diffStoredScreenshots, writeDiffOverlay } from \"./diff-record.ts\";\nimport { diffStatus, PERSIST_CONCURRENCY } from \"./pipeline.ts\";\n\n/** Dependencies for {@link rediffBuild}. */\nexport interface RediffDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n logger?: Logger;\n}\n\n/** Outcome of a re-diff run. */\nexport interface RediffResult {\n /** Snapshots diffed against their current baseline. */\n rediffed: number;\n /** Snapshots whose status changed (for example `changed` to `unchanged`). */\n statusChanged: number;\n /** Snapshot ids skipped because their stored screenshot is gone. */\n missingScreenshots: string[];\n}\n\n/** Build statuses a re-diff may run in; earlier or failed builds have no complete snapshot set. */\nexport const REDIFFABLE_BUILD_STATUSES: ReadonlySet<string> = new Set([\n \"reviewing\",\n \"approved\",\n \"rejected\",\n]);\n\nconst REDIFFABLE_SNAPSHOT_STATUSES: ReadonlySet<string> = new Set([\"new\", \"changed\", \"unchanged\"]);\n\n/**\n * Re-diff the build's undecided snapshots against the baselines that apply today.\n *\n * Approved and rejected snapshots are decisions and stay untouched; inherited snapshots were never\n * rendered. The caller refreshes the build rollup afterwards.\n */\nexport async function rediffBuild(\n deps: RediffDeps,\n project: Project,\n build: Build,\n): Promise<RediffResult> {\n const snapshots = new SnapshotModel(deps.db);\n const rows = (await snapshots.listByBuild(build.id)).filter(\n (row) => !row.inherited && REDIFFABLE_SNAPSHOT_STATUSES.has(row.status),\n );\n const outcomes = await mapLimit(\n rows,\n PERSIST_CONCURRENCY,\n async (row) => await rediffSnapshot(deps, project, build, row),\n );\n const result: RediffResult = {\n rediffed: outcomes.filter((outcome) => outcome !== \"missing\").length,\n statusChanged: outcomes.filter((outcome) => outcome === \"changed\").length,\n missingScreenshots: rows\n .filter((_row, index) => outcomes[index] === \"missing\")\n .map((row) => row.id),\n };\n deps.logger?.info({ buildId: build.id, ...result }, \"build re-diffed against current baselines\");\n return result;\n}\n\ntype Outcome = \"same\" | \"changed\" | \"missing\";\n\nasync function rediffSnapshot(\n deps: RediffDeps,\n project: Project,\n build: Build,\n row: Snapshot,\n): Promise<Outcome> {\n if (!(await deps.storage.exists(row.screenshotPath))) {\n return \"missing\";\n }\n const baseline = await new BaselineModel(deps.db).resolve(\n project.id,\n row.storyId,\n row.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n row.browser,\n );\n const patch = baseline ? await diffPatch(deps, project, build, row, baseline) : noBaselinePatch();\n await new SnapshotModel(deps.db).update(row.id, patch);\n return patch.status === row.status ? \"same\" : \"changed\";\n}\n\nfunction noBaselinePatch(): Partial<Snapshot> {\n return {\n status: \"new\",\n diffPath: null,\n diffPixels: null,\n diffRatio: null,\n diffPassed: null,\n baselineId: null,\n baselineVersion: NO_BASELINE,\n };\n}\n\nasync function diffPatch(\n deps: RediffDeps,\n project: Project,\n build: Build,\n row: Snapshot,\n baseline: Baseline,\n): Promise<Partial<Snapshot>> {\n const result = await diffStoredScreenshots(\n deps.storage,\n project,\n baseline.screenshotPath,\n row.screenshotPath,\n );\n const target = diffPath(project.id, build.id, row.storyId, row.viewportName, row.browser);\n const overlay = await writeDiffOverlay(deps.storage, target, result, result.passed);\n return {\n status: diffStatus(result.passed, false),\n diffPath: overlay,\n diffPixels: result.diffPixels,\n diffRatio: result.diffRatio,\n diffPassed: result.passed,\n baselineId: baseline.id,\n baselineVersion: baseline.updatedAt,\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"rediff.mjs","names":[],"sources":["../../src/capture/rediff.ts"],"sourcesContent":["/** Recompute a build's diffs against the baselines that apply now, without re-rendering. */\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { NO_BASELINE } from \"../models/baseline-status.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport type { Baseline } from \"../schema/baseline.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport type { Snapshot } from \"../schema/snapshot.ts\";\nimport { mapLimit } from \"../utils/map-limit.ts\";\nimport { diffPath } from \"../utils/paths.ts\";\nimport {\n freezeComparison,\n isComparisonImagePath,\n parseComparisonReference,\n sourceSnapshotId,\n} from \"./comparison-reference.ts\";\nimport { diffStoredScreenshots, writeDiffOverlay } from \"./diff-record.ts\";\nimport { diffStatus, PERSIST_CONCURRENCY } from \"./pipeline.ts\";\n\n/** Dependencies for {@link rediffBuild}. */\nexport interface RediffDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n logger?: Logger;\n}\n\n/** Outcome of a re-diff run. */\nexport interface RediffResult {\n /** Snapshots diffed against their current baseline. */\n rediffed: number;\n /** Snapshots whose status changed (for example `changed` to `unchanged`). */\n statusChanged: number;\n /** Snapshot ids skipped because their stored screenshot is gone. */\n missingScreenshots: string[];\n}\n\n/** Build statuses a re-diff may run in; earlier or failed builds have no complete snapshot set. */\nexport const REDIFFABLE_BUILD_STATUSES: ReadonlySet<string> = new Set([\n \"reviewing\",\n \"approved\",\n \"rejected\",\n]);\n\nconst REDIFFABLE_SNAPSHOT_STATUSES: ReadonlySet<string> = new Set([\"new\", \"changed\", \"unchanged\"]);\n\n/**\n * Re-diff the build's undecided snapshots against the baselines that apply today.\n *\n * Approved and rejected snapshots are decisions and stay untouched; inherited snapshots were never\n * rendered. The caller refreshes the build rollup afterwards.\n */\nexport async function rediffBuild(\n deps: RediffDeps,\n project: Project,\n build: Build,\n): Promise<RediffResult> {\n const snapshots = new SnapshotModel(deps.db);\n const rows = (await snapshots.listByBuild(build.id)).filter(\n (row) => !row.inherited && REDIFFABLE_SNAPSHOT_STATUSES.has(row.status),\n );\n const outcomes = await mapLimit(\n rows,\n PERSIST_CONCURRENCY,\n async (row) => await rediffSnapshot(deps, project, build, row),\n );\n const result: RediffResult = {\n rediffed: outcomes.filter((outcome) => outcome !== \"missing\").length,\n statusChanged: outcomes.filter((outcome) => outcome === \"changed\").length,\n missingScreenshots: rows\n .filter((_row, index) => outcomes[index] === \"missing\")\n .map((row) => row.id),\n };\n deps.logger?.info({ buildId: build.id, ...result }, \"build re-diffed against current baselines\");\n return result;\n}\n\ntype Outcome = \"same\" | \"changed\" | \"missing\";\n\nasync function rediffSnapshot(\n deps: RediffDeps,\n project: Project,\n build: Build,\n row: Snapshot,\n): Promise<Outcome> {\n if (!(await deps.storage.exists(row.screenshotPath))) {\n return \"missing\";\n }\n const baseline = await new BaselineModel(deps.db).resolve(\n project.id,\n row.storyId,\n row.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n row.browser,\n );\n const patch = baseline ? await diffPatch(deps, project, build, row, baseline) : noBaselinePatch();\n await new SnapshotModel(deps.db).update(row.id, patch);\n if (\n row.comparisonImagePath &&\n row.comparisonImagePath !== patch.comparisonImagePath &&\n isComparisonImagePath(row.comparisonImagePath, project.id, build.id)\n ) {\n await deps.storage.delete(row.comparisonImagePath).catch(() => {});\n }\n return patch.status === row.status ? \"same\" : \"changed\";\n}\n\nfunction noBaselinePatch(): Partial<Snapshot> {\n return {\n status: \"new\",\n diffPath: null,\n diffPixels: null,\n diffRatio: null,\n diffPassed: null,\n baselineId: null,\n baselineVersion: NO_BASELINE,\n comparisonImagePath: null,\n comparisonMetadata: null,\n };\n}\n\nasync function diffPatch(\n deps: RediffDeps,\n project: Project,\n build: Build,\n row: Snapshot,\n baseline: Baseline,\n): Promise<Partial<Snapshot>> {\n const comparison = await freezeComparison(deps.storage, project.id, build.id, baseline, {\n defaultBranch: project.gitDefaultBranch,\n mode: row.mode ?? null,\n target: row.browser ?? \"chromium\",\n width: row.viewportWidth ?? null,\n height: row.viewportHeight ?? null,\n captureFingerprint:\n parseComparisonReference(row.comparisonMetadata)?.captureFingerprint ?? null,\n sourceSnapshotId: await sourceSnapshotId(deps.db, baseline),\n });\n const result = await diffStoredScreenshots(\n deps.storage,\n project,\n baseline.screenshotPath === row.screenshotPath\n ? row.screenshotPath\n : comparison.comparisonImagePath,\n row.screenshotPath,\n );\n const target = diffPath(project.id, build.id, row.storyId, row.viewportName, row.browser);\n const overlay = await writeDiffOverlay(deps.storage, target, result, result.passed);\n return {\n status: diffStatus(result.passed, false),\n diffPath: overlay,\n diffPixels: result.diffPixels,\n diffRatio: result.diffRatio,\n diffPassed: result.passed,\n baselineId: baseline.id,\n baselineVersion: baseline.updatedAt,\n ...comparison,\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAwCA,MAAa,4CAAiD,IAAI,IAAI;CACpE;CACA;CACA;AACF,CAAC;AAED,MAAM,+CAAoD,IAAI,IAAI;CAAC;CAAO;CAAW;AAAW,CAAC;;;;;;;AAQjG,eAAsB,YACpB,MACA,SACA,OACuB;CAEvB,MAAM,QAAQ,MAAM,IADE,cAAc,KAAK,EACb,CAAC,CAAC,YAAY,MAAM,EAAE,EAAA,CAAG,QAClD,QAAQ,CAAC,IAAI,aAAa,6BAA6B,IAAI,IAAI,MAAM,CACxE;CACA,MAAM,WAAW,MAAM,SACrB,MAAA,GAEA,OAAO,QAAQ,MAAM,eAAe,MAAM,SAAS,OAAO,GAAG,CAC/D;CACA,MAAM,SAAuB;EAC3B,UAAU,SAAS,QAAQ,YAAY,YAAY,SAAS,CAAC,CAAC;EAC9D,eAAe,SAAS,QAAQ,YAAY,YAAY,SAAS,CAAC,CAAC;EACnE,oBAAoB,KACjB,QAAQ,MAAM,UAAU,SAAS,WAAW,SAAS,CAAC,CACtD,KAAK,QAAQ,IAAI,EAAE;CACxB;CACA,KAAK,QAAQ,KAAK;EAAE,SAAS,MAAM;EAAI,GAAG;CAAO,GAAG,2CAA2C;CAC/F,OAAO;AACT;AAIA,eAAe,eACb,MACA,SACA,OACA,KACkB;CAClB,IAAI,CAAE,MAAM,KAAK,QAAQ,OAAO,IAAI,cAAc,GAChD,OAAO;CAET,MAAM,WAAW,MAAM,IAAI,cAAc,KAAK,EAAE,CAAC,CAAC,QAChD,QAAQ,IACR,IAAI,SACJ,IAAI,cACJ,MAAM,WACN,QAAQ,kBACR,IAAI,OACN;CACA,MAAM,QAAQ,WAAW,MAAM,UAAU,MAAM,SAAS,OAAO,KAAK,QAAQ,IAAI,gBAAgB;CAChG,MAAM,IAAI,cAAc,KAAK,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI,KAAK;CACrD,IACE,IAAI,uBACJ,IAAI,wBAAwB,MAAM,uBAClC,sBAAsB,IAAI,qBAAqB,QAAQ,IAAI,MAAM,EAAE,GAEnE,MAAM,KAAK,QAAQ,OAAO,IAAI,mBAAmB,CAAC,CAAC,YAAY,CAAC,CAAC;CAEnE,OAAO,MAAM,WAAW,IAAI,SAAS,SAAS;AAChD;AAEA,SAAS,kBAAqC;CAC5C,OAAO;EACL,QAAQ;EACR,UAAU;EACV,YAAY;EACZ,WAAW;EACX,YAAY;EACZ,YAAY;EACZ,iBAAiB;EACjB,qBAAqB;EACrB,oBAAoB;CACtB;AACF;AAEA,eAAe,UACb,MACA,SACA,OACA,KACA,UAC4B;CAC5B,MAAM,aAAa,MAAM,iBAAiB,KAAK,SAAS,QAAQ,IAAI,MAAM,IAAI,UAAU;EACtF,eAAe,QAAQ;EACvB,MAAM,IAAI,QAAQ;EAClB,QAAQ,IAAI,WAAW;EACvB,OAAO,IAAI,iBAAiB;EAC5B,QAAQ,IAAI,kBAAkB;EAC9B,oBACE,yBAAyB,IAAI,kBAAkB,CAAC,EAAE,sBAAsB;EAC1E,kBAAkB,MAAM,iBAAiB,KAAK,IAAI,QAAQ;CAC5D,CAAC;CACD,MAAM,SAAS,MAAM,sBACnB,KAAK,SACL,SACA,SAAS,mBAAmB,IAAI,iBAC5B,IAAI,iBACJ,WAAW,qBACf,IAAI,cACN;CACA,MAAM,SAAS,SAAS,QAAQ,IAAI,MAAM,IAAI,IAAI,SAAS,IAAI,cAAc,IAAI,OAAO;CACxF,MAAM,UAAU,MAAM,iBAAiB,KAAK,SAAS,QAAQ,QAAQ,OAAO,MAAM;CAClF,OAAO;EACL,QAAQ,WAAW,OAAO,QAAQ,KAAK;EACvC,UAAU;EACV,YAAY,OAAO;EACnB,WAAW,OAAO;EAClB,YAAY,OAAO;EACnB,YAAY,SAAS;EACrB,iBAAiB,SAAS;EAC1B,GAAG;CACL;AACF"}
|
package/dist/capture/render.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import "../utils/storybook-token.mjs";
|
|
5
5
|
import { redactSecrets } from "../utils/redact.mjs";
|
|
6
|
-
|
|
6
|
+
// src/capture/render.ts
|
|
7
7
|
/** Default budget for one render call before the orchestrator cancels it. */
|
|
8
8
|
const RENDER_TIMEOUT_MS = 9e5;
|
|
9
9
|
/**
|
|
@@ -47,7 +47,6 @@ async function renderWithTimeout(runner, request, timeoutMs) {
|
|
|
47
47
|
clearTimeout(timer);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
//#endregion
|
|
51
50
|
export { RENDER_TIMEOUT_MS, renderBudgetMs, renderWithTimeout };
|
|
52
51
|
|
|
53
52
|
//# sourceMappingURL=render.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.mjs","names":[],"sources":["../../src/capture/render.ts"],"sourcesContent":["/** Render one engine's pass with a budget; shared by the orchestrator's browser loop. */\nimport type { Logger } from \"pino\";\nimport type {\n CaptureTarget,\n CaptureRunner,\n RenderResult,\n RenderedSnapshot,\n} from \"../adapters/capture-runner.ts\";\nimport { redactSecrets } from \"../utils/redact.ts\";\nimport { PER_CAPTURE_BUDGET_MS } from \"../utils/storybook-token.ts\";\n\n/** Default budget for one render call before the orchestrator cancels it. */\nexport const RENDER_TIMEOUT_MS = 15 * 60_000;\n\n/**\n * Budget for one render call: an explicit setting wins; otherwise the default\n * floor, raised to a per-capture allowance for large matrices. Bounded page\n * concurrency on small hosts makes big builds legitimately take longer than 15 minutes.\n */\nexport function renderBudgetMs(captureCount: number, explicitMs?: number): number {\n return explicitMs ?? Math.max(RENDER_TIMEOUT_MS, captureCount * PER_CAPTURE_BUDGET_MS);\n}\n\n/** Failure error text cap (persisted rows and logs stay bounded). */\nconst FAILURE_ERROR_MAX = 500;\n\n/** Render input assembled by the orchestrator for one build. */\nexport interface RenderRequest {\n buildId: string;\n /** This render's cancel handle within the build (see `CaptureRunner.render`). */\n runId?: string | undefined;\n storybookDir: string;\n stories: Parameters<CaptureRunner[\"render\"]>[0][\"stories\"];\n viewports: Parameters<CaptureRunner[\"render\"]>[0][\"viewports\"];\n logger: Logger | undefined;\n executePlay: boolean;\n playTimeoutMs: number;\n runA11y: boolean;\n target: CaptureTarget;\n /** The signed URL a `reach: \"url\"` target opens (never logged); absent for every other target. */\n storybookUrl?: string | undefined;\n onCapture?: (capture: RenderedSnapshot) => Promise<void>;\n}\n\n/** Cap failure error text so one chatty runner cannot flood rows and logs. */\nfunction capFailures(result: RenderResult): RenderResult {\n return {\n ...result,\n captures: result.captures,\n failures: result.failures.map((failure) => ({\n ...failure,\n // A runner's error can quote the URL it failed to open, which carries a signed token.\n error: redactSecrets(failure.error).slice(0, FAILURE_ERROR_MAX),\n })),\n };\n}\n\n/** Render with a timeout; cancels the runner when the budget expires. */\nexport async function renderWithTimeout(\n runner: CaptureRunner,\n request: RenderRequest,\n timeoutMs: number,\n): Promise<RenderResult> {\n let timedOut = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n timedOut = true;\n reject(new Error(`Render timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n timer?.unref?.();\n try {\n return capFailures(await Promise.race([runner.render(request), timeout]));\n } catch (error) {\n if (timedOut) {\n // Only this render: a sibling shard of the same build may be rendering in this process.\n await runner.cancel(request.buildId, request.runId).catch(() => {});\n }\n throw error;\n } finally {\n clearTimeout(timer);\n }\n}\n"],"mappings":";;;;;;;AAYA,MAAa,oBAAoB;;;;;;AAOjC,SAAgB,eAAe,cAAsB,YAA6B;CAChF,OAAO,cAAc,KAAK,IAAA,KAAuB,eAAA,IAAoC;AACvF;;AAGA,MAAM,oBAAoB;;
|
|
1
|
+
{"version":3,"file":"render.mjs","names":[],"sources":["../../src/capture/render.ts"],"sourcesContent":["/** Render one engine's pass with a budget; shared by the orchestrator's browser loop. */\nimport type { Logger } from \"pino\";\nimport type {\n CaptureTarget,\n CaptureRunner,\n RenderResult,\n RenderedSnapshot,\n} from \"../adapters/capture-runner.ts\";\nimport { redactSecrets } from \"../utils/redact.ts\";\nimport { PER_CAPTURE_BUDGET_MS } from \"../utils/storybook-token.ts\";\n\n/** Default budget for one render call before the orchestrator cancels it. */\nexport const RENDER_TIMEOUT_MS = 15 * 60_000;\n\n/**\n * Budget for one render call: an explicit setting wins; otherwise the default\n * floor, raised to a per-capture allowance for large matrices. Bounded page\n * concurrency on small hosts makes big builds legitimately take longer than 15 minutes.\n */\nexport function renderBudgetMs(captureCount: number, explicitMs?: number): number {\n return explicitMs ?? Math.max(RENDER_TIMEOUT_MS, captureCount * PER_CAPTURE_BUDGET_MS);\n}\n\n/** Failure error text cap (persisted rows and logs stay bounded). */\nconst FAILURE_ERROR_MAX = 500;\n\n/** Render input assembled by the orchestrator for one build. */\nexport interface RenderRequest {\n buildId: string;\n /** This render's cancel handle within the build (see `CaptureRunner.render`). */\n runId?: string | undefined;\n /** The shard's 1-based position in the build's plan (see `CaptureRunner.render`). */\n shardIndex?: number | undefined;\n storybookDir: string;\n stories: Parameters<CaptureRunner[\"render\"]>[0][\"stories\"];\n viewports: Parameters<CaptureRunner[\"render\"]>[0][\"viewports\"];\n logger: Logger | undefined;\n executePlay: boolean;\n playTimeoutMs: number;\n runA11y: boolean;\n target: CaptureTarget;\n /** The signed URL a `reach: \"url\"` target opens (never logged); absent for every other target. */\n storybookUrl?: string | undefined;\n onCapture?: (capture: RenderedSnapshot) => Promise<void>;\n}\n\n/** Cap failure error text so one chatty runner cannot flood rows and logs. */\nfunction capFailures(result: RenderResult): RenderResult {\n return {\n ...result,\n captures: result.captures,\n failures: result.failures.map((failure) => ({\n ...failure,\n // A runner's error can quote the URL it failed to open, which carries a signed token.\n error: redactSecrets(failure.error).slice(0, FAILURE_ERROR_MAX),\n })),\n };\n}\n\n/** Render with a timeout; cancels the runner when the budget expires. */\nexport async function renderWithTimeout(\n runner: CaptureRunner,\n request: RenderRequest,\n timeoutMs: number,\n): Promise<RenderResult> {\n let timedOut = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n timedOut = true;\n reject(new Error(`Render timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n timer?.unref?.();\n try {\n return capFailures(await Promise.race([runner.render(request), timeout]));\n } catch (error) {\n if (timedOut) {\n // Only this render: a sibling shard of the same build may be rendering in this process.\n await runner.cancel(request.buildId, request.runId).catch(() => {});\n }\n throw error;\n } finally {\n clearTimeout(timer);\n }\n}\n"],"mappings":";;;;;;;AAYA,MAAa,oBAAoB;;;;;;AAOjC,SAAgB,eAAe,cAAsB,YAA6B;CAChF,OAAO,cAAc,KAAK,IAAA,KAAuB,eAAA,IAAoC;AACvF;;AAGA,MAAM,oBAAoB;;AAuB1B,SAAS,YAAY,QAAoC;CACvD,OAAO;EACL,GAAG;EACH,UAAU,OAAO;EACjB,UAAU,OAAO,SAAS,KAAK,aAAa;GAC1C,GAAG;GAEH,OAAO,cAAc,QAAQ,KAAK,CAAC,CAAC,MAAM,GAAG,iBAAiB;EAChE,EAAE;CACJ;AACF;;AAGA,eAAsB,kBACpB,QACA,SACA,WACuB;CACvB,IAAI,WAAW;CACf,IAAI;CACJ,MAAM,UAAU,IAAI,SAAgB,GAAG,WAAW;EAChD,QAAQ,iBAAiB;GACvB,WAAW;GACX,uBAAO,IAAI,MAAM,0BAA0B,UAAU,GAAG,CAAC;EAC3D,GAAG,SAAS;CACd,CAAC;CACD,OAAO,QAAQ;CACf,IAAI;EACF,OAAO,YAAY,MAAM,QAAQ,KAAK,CAAC,OAAO,OAAO,OAAO,GAAG,OAAO,CAAC,CAAC;CAC1E,SAAS,OAAO;EACd,IAAI,UAEF,MAAM,OAAO,OAAO,QAAQ,SAAS,QAAQ,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC;EAEpE,MAAM;CACR,UAAU;EACR,aAAa,KAAK;CACpB;AACF"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
import { Logger } from "../logger.mjs";
|
|
4
|
-
//#region src/capture/scratch.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* Subtree of `scratchDir` that holds extracted Storybooks.
|
|
7
6
|
*
|
|
@@ -39,6 +38,5 @@ interface ScratchSweepOptions {
|
|
|
39
38
|
* @returns How many build scratch directories were removed.
|
|
40
39
|
*/
|
|
41
40
|
declare function sweepOrphanedScratch(scratchDir: string, options?: ScratchSweepOptions): Promise<number>;
|
|
42
|
-
//#endregion
|
|
43
41
|
export { SCRATCH_DIRNAME, SCRATCH_MAX_AGE_MS, ScratchSweepOptions, buildScratchDir, removeBuildScratch, removeJobScratch, sweepOrphanedScratch };
|
|
44
42
|
//# sourceMappingURL=scratch.d.mts.map
|
package/dist/capture/scratch.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import "node:path";
|
|
1
|
+
import { join } from "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { join } from "node:path";
|
|
5
4
|
import { readdir, rm, rmdir, stat } from "node:fs/promises";
|
|
6
|
-
|
|
5
|
+
// src/capture/scratch.ts
|
|
7
6
|
/** Per-build scratch directories: where they live, removing them, and sweeping crash leftovers. */
|
|
8
7
|
/**
|
|
9
8
|
* Subtree of `scratchDir` that holds extracted Storybooks.
|
|
@@ -132,7 +131,6 @@ async function sweepBuild(buildDir, cutoff) {
|
|
|
132
131
|
}
|
|
133
132
|
}
|
|
134
133
|
function ignore() {}
|
|
135
|
-
//#endregion
|
|
136
134
|
export { SCRATCH_DIRNAME, SCRATCH_MAX_AGE_MS, buildScratchDir, jobScratchDir, pruneBuildScratch, removeBuildScratch, removeJobScratch, removeShardScratch, shardScratchDir, sweepOrphanedScratch };
|
|
137
135
|
|
|
138
136
|
//# sourceMappingURL=scratch.mjs.map
|
|
@@ -1 +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":"
|
|
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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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
4
|
+
// src/capture/shard-plan.ts
|
|
5
5
|
/**
|
|
6
6
|
* Split the work into shards of about `size` captures, keeping a story's engines together so
|
|
7
7
|
* a newly enabled engine can still bootstrap from its counterpart in the same shard.
|
|
@@ -122,7 +122,6 @@ function restrictTo(work, ids) {
|
|
|
122
122
|
stories: engine.stories.filter((story) => ids.has(story.id))
|
|
123
123
|
})).filter((engine) => engine.stories.length > 0);
|
|
124
124
|
}
|
|
125
|
-
//#endregion
|
|
126
125
|
export { planShards, totalCaptures, worthSharding };
|
|
127
126
|
|
|
128
127
|
//# sourceMappingURL=shard-plan.mjs.map
|
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { storybookUrlSource } from "../storybook-urls.mjs";
|
|
5
|
-
|
|
5
|
+
// src/capture/shards/context.ts
|
|
6
6
|
/** A capture context for the persist and finalize steps of one stage. */
|
|
7
7
|
function contextOf(target, parts) {
|
|
8
8
|
const { options } = target;
|
|
@@ -42,10 +42,10 @@ function matrixInputOf(target, parts) {
|
|
|
42
42
|
logger: parts.logger ?? options.logger,
|
|
43
43
|
recordLog: parts.recordLog,
|
|
44
44
|
progress: parts.progress,
|
|
45
|
-
runId: parts.runId
|
|
45
|
+
runId: parts.runId,
|
|
46
|
+
shardIndex: parts.shardIndex
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
|
-
//#endregion
|
|
49
49
|
export { contextOf, matrixInputOf };
|
|
50
50
|
|
|
51
51
|
//# sourceMappingURL=context.mjs.map
|
|
@@ -1 +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 },\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 };\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,
|
|
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"}
|
|
@@ -11,7 +11,7 @@ import { contextOf, matrixInputOf } from "./context.mjs";
|
|
|
11
11
|
import { extractStorybookToScratch } from "../statics.mjs";
|
|
12
12
|
import { loadTarget } from "../target.mjs";
|
|
13
13
|
import { reduceBuild } from "./reduce.mjs";
|
|
14
|
-
|
|
14
|
+
// src/capture/shards/execute.ts
|
|
15
15
|
/** The render stage: one shard of a sharded capture, run by whichever worker picked up its job. */
|
|
16
16
|
/**
|
|
17
17
|
* Render and persist one shard, record its outcome, and, if it was the last one, finalize the build.
|
|
@@ -118,6 +118,7 @@ async function renderShard(target, shard, progress) {
|
|
|
118
118
|
const result = await renderAndPersist(target, payload, {
|
|
119
119
|
storybookDir,
|
|
120
120
|
runId: shard.id,
|
|
121
|
+
shardIndex: shard.ordinal,
|
|
121
122
|
progress
|
|
122
123
|
});
|
|
123
124
|
const durationMs = Math.round(performance.now() - started);
|
|
@@ -139,7 +140,8 @@ async function renderAndPersist(target, payload, run) {
|
|
|
139
140
|
storybookDir: run.storybookDir,
|
|
140
141
|
affectedPaths: null,
|
|
141
142
|
progress: run.progress,
|
|
142
|
-
runId: run.runId
|
|
143
|
+
runId: run.runId,
|
|
144
|
+
shardIndex: run.shardIndex
|
|
143
145
|
}), payload.browsers);
|
|
144
146
|
const failures = classifyFailures(stories, rendered.failures);
|
|
145
147
|
await clearEarlierRun(target, payload);
|
|
@@ -168,7 +170,6 @@ async function clearEarlierRun(target, payload) {
|
|
|
168
170
|
storyIds: new Set(engine.stories.map((story) => story.id))
|
|
169
171
|
})));
|
|
170
172
|
}
|
|
171
|
-
//#endregion
|
|
172
173
|
export { executeCaptureShard };
|
|
173
174
|
|
|
174
175
|
//# sourceMappingURL=execute.mjs.map
|
|
@@ -1 +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 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: { storybookDir: string; runId: string; progress: CaptureProgress | undefined },\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 }),\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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"}
|
|
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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"}
|
|
@@ -11,7 +11,7 @@ import { CaptureShardModel } from "../../models/capture-shard.mjs";
|
|
|
11
11
|
import { planShards, worthSharding } from "../shard-plan.mjs";
|
|
12
12
|
import { contextOf, matrixInputOf } from "./context.mjs";
|
|
13
13
|
import { storyDurations } from "./history.mjs";
|
|
14
|
-
|
|
14
|
+
// src/capture/shards/fan-out.ts
|
|
15
15
|
/**
|
|
16
16
|
* The plan stage. When sharding is on and the build is big enough, decide what to render, record
|
|
17
17
|
* the plan and its shards, and hand the shards to the queue; otherwise return null and the caller
|
|
@@ -120,7 +120,6 @@ async function handOut(args, rows) {
|
|
|
120
120
|
traceparent: args.input.traceparent
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
|
-
//#endregion
|
|
124
123
|
export { fanOutCapture };
|
|
125
124
|
|
|
126
125
|
//# sourceMappingURL=fan-out.mjs.map
|
|
@@ -3,7 +3,7 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { BuildModel } from "../../models/build.mjs";
|
|
5
5
|
import { SnapshotModel } from "../../models/snapshot.mjs";
|
|
6
|
-
|
|
6
|
+
// src/capture/shards/history.ts
|
|
7
7
|
/** What the project's recent builds say a story costs, so a plan can order its work by it. */
|
|
8
8
|
/**
|
|
9
9
|
* Milliseconds each story cost, taken from the newest build of this project whose snapshots carry
|
|
@@ -29,7 +29,6 @@ async function storyDurations(target) {
|
|
|
29
29
|
* rendered nothing (everything inherited) carries none, but several in a row do not happen.
|
|
30
30
|
*/
|
|
31
31
|
const BUILDS_SCANNED = 3;
|
|
32
|
-
//#endregion
|
|
33
32
|
export { storyDurations };
|
|
34
33
|
|
|
35
34
|
//# sourceMappingURL=history.mjs.map
|
|
@@ -5,7 +5,7 @@ import { BuildModel } from "../../models/build.mjs";
|
|
|
5
5
|
import { finalizeBuild } from "../pipeline.mjs";
|
|
6
6
|
import { CaptureShardModel } from "../../models/capture-shard.mjs";
|
|
7
7
|
import { contextOf } from "./context.mjs";
|
|
8
|
-
|
|
8
|
+
// src/capture/shards/reduce.ts
|
|
9
9
|
/** The reduce stage: when the last render shard ends, exactly one worker finishes the build. */
|
|
10
10
|
/**
|
|
11
11
|
* When every render shard has ended, claim the finalize (exactly one worker can) and finish the
|
|
@@ -91,7 +91,6 @@ function addAll(into, ids) {
|
|
|
91
91
|
function storyIdsOf(row) {
|
|
92
92
|
return (JSON.parse(row.payload ?? "{}").browsers ?? []).flatMap((engine) => engine.stories.map((story) => story.id));
|
|
93
93
|
}
|
|
94
|
-
//#endregion
|
|
95
94
|
export { reduceBuild };
|
|
96
95
|
|
|
97
96
|
//# sourceMappingURL=reduce.mjs.map
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
import { Viewport } from "./adapter.mjs";
|
|
4
|
-
//#region src/capture/sizing.d.ts
|
|
5
4
|
/** Default min/max dimensions for cropped/fullPage screenshots (diff stability). */
|
|
6
5
|
declare const SIZING_DEFAULTS: {
|
|
7
6
|
readonly minWidth: 100;
|
|
@@ -53,6 +52,5 @@ interface ScreenshotPlan {
|
|
|
53
52
|
declare function getScreenshotPlan(viewport: Viewport, box: BoundingBox | null, options?: SizingOptions): ScreenshotPlan;
|
|
54
53
|
/** Resolve viewports from project/server config with fallback to default (not sizing, just list). */
|
|
55
54
|
declare function resolveViewports(rawViewports: string | null | undefined, fallback: Viewport[], serverViewports?: Viewport[]): Viewport[];
|
|
56
|
-
//#endregion
|
|
57
55
|
export { BoundingBox, SIZING_DEFAULTS, ScreenshotPlan, SizingOptions, getScreenshotPlan, resolveViewports };
|
|
58
56
|
//# sourceMappingURL=sizing.d.mts.map
|
package/dist/capture/sizing.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
4
|
+
// src/capture/sizing.ts
|
|
5
5
|
/** Padding around the cropped component (8px each side). */
|
|
6
6
|
const PAD = 16;
|
|
7
7
|
const HALF_PAD = 8;
|
|
@@ -103,7 +103,6 @@ function parsedViewports(raw) {
|
|
|
103
103
|
} catch {}
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
106
|
-
//#endregion
|
|
107
106
|
export { SIZING_DEFAULTS, getScreenshotPlan, resolveViewports };
|
|
108
107
|
|
|
109
108
|
//# sourceMappingURL=sizing.mjs.map
|
|
@@ -2,12 +2,11 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { BuildModel } from "../models/build.mjs";
|
|
5
|
-
|
|
5
|
+
// src/capture/skip-checks.ts
|
|
6
6
|
/** Skip capture when another approved build already covers the same commit. */
|
|
7
7
|
async function hasApprovedBuildForSha(db, tables, projectId, sha, excludeBuildId) {
|
|
8
8
|
return (await new BuildModel(db, tables).list(projectId, { status: "approved" })).some((build) => build.gitSha === sha && build.id !== excludeBuildId);
|
|
9
9
|
}
|
|
10
|
-
//#endregion
|
|
11
10
|
export { hasApprovedBuildForSha };
|
|
12
11
|
|
|
13
12
|
//# sourceMappingURL=skip-checks.mjs.map
|
|
@@ -3,7 +3,6 @@ import __tsdown_shims_url from 'node:url';
|
|
|
3
3
|
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
4
4
|
import { StorageAdapter } from "../adapter/storage.mjs";
|
|
5
5
|
import { ExtractionLimits } from "./publish/limits.mjs";
|
|
6
|
-
//#region src/capture/statics.d.ts
|
|
7
6
|
/**
|
|
8
7
|
* Extract a build's stored zip into a dedicated scratch directory. The caller removes it when
|
|
9
8
|
* done (`removeJobScratch`).
|
|
@@ -32,6 +31,5 @@ declare function extractStorybookToScratch(storage: StorageAdapter, scratchDir:
|
|
|
32
31
|
* @param db - Optional database for content ref tracking (enables GC).
|
|
33
32
|
*/
|
|
34
33
|
declare function persistStorybookStatics(storage: StorageAdapter, sourceDir: string, projectId: string, buildId: string, db?: DatabaseAdapter): Promise<void>;
|
|
35
|
-
//#endregion
|
|
36
34
|
export { extractStorybookToScratch, persistStorybookStatics };
|
|
37
35
|
//# sourceMappingURL=statics.d.mts.map
|
package/dist/capture/statics.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "node:path";
|
|
1
|
+
import { join, relative, resolve } from "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { storybookDir, storybookZipPath } from "../paths.mjs";
|
|
@@ -6,10 +6,9 @@ import { mapLimit } from "../utils/map-limit.mjs";
|
|
|
6
6
|
import { jobScratchDir, shardScratchDir } from "./scratch.mjs";
|
|
7
7
|
import { DEFAULT_MAX_UNZIPPED_BYTES, DEFAULT_MAX_ZIP_ENTRIES } from "./publish/limits.mjs";
|
|
8
8
|
import { extractZipStream } from "./publish/extract.mjs";
|
|
9
|
-
import { join, relative, resolve } from "node:path";
|
|
10
9
|
import { createHash } from "node:crypto";
|
|
11
10
|
import { mkdir, readFile, readdir, rm } from "node:fs/promises";
|
|
12
|
-
|
|
11
|
+
// src/capture/statics.ts
|
|
13
12
|
/**
|
|
14
13
|
* Shared Storybook statics handling: unzip a stored build into a scratch
|
|
15
14
|
* directory and persist the extracted files back to storage. Used by the
|
|
@@ -106,7 +105,6 @@ async function walkFiles(dir) {
|
|
|
106
105
|
return [full];
|
|
107
106
|
}))).flat();
|
|
108
107
|
}
|
|
109
|
-
//#endregion
|
|
110
108
|
export { extractStorybookToScratch, persistStorybookStatics };
|
|
111
109
|
|
|
112
110
|
//# sourceMappingURL=statics.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statics.mjs","names":[],"sources":["../../src/capture/statics.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdir, readdir, readFile, rm } from \"node:fs/promises\";\nimport { join, relative, resolve } from \"node:path\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { mapLimit } from \"../utils/map-limit.ts\";\nimport { storybookDir, storybookZipPath } from \"../utils/paths.ts\";\nimport { extractZipStream } from \"./publish/extract.ts\";\nimport {\n DEFAULT_MAX_UNZIPPED_BYTES,\n DEFAULT_MAX_ZIP_ENTRIES,\n type ExtractionLimits,\n} from \"./publish/limits.ts\";\nimport { jobScratchDir, shardScratchDir } from \"./scratch.ts\";\n\n/**\n * Shared Storybook statics handling: unzip a stored build into a scratch\n * directory and persist the extracted files back to storage. Used by the\n * capture orchestrator and by the upload path for opt-in inline extraction\n * (`maxInlineUnzipSize`); both write identical keys, so either may run first.\n */\n\nconst DEFAULT_EXTRACTION_LIMITS: ExtractionLimits = {\n maxEntries: DEFAULT_MAX_ZIP_ENTRIES,\n maxBytes: DEFAULT_MAX_UNZIPPED_BYTES,\n};\n\n/**\n * Extract a build's stored zip into a dedicated scratch directory. The caller removes it when\n * done (`removeJobScratch`).\n *\n * @param storage - Storage holding the uploaded zip.\n * @param scratchDir - Root under which per-build scratch dirs are created.\n * @param projectId - Owning project id.\n * @param buildId - Build whose zip to extract.\n * @param shardId - Shard extracting it; gives the shard a directory of its own.\n * @param limits - Entry and uncompressed-byte caps (safe defaults when omitted).\n * @returns The extracted Storybook directory.\n * @throws {ZipLimitError} When the archive breaks a cap or escapes the target directory.\n */\nexport async function extractStorybookToScratch(\n storage: StorageAdapter,\n scratchDir: string,\n projectId: string,\n buildId: string,\n shardId?: string,\n limits: ExtractionLimits = DEFAULT_EXTRACTION_LIMITS,\n): Promise<string> {\n const base = shardId\n ? shardScratchDir(scratchDir, projectId, buildId, shardId)\n : jobScratchDir(scratchDir, projectId, buildId);\n const targetDir = join(base, \"storybook\");\n const root = await prepareTargetDir(targetDir);\n const source = await storage.readStream(storybookZipPath(projectId, buildId));\n await extractZipStream(source, root, limits);\n return targetDir;\n}\n\n/** Reset a scratch target directory, returning its resolved root. */\nasync function prepareTargetDir(targetDir: string): Promise<string> {\n await rm(targetDir, { recursive: true, force: true });\n await mkdir(targetDir, { recursive: true });\n return resolve(targetDir);\n}\n\n/**\n * Persist an extracted Storybook directory to storage for published serving.\n *\n * Uses content-addressed deduplication: each file is hashed and stored once\n * under `content/<hash>`, with a per-build manifest at\n * `${projectId}/builds/${buildId}/storybook/manifest.json`.\n *\n * @param storage - Destination storage adapter.\n * @param sourceDir - Extracted Storybook directory.\n * @param projectId - Owning project id.\n * @param buildId - Build the files belong to.\n * @param db - Optional database for content ref tracking (enables GC).\n */\nexport async function persistStorybookStatics(\n storage: StorageAdapter,\n sourceDir: string,\n projectId: string,\n buildId: string,\n db?: DatabaseAdapter,\n): Promise<void> {\n const root = resolve(sourceDir);\n const destinationPrefix = storybookDir(projectId, buildId);\n const files = await walkFiles(root);\n const manifest: Record<string, string> = {};\n // Bounded: a large Storybook has thousands of files, and buffering them all\n // at once exhausts memory on small hosts. Each file is read fully before any\n // write: the scratch dir is often the storage root, so the per-build path\n // below can be the very file being read (a streamed write would truncate it).\n await mapLimit(files, STATICS_CONCURRENCY, async (file) => {\n const rel = relative(root, file);\n const buffer = await readFile(file);\n const hash = createHash(\"sha256\").update(buffer).digest(\"hex\");\n manifest[rel] = hash;\n // Only write if not already exists (dedup)\n if (!(await storage.exists(`content/${hash}`))) {\n await storage.write(`content/${hash}`, buffer);\n }\n // Also write to legacy per-build path for backward compat (can be removed after migration)\n // and maintain content_refs for GC\n await storage.write(`${destinationPrefix}/${rel}`, buffer);\n if (db) {\n await upsertContentRef(db, hash);\n }\n });\n await storage.write(`${destinationPrefix}/manifest.json`, Buffer.from(JSON.stringify(manifest)));\n}\n\n/** Files persisted at once; bounds how many file buffers are in memory together. */\nconst STATICS_CONCURRENCY = 8;\n\nasync function upsertContentRef(db: DatabaseAdapter, hash: string): Promise<void> {\n try {\n const contentRefs = db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = (await db.get(contentRefs, hash)) as unknown as { refCount: number } | null;\n if (existing) {\n await db.update(contentRefs, hash, {\n refCount: existing.refCount + 1,\n lastSeenAt: now,\n });\n } else {\n await db.insert(contentRefs, { hash, refCount: 1, lastSeenAt: now, createdAt: now });\n }\n } catch {\n // content_refs table may not exist yet (old DB) — ignore\n }\n}\n\nasync function walkFiles(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const nested = await Promise.all(\n entries.map(async (entry) => {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n return await walkFiles(full);\n }\n return [full];\n }),\n );\n return nested.flat();\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"statics.mjs","names":[],"sources":["../../src/capture/statics.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdir, readdir, readFile, rm } from \"node:fs/promises\";\nimport { join, relative, resolve } from \"node:path\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { mapLimit } from \"../utils/map-limit.ts\";\nimport { storybookDir, storybookZipPath } from \"../utils/paths.ts\";\nimport { extractZipStream } from \"./publish/extract.ts\";\nimport {\n DEFAULT_MAX_UNZIPPED_BYTES,\n DEFAULT_MAX_ZIP_ENTRIES,\n type ExtractionLimits,\n} from \"./publish/limits.ts\";\nimport { jobScratchDir, shardScratchDir } from \"./scratch.ts\";\n\n/**\n * Shared Storybook statics handling: unzip a stored build into a scratch\n * directory and persist the extracted files back to storage. Used by the\n * capture orchestrator and by the upload path for opt-in inline extraction\n * (`maxInlineUnzipSize`); both write identical keys, so either may run first.\n */\n\nconst DEFAULT_EXTRACTION_LIMITS: ExtractionLimits = {\n maxEntries: DEFAULT_MAX_ZIP_ENTRIES,\n maxBytes: DEFAULT_MAX_UNZIPPED_BYTES,\n};\n\n/**\n * Extract a build's stored zip into a dedicated scratch directory. The caller removes it when\n * done (`removeJobScratch`).\n *\n * @param storage - Storage holding the uploaded zip.\n * @param scratchDir - Root under which per-build scratch dirs are created.\n * @param projectId - Owning project id.\n * @param buildId - Build whose zip to extract.\n * @param shardId - Shard extracting it; gives the shard a directory of its own.\n * @param limits - Entry and uncompressed-byte caps (safe defaults when omitted).\n * @returns The extracted Storybook directory.\n * @throws {ZipLimitError} When the archive breaks a cap or escapes the target directory.\n */\nexport async function extractStorybookToScratch(\n storage: StorageAdapter,\n scratchDir: string,\n projectId: string,\n buildId: string,\n shardId?: string,\n limits: ExtractionLimits = DEFAULT_EXTRACTION_LIMITS,\n): Promise<string> {\n const base = shardId\n ? shardScratchDir(scratchDir, projectId, buildId, shardId)\n : jobScratchDir(scratchDir, projectId, buildId);\n const targetDir = join(base, \"storybook\");\n const root = await prepareTargetDir(targetDir);\n const source = await storage.readStream(storybookZipPath(projectId, buildId));\n await extractZipStream(source, root, limits);\n return targetDir;\n}\n\n/** Reset a scratch target directory, returning its resolved root. */\nasync function prepareTargetDir(targetDir: string): Promise<string> {\n await rm(targetDir, { recursive: true, force: true });\n await mkdir(targetDir, { recursive: true });\n return resolve(targetDir);\n}\n\n/**\n * Persist an extracted Storybook directory to storage for published serving.\n *\n * Uses content-addressed deduplication: each file is hashed and stored once\n * under `content/<hash>`, with a per-build manifest at\n * `${projectId}/builds/${buildId}/storybook/manifest.json`.\n *\n * @param storage - Destination storage adapter.\n * @param sourceDir - Extracted Storybook directory.\n * @param projectId - Owning project id.\n * @param buildId - Build the files belong to.\n * @param db - Optional database for content ref tracking (enables GC).\n */\nexport async function persistStorybookStatics(\n storage: StorageAdapter,\n sourceDir: string,\n projectId: string,\n buildId: string,\n db?: DatabaseAdapter,\n): Promise<void> {\n const root = resolve(sourceDir);\n const destinationPrefix = storybookDir(projectId, buildId);\n const files = await walkFiles(root);\n const manifest: Record<string, string> = {};\n // Bounded: a large Storybook has thousands of files, and buffering them all\n // at once exhausts memory on small hosts. Each file is read fully before any\n // write: the scratch dir is often the storage root, so the per-build path\n // below can be the very file being read (a streamed write would truncate it).\n await mapLimit(files, STATICS_CONCURRENCY, async (file) => {\n const rel = relative(root, file);\n const buffer = await readFile(file);\n const hash = createHash(\"sha256\").update(buffer).digest(\"hex\");\n manifest[rel] = hash;\n // Only write if not already exists (dedup)\n if (!(await storage.exists(`content/${hash}`))) {\n await storage.write(`content/${hash}`, buffer);\n }\n // Also write to legacy per-build path for backward compat (can be removed after migration)\n // and maintain content_refs for GC\n await storage.write(`${destinationPrefix}/${rel}`, buffer);\n if (db) {\n await upsertContentRef(db, hash);\n }\n });\n await storage.write(`${destinationPrefix}/manifest.json`, Buffer.from(JSON.stringify(manifest)));\n}\n\n/** Files persisted at once; bounds how many file buffers are in memory together. */\nconst STATICS_CONCURRENCY = 8;\n\nasync function upsertContentRef(db: DatabaseAdapter, hash: string): Promise<void> {\n try {\n const contentRefs = db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = (await db.get(contentRefs, hash)) as unknown as { refCount: number } | null;\n if (existing) {\n await db.update(contentRefs, hash, {\n refCount: existing.refCount + 1,\n lastSeenAt: now,\n });\n } else {\n await db.insert(contentRefs, { hash, refCount: 1, lastSeenAt: now, createdAt: now });\n }\n } catch {\n // content_refs table may not exist yet (old DB) — ignore\n }\n}\n\nasync function walkFiles(dir: string): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const nested = await Promise.all(\n entries.map(async (entry) => {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n return await walkFiles(full);\n }\n return [full];\n }),\n );\n return nested.flat();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,MAAM,4BAA8C;CAClD,YAAY;CACZ,UAAU;AACZ;;;;;;;;;;;;;;AAeA,eAAsB,0BACpB,SACA,YACA,WACA,SACA,SACA,SAA2B,2BACV;CACjB,MAAM,OAAO,UACT,gBAAgB,YAAY,WAAW,SAAS,OAAO,IACvD,cAAc,YAAY,WAAW,OAAO;CAChD,MAAM,YAAY,KAAK,MAAM,WAAW;CACxC,MAAM,OAAO,MAAM,iBAAiB,SAAS;CAC7C,MAAM,SAAS,MAAM,QAAQ,WAAW,iBAAiB,WAAW,OAAO,CAAC;CAC5E,MAAM,iBAAiB,QAAQ,MAAM,MAAM;CAC3C,OAAO;AACT;;AAGA,eAAe,iBAAiB,WAAoC;CAClE,MAAM,GAAG,WAAW;EAAE,WAAW;EAAM,OAAO;CAAK,CAAC;CACpD,MAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAC1C,OAAO,QAAQ,SAAS;AAC1B;;;;;;;;;;;;;;AAeA,eAAsB,wBACpB,SACA,WACA,WACA,SACA,IACe;CACf,MAAM,OAAO,QAAQ,SAAS;CAC9B,MAAM,oBAAoB,aAAa,WAAW,OAAO;CACzD,MAAM,QAAQ,MAAM,UAAU,IAAI;CAClC,MAAM,WAAmC,CAAC;CAK1C,MAAM,SAAS,OAAO,qBAAqB,OAAO,SAAS;EACzD,MAAM,MAAM,SAAS,MAAM,IAAI;EAC/B,MAAM,SAAS,MAAM,SAAS,IAAI;EAClC,MAAM,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,OAAO,KAAK;EAC7D,SAAS,OAAO;EAEhB,IAAI,CAAE,MAAM,QAAQ,OAAO,WAAW,MAAM,GAC1C,MAAM,QAAQ,MAAM,WAAW,QAAQ,MAAM;EAI/C,MAAM,QAAQ,MAAM,GAAG,kBAAkB,GAAG,OAAO,MAAM;EACzD,IAAI,IACF,MAAM,iBAAiB,IAAI,IAAI;CAEnC,CAAC;CACD,MAAM,QAAQ,MAAM,GAAG,kBAAkB,iBAAiB,OAAO,KAAK,KAAK,UAAU,QAAQ,CAAC,CAAC;AACjG;;AAGA,MAAM,sBAAsB;AAE5B,eAAe,iBAAiB,IAAqB,MAA6B;CAChF,IAAI;EACF,MAAM,cAAc,GAAG,OAAO;EAC9B,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,MAAM,WAAY,MAAM,GAAG,IAAI,aAAa,IAAI;EAChD,IAAI,UACF,MAAM,GAAG,OAAO,aAAa,MAAM;GACjC,UAAU,SAAS,WAAW;GAC9B,YAAY;EACd,CAAC;OAED,MAAM,GAAG,OAAO,aAAa;GAAE;GAAM,UAAU;GAAG,YAAY;GAAK,WAAW;EAAI,CAAC;CAEvF,QAAQ,CAER;AACF;AAEA,eAAe,UAAU,KAAgC;CACvD,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;CAU1D,QAAO,MATc,QAAQ,IAC3B,QAAQ,IAAI,OAAO,UAAU;EAC3B,MAAM,OAAO,KAAK,KAAK,MAAM,IAAI;EACjC,IAAI,MAAM,YAAY,GACpB,OAAO,MAAM,UAAU,IAAI;EAE7B,OAAO,CAAC,IAAI;CACd,CAAC,CACH,EAAA,CACc,KAAK;AACrB"}
|
|
@@ -5,7 +5,6 @@ import { CaptureRunner } from "../adapter/capture-runner.mjs";
|
|
|
5
5
|
import { SecretInput } from "../utils/encrypt.mjs";
|
|
6
6
|
import { Build } from "../schema/build.mjs";
|
|
7
7
|
import { Project } from "../schema/project.mjs";
|
|
8
|
-
//#region src/capture/storybook-urls.d.ts
|
|
9
8
|
/** What a pass tells the source: how long it may run, which sizes the token's lifetime. */
|
|
10
9
|
interface StorybookUrlPass {
|
|
11
10
|
/** The render budget of the pass in ms (`renderBudgetMs`); unset when the pass has none. */
|
|
@@ -35,6 +34,5 @@ declare function storybookUrlSource(options: StorybookUrlOptions, subject: {
|
|
|
35
34
|
build: Pick<Build, "id" | "projectId">;
|
|
36
35
|
project: Pick<Project, "storybookUrlTtlSeconds">;
|
|
37
36
|
}): StorybookUrlSource;
|
|
38
|
-
//#endregion
|
|
39
37
|
export { StorybookUrlOptions, StorybookUrlPass, StorybookUrlSource, storybookUrlSource };
|
|
40
38
|
//# sourceMappingURL=storybook-urls.d.mts.map
|
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { signStorybookToken, storybookTokenPath, storybookUrlTtl } from "../utils/storybook-token.mjs";
|
|
5
|
-
|
|
5
|
+
// src/capture/storybook-urls.ts
|
|
6
6
|
/**
|
|
7
7
|
* A source of signed URLs for one build. Every call mints a fresh token, so a shard that waited in
|
|
8
8
|
* the queue or a retried pass is never handed a token minted before it started.
|
|
@@ -29,13 +29,13 @@ function mint(secret, subject, pass) {
|
|
|
29
29
|
secret,
|
|
30
30
|
projectId,
|
|
31
31
|
buildId,
|
|
32
|
-
ttlSeconds
|
|
32
|
+
ttlSeconds,
|
|
33
|
+
purpose: "capture"
|
|
33
34
|
}));
|
|
34
35
|
}
|
|
35
36
|
function needsUrl(runner, target) {
|
|
36
37
|
return runner.metadata.targets?.some((t) => t.target === target && t.reach === "url") ?? false;
|
|
37
38
|
}
|
|
38
|
-
//#endregion
|
|
39
39
|
export { storybookUrlSource };
|
|
40
40
|
|
|
41
41
|
//# sourceMappingURL=storybook-urls.mjs.map
|