@storyshelf/core 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -32
- package/dist/adapter/capture-queue.d.mts +44 -2
- package/dist/adapter/capture-runner.d.mts +127 -10
- package/dist/adapter/capture-runner.mjs +5 -12
- package/dist/adapter/database.d.mts +27 -8
- package/dist/adapter/database.mjs.map +1 -1
- package/dist/adapter/design.d.mts +85 -0
- package/dist/adapter/design.mjs +28 -0
- package/dist/adapter/design.mjs.map +1 -0
- package/dist/adapter/metadata.d.mts +3 -10
- package/dist/adapter/notifier.d.mts +6 -3
- package/dist/adapter/notifier.mjs +5 -1
- package/dist/adapter/oauth.d.mts +52 -0
- package/dist/adapter/setup.d.mts +4 -5
- package/dist/adapter/setup.mjs +2 -1
- package/dist/adapter/setup.mjs.map +1 -1
- package/dist/adapter/webhook-events.mjs +1 -1
- package/dist/adapters/capture-target-offers.d.mts +28 -0
- package/dist/adapters/capture-target-offers.mjs +58 -0
- package/dist/adapters/capture-target-offers.mjs.map +1 -0
- package/dist/adapters/capture-target.d.mts +71 -0
- package/dist/adapters/capture-target.mjs +76 -0
- package/dist/adapters/capture-target.mjs.map +1 -0
- package/dist/adapters/notifier/build-comment.d.mts +24 -0
- package/dist/adapters/notifier/build-comment.mjs +24 -0
- package/dist/adapters/notifier/build-comment.mjs.map +1 -0
- package/dist/adapters/notifier/build-state.d.mts +99 -0
- package/dist/adapters/notifier/build-state.mjs +218 -0
- package/dist/adapters/notifier/build-state.mjs.map +1 -0
- package/dist/adapters/notifier/events.d.mts +4 -2
- package/dist/adapters/notifier/events.mjs +31 -18
- package/dist/adapters/notifier/events.mjs.map +1 -1
- package/dist/adapters/notifier/provider.d.mts +53 -7
- package/dist/adapters/notifier/provider.mjs +12 -0
- package/dist/adapters/notifier/provider.mjs.map +1 -0
- package/dist/adapters/notifier/state.d.mts +24 -0
- package/dist/adapters/notifier/state.mjs +66 -0
- package/dist/adapters/notifier/state.mjs.map +1 -0
- package/dist/adapters/notifier/types.d.mts +51 -2
- package/dist/adapters/remote-browser.d.mts +87 -0
- package/dist/adapters/remote-browser.mjs +103 -0
- package/dist/adapters/remote-browser.mjs.map +1 -0
- package/dist/adapters/render-runs.d.mts +25 -0
- package/dist/adapters/render-runs.mjs +41 -0
- package/dist/adapters/render-runs.mjs.map +1 -0
- package/dist/adapters/setup-runner.mjs +2 -4
- package/dist/adapters/setup-runner.mjs.map +1 -1
- package/dist/adapters/sources.d.mts +12 -0
- package/dist/adapters/sources.mjs +20 -0
- package/dist/adapters/sources.mjs.map +1 -0
- package/dist/adapters/validate.mjs +25 -18
- package/dist/adapters/validate.mjs.map +1 -1
- package/dist/ai.d.mts +105 -0
- package/dist/ai.mjs +19 -0
- package/dist/ai.mjs.map +1 -0
- package/dist/capture/a11y/axe-source.d.mts +18 -0
- package/dist/capture/a11y/axe-source.mjs +37 -0
- package/dist/capture/a11y/axe-source.mjs.map +1 -0
- package/dist/capture/a11y/in-page.d.mts +27 -0
- package/dist/capture/a11y/in-page.mjs +60 -0
- package/dist/capture/a11y/in-page.mjs.map +1 -0
- package/dist/capture/a11y/map.d.mts +24 -0
- package/dist/capture/a11y/map.mjs +48 -0
- package/dist/capture/a11y/map.mjs.map +1 -0
- package/dist/capture/a11y/options.d.mts +23 -0
- package/dist/capture/a11y/options.mjs +75 -0
- package/dist/capture/a11y/options.mjs.map +1 -0
- package/dist/capture/adapter.d.mts +41 -3
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/affected.d.mts +2 -0
- package/dist/capture/affected.mjs +13 -4
- package/dist/capture/affected.mjs.map +1 -1
- package/dist/capture/attempt-logger.mjs +84 -0
- package/dist/capture/attempt-logger.mjs.map +1 -0
- package/dist/capture/browsers.d.mts +35 -0
- package/dist/capture/browsers.mjs +67 -0
- package/dist/capture/browsers.mjs.map +1 -0
- package/dist/capture/build-state/deliver.d.mts +32 -0
- package/dist/capture/build-state/deliver.mjs +118 -0
- package/dist/capture/build-state/deliver.mjs.map +1 -0
- package/dist/capture/build-state/index.d.mts +8 -0
- package/dist/capture/build-state/notify-job.d.mts +56 -0
- package/dist/capture/build-state/notify-job.mjs +133 -0
- package/dist/capture/build-state/notify-job.mjs.map +1 -0
- package/dist/capture/build-state/publisher.d.mts +23 -0
- package/dist/capture/build-state/publisher.mjs +107 -0
- package/dist/capture/build-state/publisher.mjs.map +1 -0
- package/dist/capture/build-state/resume.d.mts +9 -0
- package/dist/capture/build-state/resume.mjs +47 -0
- package/dist/capture/build-state/resume.mjs.map +1 -0
- package/dist/capture/build-state/types.d.mts +49 -0
- package/dist/capture/catalog/catalogue.d.mts +22 -0
- package/dist/capture/catalog/catalogue.mjs +53 -0
- package/dist/capture/catalog/catalogue.mjs.map +1 -0
- package/dist/capture/catalog/components.d.mts +23 -0
- package/dist/capture/catalog/components.mjs +55 -0
- package/dist/capture/catalog/components.mjs.map +1 -0
- package/dist/capture/catalog/history.d.mts +30 -0
- package/dist/capture/catalog/history.mjs +51 -0
- package/dist/capture/catalog/history.mjs.map +1 -0
- package/dist/capture/catalog/hook.d.mts +14 -0
- package/dist/capture/catalog/hook.mjs +19 -0
- package/dist/capture/catalog/hook.mjs.map +1 -0
- package/dist/capture/catalog/index.d.mts +11 -0
- package/dist/capture/catalog/load.d.mts +26 -0
- package/dist/capture/catalog/load.mjs +49 -0
- package/dist/capture/catalog/load.mjs.map +1 -0
- package/dist/capture/catalog/parse.d.mts +15 -0
- package/dist/capture/catalog/parse.mjs +56 -0
- package/dist/capture/catalog/parse.mjs.map +1 -0
- package/dist/capture/catalog/read.d.mts +12 -0
- package/dist/capture/catalog/read.mjs +33 -0
- package/dist/capture/catalog/read.mjs.map +1 -0
- package/dist/capture/catalog/types.d.mts +33 -0
- package/dist/capture/consumer/capture-handler.d.mts +58 -0
- package/dist/capture/consumer/capture-handler.mjs +118 -0
- package/dist/capture/consumer/capture-handler.mjs.map +1 -0
- package/dist/capture/consumer/idle.d.mts +17 -0
- package/dist/capture/consumer/idle.mjs +25 -0
- package/dist/capture/consumer/idle.mjs.map +1 -0
- package/dist/capture/consumer/index.d.mts +13 -0
- package/dist/capture/consumer/index.mjs +139 -0
- package/dist/capture/consumer/index.mjs.map +1 -0
- package/dist/capture/consumer/loop.mjs +110 -0
- package/dist/capture/consumer/loop.mjs.map +1 -0
- package/dist/capture/consumer/metrics.d.mts +41 -0
- package/dist/capture/consumer/metrics.mjs +38 -0
- package/dist/capture/consumer/metrics.mjs.map +1 -0
- package/dist/capture/consumer/retry.d.mts +33 -0
- package/dist/capture/consumer/retry.mjs +34 -0
- package/dist/capture/consumer/retry.mjs.map +1 -0
- package/dist/capture/consumer/run-job.mjs +195 -0
- package/dist/capture/consumer/run-job.mjs.map +1 -0
- package/dist/capture/consumer/startup.mjs +38 -0
- package/dist/capture/consumer/startup.mjs.map +1 -0
- package/dist/capture/consumer/types.d.mts +97 -0
- package/dist/capture/crash-failure.d.mts +25 -0
- package/dist/capture/crash-failure.mjs +70 -0
- package/dist/capture/crash-failure.mjs.map +1 -0
- package/dist/capture/db-queue/cadence.d.mts +32 -0
- package/dist/capture/db-queue/cadence.mjs +38 -0
- package/dist/capture/db-queue/cadence.mjs.map +1 -0
- package/dist/capture/db-queue/claim.d.mts +9 -0
- package/dist/capture/db-queue/claim.mjs +132 -0
- package/dist/capture/db-queue/claim.mjs.map +1 -0
- package/dist/capture/db-queue/clock.mjs +12 -0
- package/dist/capture/db-queue/clock.mjs.map +1 -0
- package/dist/capture/db-queue/codec.mjs +76 -0
- package/dist/capture/db-queue/codec.mjs.map +1 -0
- package/dist/capture/db-queue/index.d.mts +20 -0
- package/dist/capture/db-queue/index.mjs +89 -0
- package/dist/capture/db-queue/index.mjs.map +1 -0
- package/dist/capture/db-queue/keeper.d.mts +36 -0
- package/dist/capture/db-queue/keeper.mjs +92 -0
- package/dist/capture/db-queue/keeper.mjs.map +1 -0
- package/dist/capture/db-queue/lease.mjs +46 -0
- package/dist/capture/db-queue/lease.mjs.map +1 -0
- package/dist/capture/db-queue/lifecycle.mjs +36 -0
- package/dist/capture/db-queue/lifecycle.mjs.map +1 -0
- package/dist/capture/db-queue/operations.mjs +94 -0
- package/dist/capture/db-queue/operations.mjs.map +1 -0
- package/dist/capture/db-queue/poll-loop.d.mts +30 -0
- package/dist/capture/db-queue/poll-loop.mjs +89 -0
- package/dist/capture/db-queue/poll-loop.mjs.map +1 -0
- package/dist/capture/db-queue/poll.d.mts +10 -0
- package/dist/capture/db-queue/poll.mjs +69 -0
- package/dist/capture/db-queue/poll.mjs.map +1 -0
- package/dist/capture/db-queue/timers.d.mts +12 -0
- package/dist/capture/db-queue/timers.mjs +37 -0
- package/dist/capture/db-queue/timers.mjs.map +1 -0
- package/dist/capture/db-queue/types.d.mts +83 -0
- package/dist/capture/db-queue/types.mjs +12 -0
- package/dist/capture/db-queue/types.mjs.map +1 -0
- package/dist/capture/db-queue/wake.mjs +48 -0
- package/dist/capture/db-queue/wake.mjs.map +1 -0
- package/dist/capture/design.mjs +39 -0
- package/dist/capture/design.mjs.map +1 -0
- package/dist/capture/diff-record.mjs +23 -3
- package/dist/capture/diff-record.mjs.map +1 -1
- package/dist/capture/dispatch.d.mts +12 -7
- package/dist/capture/dispatch.mjs +86 -107
- package/dist/capture/dispatch.mjs.map +1 -1
- package/dist/capture/enrich.mjs +60 -0
- package/dist/capture/enrich.mjs.map +1 -0
- package/dist/capture/failure-sets.mjs +22 -0
- package/dist/capture/failure-sets.mjs.map +1 -0
- package/dist/capture/fingerprint.d.mts +42 -0
- package/dist/capture/fingerprint.mjs +83 -0
- package/dist/capture/fingerprint.mjs.map +1 -0
- package/dist/capture/forked/channel.d.mts +25 -0
- package/dist/capture/forked/child.d.mts +34 -0
- package/dist/capture/forked/child.mjs +202 -0
- package/dist/capture/forked/child.mjs.map +1 -0
- package/dist/capture/forked/crash-tracker.mjs +23 -0
- package/dist/capture/forked/crash-tracker.mjs.map +1 -0
- package/dist/capture/forked/decision.d.mts +26 -0
- package/dist/capture/forked/decision.mjs +27 -0
- package/dist/capture/forked/decision.mjs.map +1 -0
- package/dist/capture/forked/exit.mjs +12 -0
- package/dist/capture/forked/exit.mjs.map +1 -0
- package/dist/capture/forked/fork.d.mts +31 -0
- package/dist/capture/forked/fork.mjs +93 -0
- package/dist/capture/forked/fork.mjs.map +1 -0
- package/dist/capture/forked/forked-capture.d.mts +40 -0
- package/dist/capture/forked/forked-capture.mjs +240 -0
- package/dist/capture/forked/forked-capture.mjs.map +1 -0
- package/dist/capture/forked/lease-book.mjs +46 -0
- package/dist/capture/forked/lease-book.mjs.map +1 -0
- package/dist/capture/forked/protocol.d.mts +38 -0
- package/dist/capture/forked/protocol.mjs +25 -0
- package/dist/capture/forked/protocol.mjs.map +1 -0
- package/dist/capture/forked/runs.mjs +75 -0
- package/dist/capture/forked/runs.mjs.map +1 -0
- package/dist/capture/isolate.mjs +141 -0
- package/dist/capture/isolate.mjs.map +1 -0
- package/dist/capture/job-lease.d.mts +28 -0
- package/dist/capture/job-lease.mjs +57 -0
- package/dist/capture/job-lease.mjs.map +1 -0
- package/dist/capture/matrix.mjs +298 -0
- package/dist/capture/matrix.mjs.map +1 -0
- package/dist/capture/modes.d.mts +25 -0
- package/dist/capture/modes.mjs +107 -0
- package/dist/capture/modes.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +37 -5
- package/dist/capture/orchestrator.mjs +118 -107
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +9 -2
- package/dist/capture/pipeline.mjs +250 -45
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/progress.d.mts +11 -0
- package/dist/capture/progress.mjs +28 -0
- package/dist/capture/progress.mjs.map +1 -0
- package/dist/capture/publish/accept.d.mts +39 -0
- package/dist/capture/publish/accept.mjs +28 -0
- package/dist/capture/publish/accept.mjs.map +1 -0
- package/dist/capture/publish/complete.d.mts +46 -0
- package/dist/capture/publish/complete.mjs +64 -0
- package/dist/capture/publish/complete.mjs.map +1 -0
- package/dist/capture/publish/extract.d.mts +20 -0
- package/dist/capture/publish/extract.mjs +119 -0
- package/dist/capture/publish/extract.mjs.map +1 -0
- package/dist/capture/publish/handler.d.mts +28 -0
- package/dist/capture/publish/handler.mjs +62 -0
- package/dist/capture/publish/handler.mjs.map +1 -0
- package/dist/capture/publish/index.d.mts +10 -0
- package/dist/capture/publish/limits.d.mts +37 -0
- package/dist/capture/publish/limits.mjs +39 -0
- package/dist/capture/publish/limits.mjs.map +1 -0
- package/dist/capture/publish/published.d.mts +15 -0
- package/dist/capture/publish/published.mjs +18 -0
- package/dist/capture/publish/published.mjs.map +1 -0
- package/dist/capture/publish/run.d.mts +33 -0
- package/dist/capture/publish/run.mjs +30 -0
- package/dist/capture/publish/run.mjs.map +1 -0
- package/dist/capture/rediff.d.mts +1 -1
- package/dist/capture/rediff.mjs +4 -3
- package/dist/capture/rediff.mjs.map +1 -1
- package/dist/capture/render.mjs +53 -0
- package/dist/capture/render.mjs.map +1 -0
- package/dist/capture/scratch.d.mts +44 -0
- package/dist/capture/scratch.mjs +138 -0
- package/dist/capture/scratch.mjs.map +1 -0
- package/dist/capture/shard-plan.mjs +128 -0
- package/dist/capture/shard-plan.mjs.map +1 -0
- package/dist/capture/shards/context.mjs +51 -0
- package/dist/capture/shards/context.mjs.map +1 -0
- package/dist/capture/shards/execute.mjs +174 -0
- package/dist/capture/shards/execute.mjs.map +1 -0
- package/dist/capture/shards/fan-out.mjs +126 -0
- package/dist/capture/shards/fan-out.mjs.map +1 -0
- package/dist/capture/shards/history.mjs +35 -0
- package/dist/capture/shards/history.mjs.map +1 -0
- package/dist/capture/shards/reduce.mjs +97 -0
- package/dist/capture/shards/reduce.mjs.map +1 -0
- package/dist/capture/sizing.mjs +1 -11
- package/dist/capture/sizing.mjs.map +1 -1
- package/dist/capture/skip-checks.mjs +2 -65
- package/dist/capture/skip-checks.mjs.map +1 -1
- package/dist/capture/statics.d.mts +7 -8
- package/dist/capture/statics.mjs +25 -71
- package/dist/capture/statics.mjs.map +1 -1
- package/dist/capture/storybook-urls.d.mts +40 -0
- package/dist/capture/storybook-urls.mjs +41 -0
- package/dist/capture/storybook-urls.mjs.map +1 -0
- package/dist/capture/storybook.d.mts +12 -4
- package/dist/capture/storybook.mjs +56 -16
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/capture/target.mjs +21 -0
- package/dist/capture/target.mjs.map +1 -0
- package/dist/capture/targets.d.mts +25 -0
- package/dist/capture/targets.mjs +32 -0
- package/dist/capture/targets.mjs.map +1 -0
- package/dist/capture/tests.d.mts +37 -0
- package/dist/capture/tests.mjs +126 -0
- package/dist/capture/tests.mjs.map +1 -0
- package/dist/capture/token-chunks.mjs +74 -0
- package/dist/capture/token-chunks.mjs.map +1 -0
- package/dist/capture.d.mts +60 -7
- package/dist/capture.mjs +48 -3
- package/dist/config.d.mts +121 -12
- package/dist/config.mjs +9 -6
- package/dist/config.mjs.map +1 -1
- package/dist/diff.d.mts +9 -3
- package/dist/diff.mjs +16 -3
- package/dist/diff.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/insights/budget.d.mts +20 -0
- package/dist/insights/budget.mjs +33 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/envelope.d.mts +59 -0
- package/dist/insights/envelope.mjs +60 -0
- package/dist/insights/envelope.mjs.map +1 -0
- package/dist/insights/evidence-health.d.mts +32 -0
- package/dist/insights/evidence-health.mjs +28 -0
- package/dist/insights/evidence-health.mjs.map +1 -0
- package/dist/insights/evidence.d.mts +55 -0
- package/dist/insights/evidence.mjs +64 -0
- package/dist/insights/evidence.mjs.map +1 -0
- package/dist/insights/hash.d.mts +18 -0
- package/dist/insights/hash.mjs +21 -0
- package/dist/insights/hash.mjs.map +1 -0
- package/dist/insights/limits.d.mts +24 -0
- package/dist/insights/limits.mjs +24 -0
- package/dist/insights/limits.mjs.map +1 -0
- package/dist/insights/prompts/v1.d.mts +13 -0
- package/dist/insights/prompts/v1.mjs +34 -0
- package/dist/insights/prompts/v1.mjs.map +1 -0
- package/dist/insights/redact.d.mts +9 -0
- package/dist/insights/redact.mjs +24 -0
- package/dist/insights/redact.mjs.map +1 -0
- package/dist/insights/text.mjs +44 -0
- package/dist/insights/text.mjs.map +1 -0
- package/dist/insights.d.mts +11 -0
- package/dist/insights.mjs +12 -0
- package/dist/logger.mjs +17 -1
- package/dist/logger.mjs.map +1 -1
- package/dist/models/ai-budget-alert.d.mts +23 -0
- package/dist/models/ai-budget-alert.mjs +38 -0
- package/dist/models/ai-budget-alert.mjs.map +1 -0
- package/dist/models/ai-usage.d.mts +40 -0
- package/dist/models/ai-usage.mjs +61 -0
- package/dist/models/ai-usage.mjs.map +1 -0
- package/dist/models/baseline-archive.d.mts +84 -0
- package/dist/models/baseline-archive.mjs +106 -0
- package/dist/models/baseline-archive.mjs.map +1 -0
- package/dist/models/baseline-export.d.mts +19 -0
- package/dist/models/baseline-export.mjs +136 -0
- package/dist/models/baseline-export.mjs.map +1 -0
- package/dist/models/baseline-import.mjs +118 -0
- package/dist/models/baseline-import.mjs.map +1 -0
- package/dist/models/baseline-transfer.d.mts +49 -0
- package/dist/models/baseline-transfer.mjs +121 -0
- package/dist/models/baseline-transfer.mjs.map +1 -0
- package/dist/models/baseline.d.mts +32 -5
- package/dist/models/baseline.mjs +65 -20
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build-component.d.mts +28 -0
- package/dist/models/build-component.mjs +55 -0
- package/dist/models/build-component.mjs.map +1 -0
- package/dist/models/build-review-status.d.mts +29 -0
- package/dist/models/build-review-status.mjs +37 -0
- package/dist/models/build-review-status.mjs.map +1 -0
- package/dist/models/build-review.d.mts +28 -0
- package/dist/models/build-review.mjs +54 -0
- package/dist/models/build-review.mjs.map +1 -0
- package/dist/models/build.d.mts +25 -3
- package/dist/models/build.mjs +50 -11
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/capture-attempt.d.mts +7 -1
- package/dist/models/capture-attempt.mjs +29 -4
- package/dist/models/capture-attempt.mjs.map +1 -1
- package/dist/models/capture-job.d.mts +106 -0
- package/dist/models/capture-job.mjs +288 -0
- package/dist/models/capture-job.mjs.map +1 -0
- package/dist/models/capture-log.d.mts +1 -1
- package/dist/models/capture-log.mjs +4 -3
- package/dist/models/capture-log.mjs.map +1 -1
- package/dist/models/capture-shard.d.mts +62 -0
- package/dist/models/capture-shard.mjs +190 -0
- package/dist/models/capture-shard.mjs.map +1 -0
- package/dist/models/changeset.d.mts +48 -0
- package/dist/models/changeset.mjs +81 -0
- package/dist/models/changeset.mjs.map +1 -0
- package/dist/models/columns.mjs +19 -0
- package/dist/models/columns.mjs.map +1 -0
- package/dist/models/comment.d.mts +4 -1
- package/dist/models/comment.mjs +7 -4
- package/dist/models/comment.mjs.map +1 -1
- package/dist/models/component.d.mts +43 -0
- package/dist/models/component.mjs +60 -0
- package/dist/models/component.mjs.map +1 -0
- package/dist/models/connections.d.mts +41 -0
- package/dist/models/connections.mjs +112 -0
- package/dist/models/connections.mjs.map +1 -0
- package/dist/models/content-ref.d.mts +1 -1
- package/dist/models/content-ref.mjs +1 -1
- package/dist/models/content-ref.mjs.map +1 -1
- package/dist/models/credentials.mjs +12 -2
- package/dist/models/credentials.mjs.map +1 -1
- package/dist/models/design-credentials.d.mts +41 -0
- package/dist/models/design-credentials.mjs +86 -0
- package/dist/models/design-credentials.mjs.map +1 -0
- package/dist/models/design-health.d.mts +16 -0
- package/dist/models/design-health.mjs +25 -0
- package/dist/models/design-health.mjs.map +1 -0
- package/dist/models/design-nodes.d.mts +83 -0
- package/dist/models/design-nodes.mjs +222 -0
- package/dist/models/design-nodes.mjs.map +1 -0
- package/dist/models/design-renders.mjs +74 -0
- package/dist/models/design-renders.mjs.map +1 -0
- package/dist/models/design-resolve.d.mts +29 -0
- package/dist/models/design-resolve.mjs +41 -0
- package/dist/models/design-resolve.mjs.map +1 -0
- package/dist/models/design.d.mts +39 -0
- package/dist/models/design.mjs +90 -0
- package/dist/models/design.mjs.map +1 -0
- package/dist/models/inherited-ratio.d.mts +21 -0
- package/dist/models/inherited-ratio.mjs +22 -0
- package/dist/models/inherited-ratio.mjs.map +1 -0
- package/dist/models/insight.d.mts +70 -0
- package/dist/models/insight.mjs +162 -0
- package/dist/models/insight.mjs.map +1 -0
- package/dist/models/label.d.mts +4 -2
- package/dist/models/label.mjs +25 -10
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/member.d.mts +1 -1
- package/dist/models/member.mjs +5 -4
- package/dist/models/member.mjs.map +1 -1
- package/dist/models/notification-channel.d.mts +37 -3
- package/dist/models/notification-channel.mjs +88 -9
- package/dist/models/notification-channel.mjs.map +1 -1
- package/dist/models/notification-subscription.d.mts +1 -2
- package/dist/models/notification-subscription.mjs +7 -9
- package/dist/models/notification-subscription.mjs.map +1 -1
- package/dist/models/project-group-mapping.d.mts +6 -1
- package/dist/models/project-group-mapping.mjs +14 -4
- package/dist/models/project-group-mapping.mjs.map +1 -1
- package/dist/models/project.d.mts +9 -1
- package/dist/models/project.mjs +9 -4
- package/dist/models/project.mjs.map +1 -1
- package/dist/models/public-origin.d.mts +63 -0
- package/dist/models/public-origin.mjs +114 -0
- package/dist/models/public-origin.mjs.map +1 -0
- package/dist/models/review-switch-off.d.mts +14 -0
- package/dist/models/review-switch-off.mjs +26 -0
- package/dist/models/review-switch-off.mjs.map +1 -0
- package/dist/models/site-settings.d.mts +23 -0
- package/dist/models/site-settings.mjs +48 -0
- package/dist/models/site-settings.mjs.map +1 -0
- package/dist/models/snapshot.d.mts +36 -1
- package/dist/models/snapshot.mjs +50 -3
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/token.d.mts +13 -1
- package/dist/models/token.mjs +23 -5
- package/dist/models/token.mjs.map +1 -1
- package/dist/models/usage.d.mts +76 -0
- package/dist/models/usage.mjs +149 -0
- package/dist/models/usage.mjs.map +1 -0
- package/dist/models/user.d.mts +1 -1
- package/dist/models/user.mjs +4 -3
- package/dist/models/user.mjs.map +1 -1
- package/dist/models/webhook.d.mts +15 -1
- package/dist/models/webhook.mjs +33 -4
- package/dist/models/webhook.mjs.map +1 -1
- package/dist/models.d.mts +26 -3
- package/dist/models.mjs +27 -5
- package/dist/options.d.mts +18 -0
- package/dist/options.mjs +90 -0
- package/dist/options.mjs.map +1 -0
- package/dist/paths.d.mts +19 -4
- package/dist/paths.mjs +51 -10
- package/dist/paths.mjs.map +1 -1
- package/dist/retention/ai.d.mts +18 -0
- package/dist/retention/ai.mjs +23 -0
- package/dist/retention/ai.mjs.map +1 -0
- package/dist/retention/pending.mjs +47 -0
- package/dist/retention/pending.mjs.map +1 -0
- package/dist/retention/screenshot-sweep.d.mts +51 -0
- package/dist/retention/screenshot-sweep.mjs +128 -0
- package/dist/retention/screenshot-sweep.mjs.map +1 -0
- package/dist/retention.d.mts +16 -2
- package/dist/retention.mjs +71 -14
- package/dist/retention.mjs.map +1 -1
- package/dist/schema/baseline.d.mts +6 -0
- package/dist/schema/build-component.d.mts +25 -0
- package/dist/schema/build-review.d.mts +17 -0
- package/dist/schema/build.d.mts +7 -0
- package/dist/schema/capture-job.d.mts +60 -0
- package/dist/schema/capture-shard.d.mts +35 -0
- package/dist/schema/comment.d.mts +2 -0
- package/dist/schema/connection.d.mts +24 -0
- package/dist/schema/design.d.mts +58 -0
- package/dist/schema/insight.d.mts +54 -0
- package/dist/schema/notification.d.mts +4 -0
- package/dist/schema/project.d.mts +21 -0
- package/dist/schema/site-setting.d.mts +21 -0
- package/dist/schema/snapshot.d.mts +12 -0
- package/dist/schema/usage.d.mts +30 -0
- package/dist/schema.d.mts +11 -3
- package/dist/serverless.d.mts +21 -0
- package/dist/serverless.mjs +37 -0
- package/dist/serverless.mjs.map +1 -0
- package/dist/test-helpers/create-project.d.mts +1 -1
- package/dist/test-helpers/deferred.d.mts +39 -0
- package/dist/test-helpers/deferred.mjs +61 -0
- package/dist/test-helpers/deferred.mjs.map +1 -0
- package/dist/test-helpers/design-contract.d.mts +21 -0
- package/dist/test-helpers/design-contract.mjs +40 -0
- package/dist/test-helpers/design-contract.mjs.map +1 -0
- package/dist/test-helpers/fake-database.mjs +14 -2
- package/dist/test-helpers/fake-database.mjs.map +1 -1
- package/dist/test-helpers/fake-design.d.mts +43 -0
- package/dist/test-helpers/fake-design.mjs +114 -0
- package/dist/test-helpers/fake-design.mjs.map +1 -0
- package/dist/test-helpers/fake-notifier.d.mts +21 -0
- package/dist/test-helpers/fake-notifier.mjs +44 -0
- package/dist/test-helpers/fake-notifier.mjs.map +1 -0
- package/dist/test-helpers/fake-tables.mjs +255 -67
- package/dist/test-helpers/fake-tables.mjs.map +1 -1
- package/dist/test-helpers/manual-clock.d.mts +27 -0
- package/dist/test-helpers/manual-clock.mjs +56 -0
- package/dist/test-helpers/manual-clock.mjs.map +1 -0
- package/dist/test-helpers/notifier-contract.d.mts +7 -1
- package/dist/test-helpers/notifier-contract.mjs +32 -3
- package/dist/test-helpers/notifier-contract.mjs.map +1 -1
- package/dist/test-helpers/queue-contract.d.mts +16 -2
- package/dist/test-helpers/queue-contract.mjs +195 -1
- package/dist/test-helpers/queue-contract.mjs.map +1 -1
- package/dist/test-helpers/queue-multi-instance.d.mts +20 -0
- package/dist/test-helpers/queue-multi-instance.mjs +206 -0
- package/dist/test-helpers/queue-multi-instance.mjs.map +1 -0
- package/dist/test-helpers/runner-contract.mjs +11 -0
- package/dist/test-helpers/runner-contract.mjs.map +1 -1
- package/dist/test-helpers/sql-chunks.mjs +23 -0
- package/dist/test-helpers/sql-chunks.mjs.map +1 -1
- package/dist/test-helpers/storage-contract.mjs +1 -1
- package/dist/test-helpers.d.mts +9 -4
- package/dist/test-helpers.mjs +8 -3
- package/dist/types.d.mts +14 -3
- package/dist/types.mjs +21 -4
- package/dist/types.mjs.map +1 -1
- package/dist/urls.d.mts +4 -0
- package/dist/urls.mjs +11 -2
- package/dist/urls.mjs.map +1 -1
- package/dist/utils/adaptive-concurrency.d.mts +66 -0
- package/dist/utils/adaptive-concurrency.mjs +149 -0
- package/dist/utils/adaptive-concurrency.mjs.map +1 -0
- package/dist/utils/connect-retry.d.mts +26 -0
- package/dist/utils/connect-retry.mjs +43 -0
- package/dist/utils/connect-retry.mjs.map +1 -0
- package/dist/utils/environment.d.mts +19 -0
- package/dist/utils/environment.mjs +59 -0
- package/dist/utils/environment.mjs.map +1 -0
- package/dist/utils/http.d.mts +17 -1
- package/dist/utils/http.mjs +22 -3
- package/dist/utils/http.mjs.map +1 -1
- package/dist/utils/map-adaptive.d.mts +15 -0
- package/dist/utils/map-adaptive.mjs +72 -0
- package/dist/utils/map-adaptive.mjs.map +1 -0
- package/dist/utils/map-limit.d.mts +10 -0
- package/dist/utils/map-limit.mjs +32 -0
- package/dist/utils/map-limit.mjs.map +1 -0
- package/dist/utils/page-limiter.d.mts +37 -0
- package/dist/utils/page-limiter.mjs +56 -0
- package/dist/utils/page-limiter.mjs.map +1 -0
- package/dist/utils/public-origin.d.mts +20 -0
- package/dist/utils/public-origin.mjs +75 -0
- package/dist/utils/public-origin.mjs.map +1 -0
- package/dist/utils/redact.mjs +3 -0
- package/dist/utils/redact.mjs.map +1 -1
- package/dist/utils/remote-environment.d.mts +39 -0
- package/dist/utils/remote-environment.mjs +114 -0
- package/dist/utils/remote-environment.mjs.map +1 -0
- package/dist/utils/resources.d.mts +64 -0
- package/dist/utils/resources.mjs +122 -0
- package/dist/utils/resources.mjs.map +1 -0
- package/dist/utils/screenshot-store.d.mts +15 -0
- package/dist/utils/screenshot-store.mjs +41 -0
- package/dist/utils/screenshot-store.mjs.map +1 -0
- package/dist/utils/session-gate.d.mts +22 -0
- package/dist/utils/session-gate.mjs +53 -0
- package/dist/utils/session-gate.mjs.map +1 -0
- package/dist/utils/storybook-token.d.mts +81 -0
- package/dist/utils/storybook-token.mjs +134 -0
- package/dist/utils/storybook-token.mjs.map +1 -0
- package/dist/utils/storybook-url.d.mts +15 -0
- package/dist/utils/storybook-url.mjs +21 -0
- package/dist/utils/storybook-url.mjs.map +1 -0
- package/dist/utils/timing.mjs +1 -1
- package/dist/utils/timing.mjs.map +1 -1
- package/dist/utils/zip-read.mjs +129 -0
- package/dist/utils/zip-read.mjs.map +1 -0
- package/dist/utils/zip-write.d.mts +18 -0
- package/dist/utils/zip-write.mjs +188 -0
- package/dist/utils/zip-write.mjs.map +1 -0
- package/dist/utils.d.mts +17 -3
- package/dist/utils.mjs +18 -4
- package/package.json +32 -23
- package/dist/adapter/capture-runner.mjs.map +0 -1
- package/dist/adapter/git-host/comments.d.mts +0 -41
- package/dist/adapter/git-host/comments.mjs +0 -54
- package/dist/adapter/git-host/comments.mjs.map +0 -1
- package/dist/adapter/git-host/helpers.d.mts +0 -13
- package/dist/adapter/git-host/helpers.mjs +0 -30
- package/dist/adapter/git-host/helpers.mjs.map +0 -1
- package/dist/adapter/git-host.d.mts +0 -42
- package/dist/capture/queue.d.mts +0 -38
- package/dist/capture/queue.mjs +0 -98
- package/dist/capture/queue.mjs.map +0 -1
- package/dist/capture/recovery.d.mts +0 -21
- package/dist/capture/recovery.mjs +0 -83
- package/dist/capture/recovery.mjs.map +0 -1
- package/dist/capture/status-fanout.mjs +0 -72
- package/dist/capture/status-fanout.mjs.map +0 -1
- package/dist/diff/engine.d.mts +0 -16
- package/dist/diff/engine.mjs +0 -87
- package/dist/diff/engine.mjs.map +0 -1
- package/dist/diff/options.d.mts +0 -42
- package/dist/diff/options.mjs +0 -15
- package/dist/diff/options.mjs.map +0 -1
- package/dist/models/status-config.d.mts +0 -38
- package/dist/models/status-config.mjs +0 -63
- package/dist/models/status-config.mjs.map +0 -1
- package/dist/schema/status-config.d.mts +0 -16
- package/dist/test-helpers/git-contract.d.mts +0 -11
- package/dist/test-helpers/git-contract.mjs +0 -34
- package/dist/test-helpers/git-contract.mjs.map +0 -1
- /package/dist/adapter/{git-host.mjs → oauth.mjs} +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { attemptRowId, isTerminalJob } from "../../models/capture-job.mjs";
|
|
5
|
+
import { toDatabaseJob } from "./codec.mjs";
|
|
6
|
+
import { isoAt } from "./clock.mjs";
|
|
7
|
+
//#region src/capture/db-queue/claim.ts
|
|
8
|
+
/**
|
|
9
|
+
* The claim algorithm of the database queue. A poll probes for the oldest candidate row (a queued
|
|
10
|
+
* job whose `visibleAt` passed, or a running attempt whose lease ran out), then claims the job's
|
|
11
|
+
* next attempt by inserting its row: the unique key lets exactly one instance win. The winner
|
|
12
|
+
* retires the expired attempt it replaced and marks the job row running.
|
|
13
|
+
*/
|
|
14
|
+
/** Candidates tried per poll when other instances win the race for them. */
|
|
15
|
+
const MAX_CLAIM_TRIES = 3;
|
|
16
|
+
/** Error recorded on a job whose last allowed attempt lost its lease. */
|
|
17
|
+
const LEASE_EXHAUSTED_ERROR = "lease expired on the final attempt";
|
|
18
|
+
/** Probe once and claim the job found; on a lost race, try the next candidate (a few times). */
|
|
19
|
+
async function claimNext(rt) {
|
|
20
|
+
for (let tries = 0; tries < MAX_CLAIM_TRIES; tries += 1) {
|
|
21
|
+
const candidate = await rt.model.nextCandidate(rt.kinds, isoAt(rt));
|
|
22
|
+
if (candidate === null) return null;
|
|
23
|
+
const job = await claimCandidate(rt, candidate);
|
|
24
|
+
if (job !== null) return job;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/** Claim the job behind a candidate row; null when it is gone, over, exhausted or lost to a race. */
|
|
29
|
+
async function claimCandidate(rt, candidate) {
|
|
30
|
+
if (candidate.entry === "job" && candidate.status !== "queued") {
|
|
31
|
+
await rt.model.clearVisibility(candidate.id, isoAt(rt));
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const stale = candidate.entry === "attempt" ? candidate : null;
|
|
35
|
+
const job = stale === null ? candidate : await rt.model.get(candidate.jobId);
|
|
36
|
+
if (job === null || isTerminalJob(job) || stale !== null && stale.attempt < job.attempt) {
|
|
37
|
+
await retire(rt, stale);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return await claimNextAttempt(rt, job, stale);
|
|
41
|
+
}
|
|
42
|
+
/** Claim the attempt after the last one made, or fail the job when none is left. */
|
|
43
|
+
async function claimNextAttempt(rt, job, stale) {
|
|
44
|
+
const made = Math.max(job.attempt, stale?.attempt ?? 0);
|
|
45
|
+
if (made >= job.maxAttempts) {
|
|
46
|
+
await exhaust(rt, job, stale);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return await claimAttempt(rt, job, made + 1, stale);
|
|
50
|
+
}
|
|
51
|
+
async function claimAttempt(rt, job, attempt, stale) {
|
|
52
|
+
const leaseUntil = isoAt(rt, rt.leaseMs);
|
|
53
|
+
if (!await rt.model.claim({
|
|
54
|
+
job,
|
|
55
|
+
attempt,
|
|
56
|
+
owner: rt.owner,
|
|
57
|
+
leaseUntil
|
|
58
|
+
}, isoAt(rt))) return stale === null ? await afterLostRace(rt, job, attempt) : null;
|
|
59
|
+
const fresh = await rt.model.get(job.jobId);
|
|
60
|
+
if (fresh === null || isTerminalJob(fresh)) {
|
|
61
|
+
await rt.model.finishAttempt(attemptRowId(job.jobId, attempt), "expired", isoAt(rt));
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return await takeOver(rt, fresh, attempt, stale);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Another instance holds attempt `attempt` of a job whose row still reads queued. Usually that
|
|
68
|
+
* winner is about to mark the job running. If it died before doing so, its attempt's lease decides:
|
|
69
|
+
* once expired, the attempt is claimed like any expired one; until then the job row is moved
|
|
70
|
+
* behind that lease, so it does not sit at the head of the queue losing every claim. Only
|
|
71
|
+
* `visibleAt` is written, never the status, so a winner that finished meanwhile keeps its outcome.
|
|
72
|
+
*/
|
|
73
|
+
async function afterLostRace(rt, job, attempt) {
|
|
74
|
+
const id = attemptRowId(job.jobId, attempt);
|
|
75
|
+
const expired = await rt.model.expiredAttempt(id, isoAt(rt));
|
|
76
|
+
if (expired !== null) return await claimCandidate(rt, expired);
|
|
77
|
+
await deferBehind(rt, job, await rt.model.get(id));
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
async function deferBehind(rt, job, holder) {
|
|
81
|
+
const current = await rt.model.get(job.jobId);
|
|
82
|
+
if (holder?.visibleAt && current?.status === "queued" && current.attempt === job.attempt) await rt.model.defer(job.jobId, holder.visibleAt, isoAt(rt));
|
|
83
|
+
}
|
|
84
|
+
/** The claim won: retire the attempt it replaced and record the new one on the job row. */
|
|
85
|
+
async function takeOver(rt, job, attempt, stale) {
|
|
86
|
+
await retire(rt, stale);
|
|
87
|
+
await rt.model.markRunning(job.jobId, attempt, isoAt(rt));
|
|
88
|
+
rt.logger?.debug({
|
|
89
|
+
jobId: job.jobId,
|
|
90
|
+
attempt
|
|
91
|
+
}, "job claimed");
|
|
92
|
+
return toDatabaseJob(job, attempt, rt.leaseMs);
|
|
93
|
+
}
|
|
94
|
+
/** Mark an expired attempt row `expired`: it leaves the poll's range, and its holder sees `lost`. */
|
|
95
|
+
async function retire(rt, stale) {
|
|
96
|
+
if (stale !== null) await rt.model.finishAttempt(stale.id, "expired", isoAt(rt));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The final attempt lost its lease: the job fails (one limit covers failed attempts and lost
|
|
100
|
+
* leases). Idempotent, so two instances finding the same row write the same outcome. If the late
|
|
101
|
+
* holder finished after all, its `done` wins.
|
|
102
|
+
*/
|
|
103
|
+
async function exhaust(rt, job, stale) {
|
|
104
|
+
const now = isoAt(rt);
|
|
105
|
+
if (stale !== null && (await rt.model.get(stale.id))?.status === "done") {
|
|
106
|
+
await rt.model.finishJob(job.jobId, "done", now);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
await retire(rt, stale);
|
|
110
|
+
await rt.model.finishJob(job.jobId, "failed", now, LEASE_EXHAUSTED_ERROR);
|
|
111
|
+
rt.logger?.warn({
|
|
112
|
+
jobId: job.jobId,
|
|
113
|
+
attempts: job.attempt
|
|
114
|
+
}, "job failed: lease exhausted");
|
|
115
|
+
await reportExhausted(rt, job);
|
|
116
|
+
}
|
|
117
|
+
/** Tell the host a job ended without any holder to record it; its failure never blocks the poll. */
|
|
118
|
+
async function reportExhausted(rt, job) {
|
|
119
|
+
if (rt.onExhausted === void 0) return;
|
|
120
|
+
try {
|
|
121
|
+
await rt.onExhausted(toDatabaseJob(job, job.attempt, rt.leaseMs), LEASE_EXHAUSTED_ERROR);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
rt.logger?.error({
|
|
124
|
+
err: error,
|
|
125
|
+
jobId: job.jobId
|
|
126
|
+
}, "exhausted-job hook failed");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
export { LEASE_EXHAUSTED_ERROR, claimCandidate, claimNext };
|
|
131
|
+
|
|
132
|
+
//# sourceMappingURL=claim.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim.mjs","names":[],"sources":["../../../src/capture/db-queue/claim.ts"],"sourcesContent":["/**\n * The claim algorithm of the database queue. A poll probes for the oldest candidate row (a queued\n * job whose `visibleAt` passed, or a running attempt whose lease ran out), then claims the job's\n * next attempt by inserting its row: the unique key lets exactly one instance win. The winner\n * retires the expired attempt it replaced and marks the job row running.\n */\nimport { attemptRowId, isTerminalJob } from \"../../models/capture-job.ts\";\nimport type { CaptureJobRow } from \"../../schema/capture-job.ts\";\nimport { isoAt } from \"./clock.ts\";\nimport { toDatabaseJob } from \"./codec.ts\";\nimport type { DatabaseJob, QueueRuntime } from \"./types.ts\";\n\n/** Candidates tried per poll when other instances win the race for them. */\nconst MAX_CLAIM_TRIES = 3;\n\n/** Error recorded on a job whose last allowed attempt lost its lease. */\nexport const LEASE_EXHAUSTED_ERROR = \"lease expired on the final attempt\";\n\n/** Probe once and claim the job found; on a lost race, try the next candidate (a few times). */\nexport async function claimNext(rt: QueueRuntime): Promise<DatabaseJob | null> {\n for (let tries = 0; tries < MAX_CLAIM_TRIES; tries += 1) {\n // oxlint-disable-next-line no-await-in-loop -- one candidate at a time, in order\n const candidate = await rt.model.nextCandidate(rt.kinds, isoAt(rt));\n if (candidate === null) {\n return null;\n }\n // oxlint-disable-next-line no-await-in-loop -- see above\n const job = await claimCandidate(rt, candidate);\n if (job !== null) {\n return job;\n }\n }\n return null;\n}\n\n/** Claim the job behind a candidate row; null when it is gone, over, exhausted or lost to a race. */\nexport async function claimCandidate(\n rt: QueueRuntime,\n candidate: CaptureJobRow,\n): Promise<DatabaseJob | null> {\n if (candidate.entry === \"job\" && candidate.status !== \"queued\") {\n // A running job row put back in range by a late deferral (see `deferBehind`): not claimable.\n await rt.model.clearVisibility(candidate.id, isoAt(rt));\n return null;\n }\n const stale = candidate.entry === \"attempt\" ? candidate : null;\n const job = stale === null ? candidate : await rt.model.get(candidate.jobId);\n if (job === null || isTerminalJob(job) || (stale !== null && stale.attempt < job.attempt)) {\n // Nothing left to claim: the expired attempt is just cleaned out of the poll's range.\n await retire(rt, stale);\n return null;\n }\n return await claimNextAttempt(rt, job, stale);\n}\n\n/** Claim the attempt after the last one made, or fail the job when none is left. */\nasync function claimNextAttempt(\n rt: QueueRuntime,\n job: CaptureJobRow,\n stale: CaptureJobRow | null,\n): Promise<DatabaseJob | null> {\n // An attempt row ahead of its job row belongs to a claimer that died before marking the job.\n const made = Math.max(job.attempt, stale?.attempt ?? 0);\n if (made >= job.maxAttempts) {\n await exhaust(rt, job, stale);\n return null;\n }\n return await claimAttempt(rt, job, made + 1, stale);\n}\n\nasync function claimAttempt(\n rt: QueueRuntime,\n job: CaptureJobRow,\n attempt: number,\n stale: CaptureJobRow | null,\n): Promise<DatabaseJob | null> {\n const leaseUntil = isoAt(rt, rt.leaseMs);\n const won = await rt.model.claim({ job, attempt, owner: rt.owner, leaseUntil }, isoAt(rt));\n if (!won) {\n return stale === null ? await afterLostRace(rt, job, attempt) : null;\n }\n const fresh = await rt.model.get(job.jobId);\n if (fresh === null || isTerminalJob(fresh)) {\n // Cancelled (or finished by a late holder) between the probe and the insert.\n await rt.model.finishAttempt(attemptRowId(job.jobId, attempt), \"expired\", isoAt(rt));\n return null;\n }\n return await takeOver(rt, fresh, attempt, stale);\n}\n\n/**\n * Another instance holds attempt `attempt` of a job whose row still reads queued. Usually that\n * winner is about to mark the job running. If it died before doing so, its attempt's lease decides:\n * once expired, the attempt is claimed like any expired one; until then the job row is moved\n * behind that lease, so it does not sit at the head of the queue losing every claim. Only\n * `visibleAt` is written, never the status, so a winner that finished meanwhile keeps its outcome.\n */\nasync function afterLostRace(\n rt: QueueRuntime,\n job: CaptureJobRow,\n attempt: number,\n): Promise<DatabaseJob | null> {\n const id = attemptRowId(job.jobId, attempt);\n const expired = await rt.model.expiredAttempt(id, isoAt(rt));\n if (expired !== null) {\n return await claimCandidate(rt, expired);\n }\n await deferBehind(rt, job, await rt.model.get(id));\n return null;\n}\n\nasync function deferBehind(\n rt: QueueRuntime,\n job: CaptureJobRow,\n holder: CaptureJobRow | null,\n): Promise<void> {\n const current = await rt.model.get(job.jobId);\n if (holder?.visibleAt && current?.status === \"queued\" && current.attempt === job.attempt) {\n await rt.model.defer(job.jobId, holder.visibleAt, isoAt(rt));\n }\n}\n\n/** The claim won: retire the attempt it replaced and record the new one on the job row. */\nasync function takeOver(\n rt: QueueRuntime,\n job: CaptureJobRow,\n attempt: number,\n stale: CaptureJobRow | null,\n): Promise<DatabaseJob> {\n await retire(rt, stale);\n await rt.model.markRunning(job.jobId, attempt, isoAt(rt));\n rt.logger?.debug({ jobId: job.jobId, attempt }, \"job claimed\");\n return toDatabaseJob(job, attempt, rt.leaseMs);\n}\n\n/** Mark an expired attempt row `expired`: it leaves the poll's range, and its holder sees `lost`. */\nasync function retire(rt: QueueRuntime, stale: CaptureJobRow | null): Promise<void> {\n if (stale !== null) {\n await rt.model.finishAttempt(stale.id, \"expired\", isoAt(rt));\n }\n}\n\n/**\n * The final attempt lost its lease: the job fails (one limit covers failed attempts and lost\n * leases). Idempotent, so two instances finding the same row write the same outcome. If the late\n * holder finished after all, its `done` wins.\n */\nasync function exhaust(\n rt: QueueRuntime,\n job: CaptureJobRow,\n stale: CaptureJobRow | null,\n): Promise<void> {\n const now = isoAt(rt);\n if (stale !== null && (await rt.model.get(stale.id))?.status === \"done\") {\n await rt.model.finishJob(job.jobId, \"done\", now);\n return;\n }\n await retire(rt, stale);\n await rt.model.finishJob(job.jobId, \"failed\", now, LEASE_EXHAUSTED_ERROR);\n rt.logger?.warn({ jobId: job.jobId, attempts: job.attempt }, \"job failed: lease exhausted\");\n await reportExhausted(rt, job);\n}\n\n/** Tell the host a job ended without any holder to record it; its failure never blocks the poll. */\nasync function reportExhausted(rt: QueueRuntime, job: CaptureJobRow): Promise<void> {\n if (rt.onExhausted === undefined) {\n return;\n }\n try {\n await rt.onExhausted(toDatabaseJob(job, job.attempt, rt.leaseMs), LEASE_EXHAUSTED_ERROR);\n } catch (error) {\n rt.logger?.error({ err: error, jobId: job.jobId }, \"exhausted-job hook failed\");\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,MAAM,kBAAkB;;AAGxB,MAAa,wBAAwB;;AAGrC,eAAsB,UAAU,IAA+C;CAC7E,KAAK,IAAI,QAAQ,GAAG,QAAQ,iBAAiB,SAAS,GAAG;EAEvD,MAAM,YAAY,MAAM,GAAG,MAAM,cAAc,GAAG,OAAO,MAAM,EAAE,CAAC;EAClE,IAAI,cAAc,MAChB,OAAO;EAGT,MAAM,MAAM,MAAM,eAAe,IAAI,SAAS;EAC9C,IAAI,QAAQ,MACV,OAAO;CAEX;CACA,OAAO;AACT;;AAGA,eAAsB,eACpB,IACA,WAC6B;CAC7B,IAAI,UAAU,UAAU,SAAS,UAAU,WAAW,UAAU;EAE9D,MAAM,GAAG,MAAM,gBAAgB,UAAU,IAAI,MAAM,EAAE,CAAC;EACtD,OAAO;CACT;CACA,MAAM,QAAQ,UAAU,UAAU,YAAY,YAAY;CAC1D,MAAM,MAAM,UAAU,OAAO,YAAY,MAAM,GAAG,MAAM,IAAI,UAAU,KAAK;CAC3E,IAAI,QAAQ,QAAQ,cAAc,GAAG,KAAM,UAAU,QAAQ,MAAM,UAAU,IAAI,SAAU;EAEzF,MAAM,OAAO,IAAI,KAAK;EACtB,OAAO;CACT;CACA,OAAO,MAAM,iBAAiB,IAAI,KAAK,KAAK;AAC9C;;AAGA,eAAe,iBACb,IACA,KACA,OAC6B;CAE7B,MAAM,OAAO,KAAK,IAAI,IAAI,SAAS,OAAO,WAAW,CAAC;CACtD,IAAI,QAAQ,IAAI,aAAa;EAC3B,MAAM,QAAQ,IAAI,KAAK,KAAK;EAC5B,OAAO;CACT;CACA,OAAO,MAAM,aAAa,IAAI,KAAK,OAAO,GAAG,KAAK;AACpD;AAEA,eAAe,aACb,IACA,KACA,SACA,OAC6B;CAC7B,MAAM,aAAa,MAAM,IAAI,GAAG,OAAO;CAEvC,IAAI,CAAC,MADa,GAAG,MAAM,MAAM;EAAE;EAAK;EAAS,OAAO,GAAG;EAAO;CAAW,GAAG,MAAM,EAAE,CAAC,GAEvF,OAAO,UAAU,OAAO,MAAM,cAAc,IAAI,KAAK,OAAO,IAAI;CAElE,MAAM,QAAQ,MAAM,GAAG,MAAM,IAAI,IAAI,KAAK;CAC1C,IAAI,UAAU,QAAQ,cAAc,KAAK,GAAG;EAE1C,MAAM,GAAG,MAAM,cAAc,aAAa,IAAI,OAAO,OAAO,GAAG,WAAW,MAAM,EAAE,CAAC;EACnF,OAAO;CACT;CACA,OAAO,MAAM,SAAS,IAAI,OAAO,SAAS,KAAK;AACjD;;;;;;;;AASA,eAAe,cACb,IACA,KACA,SAC6B;CAC7B,MAAM,KAAK,aAAa,IAAI,OAAO,OAAO;CAC1C,MAAM,UAAU,MAAM,GAAG,MAAM,eAAe,IAAI,MAAM,EAAE,CAAC;CAC3D,IAAI,YAAY,MACd,OAAO,MAAM,eAAe,IAAI,OAAO;CAEzC,MAAM,YAAY,IAAI,KAAK,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;CACjD,OAAO;AACT;AAEA,eAAe,YACb,IACA,KACA,QACe;CACf,MAAM,UAAU,MAAM,GAAG,MAAM,IAAI,IAAI,KAAK;CAC5C,IAAI,QAAQ,aAAa,SAAS,WAAW,YAAY,QAAQ,YAAY,IAAI,SAC/E,MAAM,GAAG,MAAM,MAAM,IAAI,OAAO,OAAO,WAAW,MAAM,EAAE,CAAC;AAE/D;;AAGA,eAAe,SACb,IACA,KACA,SACA,OACsB;CACtB,MAAM,OAAO,IAAI,KAAK;CACtB,MAAM,GAAG,MAAM,YAAY,IAAI,OAAO,SAAS,MAAM,EAAE,CAAC;CACxD,GAAG,QAAQ,MAAM;EAAE,OAAO,IAAI;EAAO;CAAQ,GAAG,aAAa;CAC7D,OAAO,cAAc,KAAK,SAAS,GAAG,OAAO;AAC/C;;AAGA,eAAe,OAAO,IAAkB,OAA4C;CAClF,IAAI,UAAU,MACZ,MAAM,GAAG,MAAM,cAAc,MAAM,IAAI,WAAW,MAAM,EAAE,CAAC;AAE/D;;;;;;AAOA,eAAe,QACb,IACA,KACA,OACe;CACf,MAAM,MAAM,MAAM,EAAE;CACpB,IAAI,UAAU,SAAS,MAAM,GAAG,MAAM,IAAI,MAAM,EAAE,EAAA,EAAI,WAAW,QAAQ;EACvE,MAAM,GAAG,MAAM,UAAU,IAAI,OAAO,QAAQ,GAAG;EAC/C;CACF;CACA,MAAM,OAAO,IAAI,KAAK;CACtB,MAAM,GAAG,MAAM,UAAU,IAAI,OAAO,UAAU,KAAK,qBAAqB;CACxE,GAAG,QAAQ,KAAK;EAAE,OAAO,IAAI;EAAO,UAAU,IAAI;CAAQ,GAAG,6BAA6B;CAC1F,MAAM,gBAAgB,IAAI,GAAG;AAC/B;;AAGA,eAAe,gBAAgB,IAAkB,KAAmC;CAClF,IAAI,GAAG,gBAAgB,KAAA,GACrB;CAEF,IAAI;EACF,MAAM,GAAG,YAAY,cAAc,KAAK,IAAI,SAAS,GAAG,OAAO,GAAG,qBAAqB;CACzF,SAAS,OAAO;EACd,GAAG,QAAQ,MAAM;GAAE,KAAK;GAAO,OAAO,IAAI;EAAM,GAAG,2BAA2B;CAChF;AACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/capture/db-queue/clock.ts
|
|
5
|
+
/** ISO-8601 time `plusMs` from now on the queue's clock. */
|
|
6
|
+
function isoAt(rt, plusMs = 0) {
|
|
7
|
+
return new Date(rt.now() + plusMs).toISOString();
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { isoAt };
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=clock.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.mjs","names":[],"sources":["../../../src/capture/db-queue/clock.ts"],"sourcesContent":["/** Time of the database queue: every timestamp it writes or compares comes from the runtime clock. */\nimport type { QueueRuntime } from \"./types.ts\";\n\n/** ISO-8601 time `plusMs` from now on the queue's clock. */\nexport function isoAt(rt: Pick<QueueRuntime, \"now\">, plusMs = 0): string {\n return new Date(rt.now() + plusMs).toISOString();\n}\n"],"mappings":";;;;;AAIA,SAAgB,MAAM,IAA+B,SAAS,GAAW;CACvE,OAAO,IAAI,KAAK,GAAG,IAAI,IAAI,MAAM,CAAC,CAAC,YAAY;AACjD"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { attemptRowId } from "../../models/capture-job.mjs";
|
|
5
|
+
//#region src/capture/db-queue/codec.ts
|
|
6
|
+
/** Serialize the request id, trace context and kind-specific fields of a job; null when empty. */
|
|
7
|
+
function encodePayload(job) {
|
|
8
|
+
const stored = {};
|
|
9
|
+
if (job.reqId !== void 0) stored.reqId = job.reqId;
|
|
10
|
+
if (job.traceparent !== void 0) stored.traceparent = job.traceparent;
|
|
11
|
+
if (job.data !== void 0 && Object.keys(job.data).length > 0) stored.data = job.data;
|
|
12
|
+
return Object.keys(stored).length === 0 ? null : JSON.stringify(stored);
|
|
13
|
+
}
|
|
14
|
+
/** Parse a stored payload. A malformed one (a poison row) decodes as empty rather than throwing. */
|
|
15
|
+
function decodePayload(raw) {
|
|
16
|
+
if (raw === null || raw === "") return {};
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
return isRecord(parsed) ? pickPayload(parsed) : {};
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** The job handed to a consumer for attempt `attempt` of a job row. */
|
|
25
|
+
function toDatabaseJob(row, attempt, leaseMs) {
|
|
26
|
+
const payload = decodePayload(row.payload);
|
|
27
|
+
return withOptional({
|
|
28
|
+
buildId: row.buildId ?? "",
|
|
29
|
+
jobId: row.jobId,
|
|
30
|
+
kind: row.kind,
|
|
31
|
+
attempt,
|
|
32
|
+
attempts: attempt - 1,
|
|
33
|
+
maxAttempts: row.maxAttempts,
|
|
34
|
+
receipt: attemptRowId(row.jobId, attempt),
|
|
35
|
+
leaseMs,
|
|
36
|
+
data: payload.data ?? {}
|
|
37
|
+
}, row, payload);
|
|
38
|
+
}
|
|
39
|
+
/** The queue-view entry of a job row. */
|
|
40
|
+
function toQueueEntry(row) {
|
|
41
|
+
const entry = {
|
|
42
|
+
buildId: row.buildId ?? "",
|
|
43
|
+
status: entryStatus(row.status),
|
|
44
|
+
queuedAt: row.createdAt
|
|
45
|
+
};
|
|
46
|
+
if (row.shardId !== null) entry.shard = row.shardId;
|
|
47
|
+
if (row.startedAt !== null) entry.startedAt = row.startedAt;
|
|
48
|
+
if (row.finishedAt !== null) entry.finishedAt = row.finishedAt;
|
|
49
|
+
if (row.error !== null) entry.error = row.error;
|
|
50
|
+
return entry;
|
|
51
|
+
}
|
|
52
|
+
function withOptional(job, row, payload) {
|
|
53
|
+
if (row.shardId !== null) job.shard = row.shardId;
|
|
54
|
+
if (payload.reqId !== void 0) job.reqId = payload.reqId;
|
|
55
|
+
if (payload.traceparent !== void 0) job.traceparent = payload.traceparent;
|
|
56
|
+
return job;
|
|
57
|
+
}
|
|
58
|
+
function entryStatus(status) {
|
|
59
|
+
if (status === "done") return "completed";
|
|
60
|
+
if (status === "expired") return "failed";
|
|
61
|
+
return status;
|
|
62
|
+
}
|
|
63
|
+
function pickPayload(parsed) {
|
|
64
|
+
const stored = {};
|
|
65
|
+
if (typeof parsed["reqId"] === "string") stored.reqId = parsed["reqId"];
|
|
66
|
+
if (typeof parsed["traceparent"] === "string") stored.traceparent = parsed["traceparent"];
|
|
67
|
+
if (isRecord(parsed["data"])) stored.data = parsed["data"];
|
|
68
|
+
return stored;
|
|
69
|
+
}
|
|
70
|
+
function isRecord(value) {
|
|
71
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { decodePayload, encodePayload, toDatabaseJob, toQueueEntry };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=codec.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.mjs","names":[],"sources":["../../../src/capture/db-queue/codec.ts"],"sourcesContent":["/** Codec of the database queue: job payload JSON and the row → job / queue-entry mappings. */\nimport type { JobStatus, QueueEntry } from \"../../adapters/capture-queue.ts\";\nimport { attemptRowId } from \"../../models/capture-job.ts\";\nimport type { CaptureJobRow } from \"../../schema/capture-job.ts\";\nimport type { DatabaseJob, QueueJobInput } from \"./types.ts\";\n\n/** What the `payload` column holds. */\ninterface StoredPayload {\n reqId?: string;\n traceparent?: string;\n data?: Record<string, unknown>;\n}\n\n/** Serialize the request id, trace context and kind-specific fields of a job; null when empty. */\nexport function encodePayload(job: QueueJobInput): string | null {\n const stored: StoredPayload = {};\n if (job.reqId !== undefined) {\n stored.reqId = job.reqId;\n }\n if (job.traceparent !== undefined) {\n stored.traceparent = job.traceparent;\n }\n if (job.data !== undefined && Object.keys(job.data).length > 0) {\n stored.data = job.data;\n }\n return Object.keys(stored).length === 0 ? null : JSON.stringify(stored);\n}\n\n/** Parse a stored payload. A malformed one (a poison row) decodes as empty rather than throwing. */\nexport function decodePayload(raw: string | null): StoredPayload {\n if (raw === null || raw === \"\") {\n return {};\n }\n try {\n const parsed: unknown = JSON.parse(raw);\n return isRecord(parsed) ? pickPayload(parsed) : {};\n } catch {\n return {};\n }\n}\n\n/** The job handed to a consumer for attempt `attempt` of a job row. */\nexport function toDatabaseJob(row: CaptureJobRow, attempt: number, leaseMs: number): DatabaseJob {\n const payload = decodePayload(row.payload);\n const job: DatabaseJob = {\n buildId: row.buildId ?? \"\",\n jobId: row.jobId,\n kind: row.kind,\n attempt,\n attempts: attempt - 1,\n maxAttempts: row.maxAttempts,\n receipt: attemptRowId(row.jobId, attempt),\n leaseMs,\n data: payload.data ?? {},\n };\n return withOptional(job, row, payload);\n}\n\n/** The queue-view entry of a job row. */\nexport function toQueueEntry(row: CaptureJobRow): QueueEntry {\n const entry: QueueEntry = {\n buildId: row.buildId ?? \"\",\n status: entryStatus(row.status),\n queuedAt: row.createdAt,\n };\n if (row.shardId !== null) {\n entry.shard = row.shardId;\n }\n if (row.startedAt !== null) {\n entry.startedAt = row.startedAt;\n }\n if (row.finishedAt !== null) {\n entry.finishedAt = row.finishedAt;\n }\n if (row.error !== null) {\n entry.error = row.error;\n }\n return entry;\n}\n\nfunction withOptional(job: DatabaseJob, row: CaptureJobRow, payload: StoredPayload): DatabaseJob {\n if (row.shardId !== null) {\n job.shard = row.shardId;\n }\n if (payload.reqId !== undefined) {\n job.reqId = payload.reqId;\n }\n if (payload.traceparent !== undefined) {\n job.traceparent = payload.traceparent;\n }\n return job;\n}\n\nfunction entryStatus(status: CaptureJobRow[\"status\"]): JobStatus {\n if (status === \"done\") {\n return \"completed\";\n }\n if (status === \"expired\") {\n return \"failed\";\n }\n return status;\n}\n\nfunction pickPayload(parsed: Record<string, unknown>): StoredPayload {\n const stored: StoredPayload = {};\n if (typeof parsed[\"reqId\"] === \"string\") {\n stored.reqId = parsed[\"reqId\"];\n }\n if (typeof parsed[\"traceparent\"] === \"string\") {\n stored.traceparent = parsed[\"traceparent\"];\n }\n if (isRecord(parsed[\"data\"])) {\n stored.data = parsed[\"data\"];\n }\n return stored;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"],"mappings":";;;;;;AAcA,SAAgB,cAAc,KAAmC;CAC/D,MAAM,SAAwB,CAAC;CAC/B,IAAI,IAAI,UAAU,KAAA,GAChB,OAAO,QAAQ,IAAI;CAErB,IAAI,IAAI,gBAAgB,KAAA,GACtB,OAAO,cAAc,IAAI;CAE3B,IAAI,IAAI,SAAS,KAAA,KAAa,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,SAAS,GAC3D,OAAO,OAAO,IAAI;CAEpB,OAAO,OAAO,KAAK,MAAM,CAAC,CAAC,WAAW,IAAI,OAAO,KAAK,UAAU,MAAM;AACxE;;AAGA,SAAgB,cAAc,KAAmC;CAC/D,IAAI,QAAQ,QAAQ,QAAQ,IAC1B,OAAO,CAAC;CAEV,IAAI;EACF,MAAM,SAAkB,KAAK,MAAM,GAAG;EACtC,OAAO,SAAS,MAAM,IAAI,YAAY,MAAM,IAAI,CAAC;CACnD,QAAQ;EACN,OAAO,CAAC;CACV;AACF;;AAGA,SAAgB,cAAc,KAAoB,SAAiB,SAA8B;CAC/F,MAAM,UAAU,cAAc,IAAI,OAAO;CAYzC,OAAO,aAAa;EAVlB,SAAS,IAAI,WAAW;EACxB,OAAO,IAAI;EACX,MAAM,IAAI;EACV;EACA,UAAU,UAAU;EACpB,aAAa,IAAI;EACjB,SAAS,aAAa,IAAI,OAAO,OAAO;EACxC;EACA,MAAM,QAAQ,QAAQ,CAAC;CAEH,GAAG,KAAK,OAAO;AACvC;;AAGA,SAAgB,aAAa,KAAgC;CAC3D,MAAM,QAAoB;EACxB,SAAS,IAAI,WAAW;EACxB,QAAQ,YAAY,IAAI,MAAM;EAC9B,UAAU,IAAI;CAChB;CACA,IAAI,IAAI,YAAY,MAClB,MAAM,QAAQ,IAAI;CAEpB,IAAI,IAAI,cAAc,MACpB,MAAM,YAAY,IAAI;CAExB,IAAI,IAAI,eAAe,MACrB,MAAM,aAAa,IAAI;CAEzB,IAAI,IAAI,UAAU,MAChB,MAAM,QAAQ,IAAI;CAEpB,OAAO;AACT;AAEA,SAAS,aAAa,KAAkB,KAAoB,SAAqC;CAC/F,IAAI,IAAI,YAAY,MAClB,IAAI,QAAQ,IAAI;CAElB,IAAI,QAAQ,UAAU,KAAA,GACpB,IAAI,QAAQ,QAAQ;CAEtB,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,IAAI,cAAc,QAAQ;CAE5B,OAAO;AACT;AAEA,SAAS,YAAY,QAA4C;CAC/D,IAAI,WAAW,QACb,OAAO;CAET,IAAI,WAAW,WACb,OAAO;CAET,OAAO;AACT;AAEA,SAAS,YAAY,QAAgD;CACnE,MAAM,SAAwB,CAAC;CAC/B,IAAI,OAAO,OAAO,aAAa,UAC7B,OAAO,QAAQ,OAAO;CAExB,IAAI,OAAO,OAAO,mBAAmB,UACnC,OAAO,cAAc,OAAO;CAE9B,IAAI,SAAS,OAAO,OAAO,GACzB,OAAO,OAAO,OAAO;CAEvB,OAAO;AACT;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DatabaseAdapter } from "../../adapter/database.mjs";
|
|
4
|
+
import { DEFAULT_LEASE_MS, DEFAULT_RETRIES, DatabaseCaptureQueue, DatabaseCaptureQueueOptions, DatabaseJob, QueueJobInput } from "./types.mjs";
|
|
5
|
+
import { LEASE_EXHAUSTED_ERROR } from "./claim.mjs";
|
|
6
|
+
import { MAX_NACK_DELAY_MS } from "./poll.mjs";
|
|
7
|
+
//#region src/capture/db-queue/index.d.ts
|
|
8
|
+
/** What the database queue reports about itself; hosts show it before the queue is built. */
|
|
9
|
+
declare const DATABASE_QUEUE_METADATA: {
|
|
10
|
+
readonly name: "Database Queue";
|
|
11
|
+
readonly version: string;
|
|
12
|
+
readonly description: "Capture queue on the app database (capture_jobs)";
|
|
13
|
+
readonly kind: "database";
|
|
14
|
+
readonly category: "capture-queue";
|
|
15
|
+
};
|
|
16
|
+
/** A `PollableCaptureQueue` over `db`'s `capture_jobs` table. */
|
|
17
|
+
declare function createDatabaseCaptureQueue(db: DatabaseAdapter, options?: DatabaseCaptureQueueOptions): DatabaseCaptureQueue;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { DATABASE_QUEUE_METADATA, DEFAULT_LEASE_MS, DEFAULT_RETRIES, type DatabaseCaptureQueue, type DatabaseCaptureQueueOptions, type DatabaseJob, LEASE_EXHAUSTED_ERROR, MAX_NACK_DELAY_MS, type QueueJobInput, createDatabaseCaptureQueue };
|
|
20
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { ulid } from "../../utils/ulid.mjs";
|
|
5
|
+
import { CaptureJobModel } from "../../models/capture-job.mjs";
|
|
6
|
+
import { LEASE_EXHAUSTED_ERROR } from "./claim.mjs";
|
|
7
|
+
import { MAX_NACK_DELAY_MS, ackJob, nackJob, pollJob } from "./poll.mjs";
|
|
8
|
+
import { extendJob, inspectJob } from "./lease.mjs";
|
|
9
|
+
import { buildQueueLifecycle } from "./lifecycle.mjs";
|
|
10
|
+
import { activeEntries, cancelJobs, enqueueCapture, enqueueJob, recentEntries, statusOf } from "./operations.mjs";
|
|
11
|
+
import { DEFAULT_LEASE_MS, DEFAULT_RETRIES } from "./types.mjs";
|
|
12
|
+
import { createWakeSignal } from "./wake.mjs";
|
|
13
|
+
import { hostname } from "node:os";
|
|
14
|
+
//#region src/capture/db-queue/index.ts
|
|
15
|
+
/**
|
|
16
|
+
* Create the database capture queue (ADR 0006, decision 4): `capture_jobs` on the app's own
|
|
17
|
+
* `DatabaseAdapter` is the queue, safe with any number of instances.
|
|
18
|
+
*
|
|
19
|
+
* Composition root: wires the runtime, lifecycle, enqueue/cancel/views (`operations`), the claim
|
|
20
|
+
* (`claim`, `poll`) and the lease (`lease`) into a `PollableCaptureQueue`. Every attempt has its own
|
|
21
|
+
* row `<jobId>:<attempt>`, created by a unique-key insert, so claiming needs no compare-and-swap and
|
|
22
|
+
* a holder only ever writes its own attempt row.
|
|
23
|
+
*/
|
|
24
|
+
/** What the database queue reports about itself; hosts show it before the queue is built. */
|
|
25
|
+
const DATABASE_QUEUE_METADATA = {
|
|
26
|
+
name: "Database Queue",
|
|
27
|
+
version: "0.7.0",
|
|
28
|
+
description: "Capture queue on the app database (capture_jobs)",
|
|
29
|
+
kind: "database",
|
|
30
|
+
category: "capture-queue"
|
|
31
|
+
};
|
|
32
|
+
/** A `PollableCaptureQueue` over `db`'s `capture_jobs` table. */
|
|
33
|
+
function createDatabaseCaptureQueue(db, options = {}) {
|
|
34
|
+
const rt = resolveRuntime(db, options);
|
|
35
|
+
return {
|
|
36
|
+
metadata: DATABASE_QUEUE_METADATA,
|
|
37
|
+
lifecycle: buildQueueLifecycle(rt),
|
|
38
|
+
setLogger(bound) {
|
|
39
|
+
rt.logger ??= bound;
|
|
40
|
+
},
|
|
41
|
+
enqueue: async (job) => {
|
|
42
|
+
await enqueueCapture(rt, job);
|
|
43
|
+
},
|
|
44
|
+
enqueueJob: async (job) => await enqueueJob(rt, job),
|
|
45
|
+
status: async (buildId) => await statusOf(rt, buildId),
|
|
46
|
+
active: async () => await activeEntries(rt),
|
|
47
|
+
recent: async (limit) => await recentEntries(rt, limit),
|
|
48
|
+
cancel: async (id) => await cancelJobs(rt, id),
|
|
49
|
+
poll: async (pollOptions) => await pollJob(rt, pollOptions),
|
|
50
|
+
ack: async (job) => {
|
|
51
|
+
await ackJob(rt, job);
|
|
52
|
+
},
|
|
53
|
+
nack: async (job, nackOptions) => {
|
|
54
|
+
await nackJob(rt, job, nackOptions);
|
|
55
|
+
},
|
|
56
|
+
extend: async (job) => await extendJob(rt, job),
|
|
57
|
+
inspect: async (job) => await inspectJob(rt, job),
|
|
58
|
+
waitForWork: async (ms) => await rt.wake.wait(ms, rt.pollVersion)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function resolveRuntime(db, options) {
|
|
62
|
+
const kinds = options.kinds ?? ["capture"];
|
|
63
|
+
const retries = options.retries ?? 2;
|
|
64
|
+
if (kinds.length === 0) throw new Error("createDatabaseCaptureQueue: `kinds` must name at least one job kind");
|
|
65
|
+
if (!Number.isInteger(retries) || retries < 0) throw new Error("createDatabaseCaptureQueue: `retries` must be a non-negative integer");
|
|
66
|
+
return withHooks({
|
|
67
|
+
model: new CaptureJobModel(db),
|
|
68
|
+
kinds,
|
|
69
|
+
leaseMs: positive(options.leaseMs, DEFAULT_LEASE_MS),
|
|
70
|
+
maxAttempts: retries + 1,
|
|
71
|
+
owner: options.owner ?? `${hostname()}:${String(process.pid)}:${ulid().slice(-6)}`,
|
|
72
|
+
now: options.now ?? Date.now,
|
|
73
|
+
wake: createWakeSignal(options.timers),
|
|
74
|
+
pollVersion: 0
|
|
75
|
+
}, options);
|
|
76
|
+
}
|
|
77
|
+
/** The optional logger and exhausted-job hook, when given. */
|
|
78
|
+
function withHooks(rt, options) {
|
|
79
|
+
if (options.logger !== void 0) rt.logger = options.logger;
|
|
80
|
+
if (options.onExhausted !== void 0) rt.onExhausted = options.onExhausted;
|
|
81
|
+
return rt;
|
|
82
|
+
}
|
|
83
|
+
function positive(value, fallback) {
|
|
84
|
+
return value !== void 0 && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { DATABASE_QUEUE_METADATA, DEFAULT_LEASE_MS, DEFAULT_RETRIES, LEASE_EXHAUSTED_ERROR, MAX_NACK_DELAY_MS, createDatabaseCaptureQueue };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/capture/db-queue/index.ts"],"sourcesContent":["/**\n * Create the database capture queue (ADR 0006, decision 4): `capture_jobs` on the app's own\n * `DatabaseAdapter` is the queue, safe with any number of instances.\n *\n * Composition root: wires the runtime, lifecycle, enqueue/cancel/views (`operations`), the claim\n * (`claim`, `poll`) and the lease (`lease`) into a `PollableCaptureQueue`. Every attempt has its own\n * row `<jobId>:<attempt>`, created by a unique-key insert, so claiming needs no compare-and-swap and\n * a holder only ever writes its own attempt row.\n */\nimport { hostname } from \"node:os\";\nimport type { DatabaseAdapter } from \"../../adapters/database.ts\";\nimport type { Logger } from \"../../logger.ts\";\nimport { CaptureJobModel } from \"../../models/capture-job.ts\";\nimport { ulid } from \"../../utils/ulid.ts\";\nimport { extendJob, inspectJob } from \"./lease.ts\";\nimport { buildQueueLifecycle } from \"./lifecycle.ts\";\nimport {\n activeEntries,\n cancelJobs,\n enqueueCapture,\n enqueueJob,\n recentEntries,\n statusOf,\n} from \"./operations.ts\";\nimport { ackJob, nackJob, pollJob } from \"./poll.ts\";\nimport {\n DEFAULT_LEASE_MS,\n DEFAULT_RETRIES,\n type DatabaseCaptureQueue,\n type DatabaseCaptureQueueOptions,\n type QueueRuntime,\n} from \"./types.ts\";\nimport { createWakeSignal } from \"./wake.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/** What the database queue reports about itself; hosts show it before the queue is built. */\nexport const DATABASE_QUEUE_METADATA = {\n name: \"Database Queue\",\n version: typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0\",\n description: \"Capture queue on the app database (capture_jobs)\",\n kind: \"database\",\n category: \"capture-queue\",\n} as const;\n\n/** A `PollableCaptureQueue` over `db`'s `capture_jobs` table. */\nexport function createDatabaseCaptureQueue(\n db: DatabaseAdapter,\n options: DatabaseCaptureQueueOptions = {},\n): DatabaseCaptureQueue {\n const rt = resolveRuntime(db, options);\n return {\n metadata: DATABASE_QUEUE_METADATA,\n lifecycle: buildQueueLifecycle(rt),\n setLogger(bound: Logger): void {\n rt.logger ??= bound;\n },\n enqueue: async (job) => {\n await enqueueCapture(rt, job);\n },\n enqueueJob: async (job) => await enqueueJob(rt, job),\n status: async (buildId) => await statusOf(rt, buildId),\n active: async () => await activeEntries(rt),\n recent: async (limit) => await recentEntries(rt, limit),\n cancel: async (id) => await cancelJobs(rt, id),\n poll: async (pollOptions) => await pollJob(rt, pollOptions),\n ack: async (job) => {\n await ackJob(rt, job);\n },\n nack: async (job, nackOptions) => {\n await nackJob(rt, job, nackOptions);\n },\n extend: async (job) => await extendJob(rt, job),\n inspect: async (job) => await inspectJob(rt, job),\n waitForWork: async (ms) => await rt.wake.wait(ms, rt.pollVersion),\n };\n}\n\nfunction resolveRuntime(db: DatabaseAdapter, options: DatabaseCaptureQueueOptions): QueueRuntime {\n const kinds = options.kinds ?? [\"capture\"];\n const retries = options.retries ?? DEFAULT_RETRIES;\n if (kinds.length === 0) {\n throw new Error(\"createDatabaseCaptureQueue: `kinds` must name at least one job kind\");\n }\n if (!Number.isInteger(retries) || retries < 0) {\n throw new Error(\"createDatabaseCaptureQueue: `retries` must be a non-negative integer\");\n }\n const rt: QueueRuntime = {\n model: new CaptureJobModel(db),\n kinds,\n leaseMs: positive(options.leaseMs, DEFAULT_LEASE_MS),\n maxAttempts: retries + 1,\n owner: options.owner ?? `${hostname()}:${String(process.pid)}:${ulid().slice(-6)}`,\n now: options.now ?? Date.now,\n wake: createWakeSignal(options.timers),\n pollVersion: 0,\n };\n return withHooks(rt, options);\n}\n\n/** The optional logger and exhausted-job hook, when given. */\nfunction withHooks(rt: QueueRuntime, options: DatabaseCaptureQueueOptions): QueueRuntime {\n if (options.logger !== undefined) {\n rt.logger = options.logger;\n }\n if (options.onExhausted !== undefined) {\n rt.onExhausted = options.onExhausted;\n }\n return rt;\n}\n\nfunction positive(value: number | undefined, fallback: number): number {\n return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;\n}\n\nexport {\n DEFAULT_LEASE_MS,\n DEFAULT_RETRIES,\n type DatabaseCaptureQueue,\n type DatabaseCaptureQueueOptions,\n type DatabaseJob,\n type QueueJobInput,\n} from \"./types.ts\";\nexport { LEASE_EXHAUSTED_ERROR } from \"./claim.ts\";\nexport { MAX_NACK_DELAY_MS } from \"./poll.ts\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAqCA,MAAa,0BAA0B;CACrC,MAAM;CACN,SAAA;CACA,aAAa;CACb,MAAM;CACN,UAAU;AACZ;;AAGA,SAAgB,2BACd,IACA,UAAuC,CAAC,GAClB;CACtB,MAAM,KAAK,eAAe,IAAI,OAAO;CACrC,OAAO;EACL,UAAU;EACV,WAAW,oBAAoB,EAAE;EACjC,UAAU,OAAqB;GAC7B,GAAG,WAAW;EAChB;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,eAAe,IAAI,GAAG;EAC9B;EACA,YAAY,OAAO,QAAQ,MAAM,WAAW,IAAI,GAAG;EACnD,QAAQ,OAAO,YAAY,MAAM,SAAS,IAAI,OAAO;EACrD,QAAQ,YAAY,MAAM,cAAc,EAAE;EAC1C,QAAQ,OAAO,UAAU,MAAM,cAAc,IAAI,KAAK;EACtD,QAAQ,OAAO,OAAO,MAAM,WAAW,IAAI,EAAE;EAC7C,MAAM,OAAO,gBAAgB,MAAM,QAAQ,IAAI,WAAW;EAC1D,KAAK,OAAO,QAAQ;GAClB,MAAM,OAAO,IAAI,GAAG;EACtB;EACA,MAAM,OAAO,KAAK,gBAAgB;GAChC,MAAM,QAAQ,IAAI,KAAK,WAAW;EACpC;EACA,QAAQ,OAAO,QAAQ,MAAM,UAAU,IAAI,GAAG;EAC9C,SAAS,OAAO,QAAQ,MAAM,WAAW,IAAI,GAAG;EAChD,aAAa,OAAO,OAAO,MAAM,GAAG,KAAK,KAAK,IAAI,GAAG,WAAW;CAClE;AACF;AAEA,SAAS,eAAe,IAAqB,SAAoD;CAC/F,MAAM,QAAQ,QAAQ,SAAS,CAAC,SAAS;CACzC,MAAM,UAAU,QAAQ,WAAA;CACxB,IAAI,MAAM,WAAW,GACnB,MAAM,IAAI,MAAM,qEAAqE;CAEvF,IAAI,CAAC,OAAO,UAAU,OAAO,KAAK,UAAU,GAC1C,MAAM,IAAI,MAAM,sEAAsE;CAYxF,OAAO,UAAU;EATf,OAAO,IAAI,gBAAgB,EAAE;EAC7B;EACA,SAAS,SAAS,QAAQ,SAAS,gBAAgB;EACnD,aAAa,UAAU;EACvB,OAAO,QAAQ,SAAS,GAAG,SAAS,EAAE,GAAG,OAAO,QAAQ,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE;EAC/E,KAAK,QAAQ,OAAO,KAAK;EACzB,MAAM,iBAAiB,QAAQ,MAAM;EACrC,aAAa;CAEG,GAAG,OAAO;AAC9B;;AAGA,SAAS,UAAU,IAAkB,SAAoD;CACvF,IAAI,QAAQ,WAAW,KAAA,GACrB,GAAG,SAAS,QAAQ;CAEtB,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,GAAG,cAAc,QAAQ;CAE3B,OAAO;AACT;AAEA,SAAS,SAAS,OAA2B,UAA0B;CACrE,OAAO,UAAU,KAAA,KAAa,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,QAAQ;AAC9E"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { Logger } from "../../logger.mjs";
|
|
4
|
+
import { LeaseRenewal, PollableCaptureQueue, PollableJob } from "../../adapter/capture-queue.mjs";
|
|
5
|
+
import { QueueTimers } from "./timers.mjs";
|
|
6
|
+
//#region src/capture/db-queue/keeper.d.ts
|
|
7
|
+
/** Longest gap between two renewals while progress keeps coming. */
|
|
8
|
+
declare const RENEW_THROTTLE_MS = 30000;
|
|
9
|
+
/** Why a held job must stop. */
|
|
10
|
+
type LeaseEnd = Exclude<LeaseRenewal, "ok">;
|
|
11
|
+
/** Options of {@link keepJobLease}. */
|
|
12
|
+
interface LeaseKeeperOptions {
|
|
13
|
+
/** Renew at most this often (default `min(RENEW_THROTTLE_MS, leaseMs / 3)`). */
|
|
14
|
+
renewEveryMs?: number;
|
|
15
|
+
/** State check interval (default {@link CANCEL_CHECK_MS}). */
|
|
16
|
+
checkMs?: number;
|
|
17
|
+
/** Clock in epoch ms (default `Date.now`). */
|
|
18
|
+
now?: () => number;
|
|
19
|
+
/** Timers of the state check (default the global timers). */
|
|
20
|
+
timers?: QueueTimers;
|
|
21
|
+
logger?: Logger;
|
|
22
|
+
}
|
|
23
|
+
/** Handle on a held job's lease. */
|
|
24
|
+
interface JobLeaseKeeper {
|
|
25
|
+
/** Report progress: renews the lease when the throttle allows. */
|
|
26
|
+
touch(): void;
|
|
27
|
+
/** Resolves once, when the job was cancelled or the lease lost; never while the job is ours. */
|
|
28
|
+
readonly ended: Promise<LeaseEnd>;
|
|
29
|
+
/** Stop checking; idempotent. */
|
|
30
|
+
stop(): void;
|
|
31
|
+
}
|
|
32
|
+
/** Keep `job`'s lease on `queue` while the caller reports progress through `touch`. */
|
|
33
|
+
declare function keepJobLease(queue: Pick<PollableCaptureQueue, "extend" | "inspect">, job: PollableJob, options?: LeaseKeeperOptions): JobLeaseKeeper;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { JobLeaseKeeper, LeaseEnd, LeaseKeeperOptions, RENEW_THROTTLE_MS, keepJobLease };
|
|
36
|
+
//# sourceMappingURL=keeper.d.mts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { REAL_TIMERS, repeatEvery } from "./timers.mjs";
|
|
5
|
+
import "./cadence.mjs";
|
|
6
|
+
//#region src/capture/db-queue/keeper.ts
|
|
7
|
+
/** Longest gap between two renewals while progress keeps coming. */
|
|
8
|
+
const RENEW_THROTTLE_MS = 3e4;
|
|
9
|
+
/** Keep `job`'s lease on `queue` while the caller reports progress through `touch`. */
|
|
10
|
+
function keepJobLease(queue, job, options = {}) {
|
|
11
|
+
const now = options.now ?? Date.now;
|
|
12
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
13
|
+
const state = {
|
|
14
|
+
lastRenewAt: now(),
|
|
15
|
+
renewing: false,
|
|
16
|
+
warned: false,
|
|
17
|
+
stopped: false,
|
|
18
|
+
settle: (end) => {
|
|
19
|
+
state.stopped = true;
|
|
20
|
+
resolve(end);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const stopChecks = startChecks(queue, job, state, options);
|
|
24
|
+
const stopBeat = startHeartbeat(queue, job, state, {
|
|
25
|
+
...options,
|
|
26
|
+
now
|
|
27
|
+
});
|
|
28
|
+
const stop = () => {
|
|
29
|
+
state.stopped = true;
|
|
30
|
+
stopChecks();
|
|
31
|
+
stopBeat();
|
|
32
|
+
};
|
|
33
|
+
promise.then(stop, stop);
|
|
34
|
+
return {
|
|
35
|
+
touch: () => {
|
|
36
|
+
renewIfDue(queue, job, state, {
|
|
37
|
+
...options,
|
|
38
|
+
now
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
ended: promise,
|
|
42
|
+
stop
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function startChecks(queue, job, state, options) {
|
|
46
|
+
const { inspect } = queue;
|
|
47
|
+
if (inspect === void 0) return () => {};
|
|
48
|
+
return repeatEvery(options.timers ?? REAL_TIMERS, options.checkMs ?? 5e3, () => {
|
|
49
|
+
if (!state.stopped) report(state, inspect(job), options.logger, job).catch(() => {});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Renew on a timer, so a silent stage keeps its lease; progress renewals count toward the interval. */
|
|
53
|
+
function startHeartbeat(queue, job, state, options) {
|
|
54
|
+
if (queue.extend === void 0) return () => {};
|
|
55
|
+
return repeatEvery(options.timers ?? REAL_TIMERS, renewInterval(job, options), () => {
|
|
56
|
+
renewIfDue(queue, job, state, options);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function renewIfDue(queue, job, state, options) {
|
|
60
|
+
const { extend } = queue;
|
|
61
|
+
const at = options.now();
|
|
62
|
+
if (extend === void 0 || state.stopped || state.renewing || at - state.lastRenewAt < renewInterval(job, options)) return;
|
|
63
|
+
state.renewing = true;
|
|
64
|
+
state.lastRenewAt = at;
|
|
65
|
+
report(state, extend(job), options.logger, job).finally(() => {
|
|
66
|
+
state.renewing = false;
|
|
67
|
+
}).catch(() => {});
|
|
68
|
+
}
|
|
69
|
+
/** Settle on a non-ok outcome; a failed call is transient (warned once, the next one retries). */
|
|
70
|
+
async function report(state, outcome, logger, job) {
|
|
71
|
+
try {
|
|
72
|
+
const result = await outcome;
|
|
73
|
+
if (result !== "ok" && !state.stopped) state.settle(result);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!state.warned) {
|
|
76
|
+
state.warned = true;
|
|
77
|
+
logger?.warn({
|
|
78
|
+
err: error,
|
|
79
|
+
buildId: job.buildId
|
|
80
|
+
}, "failed to renew or check queue lease");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function renewInterval(job, options) {
|
|
85
|
+
if (options.renewEveryMs !== void 0) return options.renewEveryMs;
|
|
86
|
+
const third = job.leaseMs === void 0 ? RENEW_THROTTLE_MS : Math.floor(job.leaseMs / 3);
|
|
87
|
+
return Math.max(1, Math.min(RENEW_THROTTLE_MS, third));
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { RENEW_THROTTLE_MS, keepJobLease };
|
|
91
|
+
|
|
92
|
+
//# sourceMappingURL=keeper.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keeper.mjs","names":[],"sources":["../../../src/capture/db-queue/keeper.ts"],"sourcesContent":["/**\n * The holder's side of a lease: while the handler is alive the lease is renewed by a heartbeat\n * timer (every `min(30 s, lease / 3)`) and by progress (throttled to the same interval), so a stage\n * that prints nothing for longer than the lease is not redelivered. A holder that dies stops\n * renewing and its lease expires. A separate timer reads the job's state every 5 s, so cancel and\n * takeover are seen within that interval even while the run is hung. The heartbeat stops with the\n * keeper: on completion, on a lost lease and on cancel.\n */\nimport type {\n LeaseRenewal,\n PollableCaptureQueue,\n PollableJob,\n} from \"../../adapters/capture-queue.ts\";\nimport type { Logger } from \"../../logger.ts\";\nimport { CANCEL_CHECK_MS } from \"./cadence.ts\";\nimport { REAL_TIMERS, repeatEvery, type QueueTimers } from \"./timers.ts\";\n\n/** Longest gap between two renewals while progress keeps coming. */\nexport const RENEW_THROTTLE_MS = 30_000;\n\n/** Why a held job must stop. */\nexport type LeaseEnd = Exclude<LeaseRenewal, \"ok\">;\n\n/** Options of {@link keepJobLease}. */\nexport interface LeaseKeeperOptions {\n /** Renew at most this often (default `min(RENEW_THROTTLE_MS, leaseMs / 3)`). */\n renewEveryMs?: number;\n /** State check interval (default {@link CANCEL_CHECK_MS}). */\n checkMs?: number;\n /** Clock in epoch ms (default `Date.now`). */\n now?: () => number;\n /** Timers of the state check (default the global timers). */\n timers?: QueueTimers;\n logger?: Logger;\n}\n\n/** Handle on a held job's lease. */\nexport interface JobLeaseKeeper {\n /** Report progress: renews the lease when the throttle allows. */\n touch(): void;\n /** Resolves once, when the job was cancelled or the lease lost; never while the job is ours. */\n readonly ended: Promise<LeaseEnd>;\n /** Stop checking; idempotent. */\n stop(): void;\n}\n\ninterface KeeperState {\n lastRenewAt: number;\n renewing: boolean;\n warned: boolean;\n stopped: boolean;\n settle: (end: LeaseEnd) => void;\n}\n\n/** Keep `job`'s lease on `queue` while the caller reports progress through `touch`. */\nexport function keepJobLease(\n queue: Pick<PollableCaptureQueue, \"extend\" | \"inspect\">,\n job: PollableJob,\n options: LeaseKeeperOptions = {},\n): JobLeaseKeeper {\n const now = options.now ?? Date.now;\n const { promise, resolve } = Promise.withResolvers<LeaseEnd>();\n const state: KeeperState = {\n lastRenewAt: now(),\n renewing: false,\n warned: false,\n stopped: false,\n settle: (end) => {\n state.stopped = true;\n resolve(end);\n },\n };\n const stopChecks = startChecks(queue, job, state, options);\n const stopBeat = startHeartbeat(queue, job, state, { ...options, now });\n const stop = (): void => {\n state.stopped = true;\n stopChecks();\n stopBeat();\n };\n promise.then(stop, stop);\n return {\n touch: () => {\n renewIfDue(queue, job, state, { ...options, now });\n },\n ended: promise,\n stop,\n };\n}\n\nfunction startChecks(\n queue: Pick<PollableCaptureQueue, \"inspect\">,\n job: PollableJob,\n state: KeeperState,\n options: LeaseKeeperOptions,\n): () => void {\n const { inspect } = queue;\n if (inspect === undefined) {\n return () => {};\n }\n return repeatEvery(options.timers ?? REAL_TIMERS, options.checkMs ?? CANCEL_CHECK_MS, () => {\n if (!state.stopped) {\n report(state, inspect(job), options.logger, job).catch(() => {});\n }\n });\n}\n\n/** Renew on a timer, so a silent stage keeps its lease; progress renewals count toward the interval. */\nfunction startHeartbeat(\n queue: Pick<PollableCaptureQueue, \"extend\">,\n job: PollableJob,\n state: KeeperState,\n options: LeaseKeeperOptions & { now: () => number },\n): () => void {\n if (queue.extend === undefined) {\n return () => {};\n }\n return repeatEvery(options.timers ?? REAL_TIMERS, renewInterval(job, options), () => {\n renewIfDue(queue, job, state, options);\n });\n}\n\nfunction renewIfDue(\n queue: Pick<PollableCaptureQueue, \"extend\">,\n job: PollableJob,\n state: KeeperState,\n options: LeaseKeeperOptions & { now: () => number },\n): void {\n const { extend } = queue;\n const at = options.now();\n if (\n extend === undefined ||\n state.stopped ||\n state.renewing ||\n at - state.lastRenewAt < renewInterval(job, options)\n ) {\n return;\n }\n state.renewing = true;\n state.lastRenewAt = at;\n report(state, extend(job), options.logger, job)\n .finally(() => {\n state.renewing = false;\n })\n .catch(() => {});\n}\n\n/** Settle on a non-ok outcome; a failed call is transient (warned once, the next one retries). */\nasync function report(\n state: KeeperState,\n outcome: Promise<LeaseRenewal>,\n logger: Logger | undefined,\n job: PollableJob,\n): Promise<void> {\n try {\n const result = await outcome;\n if (result !== \"ok\" && !state.stopped) {\n state.settle(result);\n }\n } catch (error) {\n if (!state.warned) {\n state.warned = true;\n logger?.warn({ err: error, buildId: job.buildId }, \"failed to renew or check queue lease\");\n }\n }\n}\n\nfunction renewInterval(job: PollableJob, options: LeaseKeeperOptions): number {\n if (options.renewEveryMs !== undefined) {\n return options.renewEveryMs;\n }\n const third = job.leaseMs === undefined ? RENEW_THROTTLE_MS : Math.floor(job.leaseMs / 3);\n return Math.max(1, Math.min(RENEW_THROTTLE_MS, third));\n}\n"],"mappings":";;;;;;;AAkBA,MAAa,oBAAoB;;AAqCjC,SAAgB,aACd,OACA,KACA,UAA8B,CAAC,GACf;CAChB,MAAM,MAAM,QAAQ,OAAO,KAAK;CAChC,MAAM,EAAE,SAAS,YAAY,QAAQ,cAAwB;CAC7D,MAAM,QAAqB;EACzB,aAAa,IAAI;EACjB,UAAU;EACV,QAAQ;EACR,SAAS;EACT,SAAS,QAAQ;GACf,MAAM,UAAU;GAChB,QAAQ,GAAG;EACb;CACF;CACA,MAAM,aAAa,YAAY,OAAO,KAAK,OAAO,OAAO;CACzD,MAAM,WAAW,eAAe,OAAO,KAAK,OAAO;EAAE,GAAG;EAAS;CAAI,CAAC;CACtE,MAAM,aAAmB;EACvB,MAAM,UAAU;EAChB,WAAW;EACX,SAAS;CACX;CACA,QAAQ,KAAK,MAAM,IAAI;CACvB,OAAO;EACL,aAAa;GACX,WAAW,OAAO,KAAK,OAAO;IAAE,GAAG;IAAS;GAAI,CAAC;EACnD;EACA,OAAO;EACP;CACF;AACF;AAEA,SAAS,YACP,OACA,KACA,OACA,SACY;CACZ,MAAM,EAAE,YAAY;CACpB,IAAI,YAAY,KAAA,GACd,aAAa,CAAC;CAEhB,OAAO,YAAY,QAAQ,UAAU,aAAa,QAAQ,WAAA,WAAkC;EAC1F,IAAI,CAAC,MAAM,SACT,OAAO,OAAO,QAAQ,GAAG,GAAG,QAAQ,QAAQ,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;CAEnE,CAAC;AACH;;AAGA,SAAS,eACP,OACA,KACA,OACA,SACY;CACZ,IAAI,MAAM,WAAW,KAAA,GACnB,aAAa,CAAC;CAEhB,OAAO,YAAY,QAAQ,UAAU,aAAa,cAAc,KAAK,OAAO,SAAS;EACnF,WAAW,OAAO,KAAK,OAAO,OAAO;CACvC,CAAC;AACH;AAEA,SAAS,WACP,OACA,KACA,OACA,SACM;CACN,MAAM,EAAE,WAAW;CACnB,MAAM,KAAK,QAAQ,IAAI;CACvB,IACE,WAAW,KAAA,KACX,MAAM,WACN,MAAM,YACN,KAAK,MAAM,cAAc,cAAc,KAAK,OAAO,GAEnD;CAEF,MAAM,WAAW;CACjB,MAAM,cAAc;CACpB,OAAO,OAAO,OAAO,GAAG,GAAG,QAAQ,QAAQ,GAAG,CAAC,CAC5C,cAAc;EACb,MAAM,WAAW;CACnB,CAAC,CAAC,CACD,YAAY,CAAC,CAAC;AACnB;;AAGA,eAAe,OACb,OACA,SACA,QACA,KACe;CACf,IAAI;EACF,MAAM,SAAS,MAAM;EACrB,IAAI,WAAW,QAAQ,CAAC,MAAM,SAC5B,MAAM,OAAO,MAAM;CAEvB,SAAS,OAAO;EACd,IAAI,CAAC,MAAM,QAAQ;GACjB,MAAM,SAAS;GACf,QAAQ,KAAK;IAAE,KAAK;IAAO,SAAS,IAAI;GAAQ,GAAG,sCAAsC;EAC3F;CACF;AACF;AAEA,SAAS,cAAc,KAAkB,SAAqC;CAC5E,IAAI,QAAQ,iBAAiB,KAAA,GAC3B,OAAO,QAAQ;CAEjB,MAAM,QAAQ,IAAI,YAAY,KAAA,IAAY,oBAAoB,KAAK,MAAM,IAAI,UAAU,CAAC;CACxF,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,mBAAmB,KAAK,CAAC;AACvD"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { isTerminalJob } from "../../models/capture-job.mjs";
|
|
5
|
+
import { isoAt } from "./clock.mjs";
|
|
6
|
+
import { attemptRef } from "./poll.mjs";
|
|
7
|
+
//#region src/capture/db-queue/lease.ts
|
|
8
|
+
/**
|
|
9
|
+
* Renew the lease by another `leaseMs`. The write touches only the holder's attempt row and returns
|
|
10
|
+
* it: an attempt retired by another instance reads `expired` (lost), a flagged one `cancelled`.
|
|
11
|
+
* When the attempt is still ours, the job row is read too, so a job finished by another attempt or
|
|
12
|
+
* cancelled is noticed at this progress touch.
|
|
13
|
+
*/
|
|
14
|
+
async function extendJob(rt, job) {
|
|
15
|
+
const row = await renewOwn(rt, attemptRef(job).receipt);
|
|
16
|
+
const own = attemptState(row);
|
|
17
|
+
if (row !== null && row.status !== "running") await rt.model.clearVisibility(row.id, isoAt(rt));
|
|
18
|
+
return own === "ok" ? await inspectJob(rt, job) : own;
|
|
19
|
+
}
|
|
20
|
+
/** The job's state for the holder of `job`, without renewing: the periodic cancel check. */
|
|
21
|
+
async function inspectJob(rt, job) {
|
|
22
|
+
const ref = attemptRef(job);
|
|
23
|
+
return jobState(await rt.model.get(ref.jobId), ref.attempt);
|
|
24
|
+
}
|
|
25
|
+
function attemptState(row) {
|
|
26
|
+
if (row?.status !== "running") return "lost";
|
|
27
|
+
return row.cancelRequestedAt === null ? "ok" : "cancelled";
|
|
28
|
+
}
|
|
29
|
+
function jobState(row, attempt) {
|
|
30
|
+
if (row === null) return "lost";
|
|
31
|
+
if (row.status === "cancelled" || row.cancelRequestedAt !== null && !isTerminalJob(row)) return "cancelled";
|
|
32
|
+
return isTerminalJob(row) || row.attempt !== attempt ? "lost" : "ok";
|
|
33
|
+
}
|
|
34
|
+
/** Renew an attempt row; a row that no longer exists is lost, any other failure is transient. */
|
|
35
|
+
async function renewOwn(rt, receipt) {
|
|
36
|
+
try {
|
|
37
|
+
return await rt.model.renew(receipt, isoAt(rt, rt.leaseMs), isoAt(rt));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (await rt.model.get(receipt) === null) return null;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { extendJob, inspectJob };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=lease.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lease.mjs","names":[],"sources":["../../../src/capture/db-queue/lease.ts"],"sourcesContent":["/**\n * Lease operations of the database queue: `extend` renews the holder's own attempt row (and never\n * any other), `inspect` reads the job row for cancel and supersession without renewing anything.\n */\nimport type { LeaseRenewal, PollableJob } from \"../../adapters/capture-queue.ts\";\nimport { isTerminalJob } from \"../../models/capture-job.ts\";\nimport type { CaptureJobRow } from \"../../schema/capture-job.ts\";\nimport { isoAt } from \"./clock.ts\";\nimport { attemptRef } from \"./poll.ts\";\nimport type { QueueRuntime } from \"./types.ts\";\n\n/**\n * Renew the lease by another `leaseMs`. The write touches only the holder's attempt row and returns\n * it: an attempt retired by another instance reads `expired` (lost), a flagged one `cancelled`.\n * When the attempt is still ours, the job row is read too, so a job finished by another attempt or\n * cancelled is noticed at this progress touch.\n */\nexport async function extendJob(rt: QueueRuntime, job: PollableJob): Promise<LeaseRenewal> {\n const ref = attemptRef(job);\n const row = await renewOwn(rt, ref.receipt);\n const own = attemptState(row);\n if (row !== null && row.status !== \"running\") {\n // A late renewal of a retired row put a lease back on it; take the row out of the poll's range.\n await rt.model.clearVisibility(row.id, isoAt(rt));\n }\n return own === \"ok\" ? await inspectJob(rt, job) : own;\n}\n\n/** The job's state for the holder of `job`, without renewing: the periodic cancel check. */\nexport async function inspectJob(rt: QueueRuntime, job: PollableJob): Promise<LeaseRenewal> {\n const ref = attemptRef(job);\n return jobState(await rt.model.get(ref.jobId), ref.attempt);\n}\n\nfunction attemptState(row: CaptureJobRow | null): LeaseRenewal {\n if (row?.status !== \"running\") {\n return \"lost\";\n }\n return row.cancelRequestedAt === null ? \"ok\" : \"cancelled\";\n}\n\nfunction jobState(row: CaptureJobRow | null, attempt: number): LeaseRenewal {\n if (row === null) {\n return \"lost\";\n }\n if (row.status === \"cancelled\" || (row.cancelRequestedAt !== null && !isTerminalJob(row))) {\n return \"cancelled\";\n }\n return isTerminalJob(row) || row.attempt !== attempt ? \"lost\" : \"ok\";\n}\n\n/** Renew an attempt row; a row that no longer exists is lost, any other failure is transient. */\nasync function renewOwn(rt: QueueRuntime, receipt: string): Promise<CaptureJobRow | null> {\n try {\n return await rt.model.renew(receipt, isoAt(rt, rt.leaseMs), isoAt(rt));\n } catch (error) {\n if ((await rt.model.get(receipt)) === null) {\n return null;\n }\n throw error;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAiBA,eAAsB,UAAU,IAAkB,KAAyC;CAEzF,MAAM,MAAM,MAAM,SAAS,IADf,WAAW,GACU,CAAC,CAAC,OAAO;CAC1C,MAAM,MAAM,aAAa,GAAG;CAC5B,IAAI,QAAQ,QAAQ,IAAI,WAAW,WAEjC,MAAM,GAAG,MAAM,gBAAgB,IAAI,IAAI,MAAM,EAAE,CAAC;CAElD,OAAO,QAAQ,OAAO,MAAM,WAAW,IAAI,GAAG,IAAI;AACpD;;AAGA,eAAsB,WAAW,IAAkB,KAAyC;CAC1F,MAAM,MAAM,WAAW,GAAG;CAC1B,OAAO,SAAS,MAAM,GAAG,MAAM,IAAI,IAAI,KAAK,GAAG,IAAI,OAAO;AAC5D;AAEA,SAAS,aAAa,KAAyC;CAC7D,IAAI,KAAK,WAAW,WAClB,OAAO;CAET,OAAO,IAAI,sBAAsB,OAAO,OAAO;AACjD;AAEA,SAAS,SAAS,KAA2B,SAA+B;CAC1E,IAAI,QAAQ,MACV,OAAO;CAET,IAAI,IAAI,WAAW,eAAgB,IAAI,sBAAsB,QAAQ,CAAC,cAAc,GAAG,GACrF,OAAO;CAET,OAAO,cAAc,GAAG,KAAK,IAAI,YAAY,UAAU,SAAS;AAClE;;AAGA,eAAe,SAAS,IAAkB,SAAgD;CACxF,IAAI;EACF,OAAO,MAAM,GAAG,MAAM,MAAM,SAAS,MAAM,IAAI,GAAG,OAAO,GAAG,MAAM,EAAE,CAAC;CACvE,SAAS,OAAO;EACd,IAAK,MAAM,GAAG,MAAM,IAAI,OAAO,MAAO,MACpC,OAAO;EAET,MAAM;CACR;AACF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/capture/db-queue/lifecycle.ts
|
|
5
|
+
/** Id no row ever has: the health probe is a primary-key read that finds nothing. */
|
|
6
|
+
const HEALTH_PROBE_ID = "~health";
|
|
7
|
+
/**
|
|
8
|
+
* Setup is a no-op (the database adapter's own setup creates `capture_jobs`); teardown releases any
|
|
9
|
+
* poll waiting on a local wake; health reads the table by primary key.
|
|
10
|
+
*/
|
|
11
|
+
function buildQueueLifecycle(rt) {
|
|
12
|
+
return {
|
|
13
|
+
setup: async () => {
|
|
14
|
+
await Promise.resolve();
|
|
15
|
+
},
|
|
16
|
+
teardown: async () => {
|
|
17
|
+
rt.wake.close();
|
|
18
|
+
await Promise.resolve();
|
|
19
|
+
},
|
|
20
|
+
health: async () => {
|
|
21
|
+
try {
|
|
22
|
+
await rt.model.get(HEALTH_PROBE_ID);
|
|
23
|
+
return { ok: true };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { buildQueueLifecycle };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=lifecycle.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.mjs","names":[],"sources":["../../../src/capture/db-queue/lifecycle.ts"],"sourcesContent":["/** Lifecycle of the database queue: the database adapter owns its connection, the queue its waiters. */\nimport type { AdapterLifecycle } from \"../../adapters/metadata.ts\";\nimport type { QueueRuntime } from \"./types.ts\";\n\n/** Id no row ever has: the health probe is a primary-key read that finds nothing. */\nconst HEALTH_PROBE_ID = \"~health\";\n\n/**\n * Setup is a no-op (the database adapter's own setup creates `capture_jobs`); teardown releases any\n * poll waiting on a local wake; health reads the table by primary key.\n */\nexport function buildQueueLifecycle(rt: QueueRuntime): AdapterLifecycle {\n return {\n setup: async () => {\n await Promise.resolve();\n },\n teardown: async () => {\n rt.wake.close();\n await Promise.resolve();\n },\n health: async () => {\n try {\n await rt.model.get(HEALTH_PROBE_ID);\n return { ok: true };\n } catch (error) {\n return { ok: false, detail: error instanceof Error ? error.message : String(error) };\n }\n },\n };\n}\n"],"mappings":";;;;;AAKA,MAAM,kBAAkB;;;;;AAMxB,SAAgB,oBAAoB,IAAoC;CACtE,OAAO;EACL,OAAO,YAAY;GACjB,MAAM,QAAQ,QAAQ;EACxB;EACA,UAAU,YAAY;GACpB,GAAG,KAAK,MAAM;GACd,MAAM,QAAQ,QAAQ;EACxB;EACA,QAAQ,YAAY;GAClB,IAAI;IACF,MAAM,GAAG,MAAM,IAAI,eAAe;IAClC,OAAO,EAAE,IAAI,KAAK;GACpB,SAAS,OAAO;IACd,OAAO;KAAE,IAAI;KAAO,QAAQ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAAE;GACrF;EACF;CACF;AACF"}
|