firedrill-run 2.2.5 → 3.0.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 +178 -22
- package/bin/firedrill.mjs +29 -5
- package/dist/assembly.json +13 -0
- package/dist/cli/runtime/candidate_copy.py +395 -0
- package/dist/cli/runtime/source_snapshot.py +391 -0
- package/dist/cli/src/administration.js +295 -0
- package/dist/cli/src/agent-changes.js +496 -0
- package/dist/cli/src/assessment-requests.js +67 -0
- package/dist/cli/src/auth.js +465 -0
- package/dist/cli/src/billing-commerce-contracts.js +1 -0
- package/dist/cli/src/billing-usage.js +227 -0
- package/dist/cli/src/billing-wallet-contracts.js +1 -0
- package/dist/cli/src/billing.js +170 -0
- package/dist/cli/src/bootstrap-process-lock.js +136 -0
- package/dist/cli/src/browser-links.js +14 -0
- package/dist/cli/src/campaign-controls.js +122 -0
- package/dist/cli/src/campaign-diagnoses.js +118 -0
- package/dist/cli/src/campaign-reports.js +1215 -0
- package/dist/cli/src/campaign-world.js +156 -0
- package/dist/cli/src/candidate-copy.js +376 -0
- package/dist/cli/src/catalog-contracts.js +1 -0
- package/dist/cli/src/ci-access-contracts.js +1 -0
- package/dist/cli/src/ci-configuration-contracts.js +1 -0
- package/dist/cli/src/ci-coverage.js +64 -0
- package/dist/cli/src/ci-delivery-contracts.js +1 -0
- package/dist/cli/src/ci-result-contracts.js +1 -0
- package/dist/cli/src/ci-run-contracts.js +1 -0
- package/dist/cli/src/ci-setup-proposals.js +52 -0
- package/dist/cli/src/company-policies.js +229 -0
- package/dist/cli/src/completion.js +79 -0
- package/dist/cli/src/configuration-intents.js +277 -0
- package/dist/cli/src/configuration.js +369 -0
- package/dist/cli/src/continuous-simulation.js +277 -0
- package/dist/cli/src/coordinated-checkpoints.js +133 -0
- package/dist/cli/src/credential-mode.js +139 -0
- package/dist/cli/src/credentials.js +331 -0
- package/dist/cli/src/credit-format.js +8 -0
- package/dist/cli/src/dataset-contracts.js +1 -0
- package/dist/cli/src/dataset-export.js +154 -0
- package/dist/cli/src/dataset-reads.js +37 -0
- package/dist/cli/src/dependency-preparation.js +239 -0
- package/dist/cli/src/director-tool-setup.js +104 -0
- package/dist/cli/src/driver-review.js +225 -0
- package/dist/cli/src/evaluation-authoring.js +315 -0
- package/dist/cli/src/evidence-bundles.js +367 -0
- package/dist/cli/src/execution-proposals.js +36 -0
- package/dist/cli/src/http.js +302 -0
- package/dist/cli/src/local-container-boundary.js +38 -0
- package/dist/cli/src/local-container-policy.js +201 -0
- package/dist/cli/src/local-execution-setup.js +81 -0
- package/dist/cli/src/local-source-basis.js +73 -0
- package/dist/cli/src/local-source.js +66 -0
- package/dist/cli/src/local-value-store.js +91 -0
- package/dist/cli/src/local-values.js +163 -0
- package/dist/cli/src/login-organizations.js +70 -0
- package/dist/cli/src/main.js +1622 -0
- package/dist/cli/src/mcp-connection.js +109 -0
- package/dist/cli/src/native/bindings.js +137 -0
- package/dist/cli/src/native/canonical.js +370 -0
- package/dist/cli/src/native/cloud-resources.js +942 -0
- package/dist/cli/src/native/companion-channel.js +421 -0
- package/dist/cli/src/native/companion-ipc.js +473 -0
- package/dist/cli/src/native/contracts.js +218 -0
- package/dist/cli/src/native/control-plane.js +1354 -0
- package/dist/cli/src/native/credit-approval.js +15 -0
- package/dist/cli/src/native/dependency-proxy.js +1006 -0
- package/dist/cli/src/native/drill-cleanup.js +115 -0
- package/dist/cli/src/native/drill-runner.js +1562 -0
- package/dist/cli/src/native/drivers/action-trace.js +210 -0
- package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
- package/dist/cli/src/native/drivers/http.js +442 -0
- package/dist/cli/src/native/drivers/index.js +47 -0
- package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
- package/dist/cli/src/native/drivers/playwright.js +600 -0
- package/dist/cli/src/native/drivers/signed-event.js +244 -0
- package/dist/cli/src/native/drivers/types.js +578 -0
- package/dist/cli/src/native/evidence-filter.js +255 -0
- package/dist/cli/src/native/isolated-access.js +241 -0
- package/dist/cli/src/native/library-sharing.js +220 -0
- package/dist/cli/src/native/local-run.js +667 -0
- package/dist/cli/src/native/local-secrets.js +19 -0
- package/dist/cli/src/native/mcp-config.js +123 -0
- package/dist/cli/src/native/metadata-requests.js +120 -0
- package/dist/cli/src/native/observations.js +1159 -0
- package/dist/cli/src/native/owned-groups.js +46 -0
- package/dist/cli/src/native/plan.js +594 -0
- package/dist/cli/src/native/png.js +159 -0
- package/dist/cli/src/native/ports.js +43 -0
- package/dist/cli/src/native/readiness.js +139 -0
- package/dist/cli/src/native/reassessments.js +206 -0
- package/dist/cli/src/native/redaction.js +235 -0
- package/dist/cli/src/native/registration.js +155 -0
- package/dist/cli/src/native/roots.js +110 -0
- package/dist/cli/src/native/run-cleanup.js +199 -0
- package/dist/cli/src/native/scratch.js +117 -0
- package/dist/cli/src/native/screenshots.js +60 -0
- package/dist/cli/src/native/session.js +556 -0
- package/dist/cli/src/native/standalone-proxy.js +580 -0
- package/dist/cli/src/native/standalone-renewals.js +183 -0
- package/dist/cli/src/native/standalone-state.js +193 -0
- package/dist/cli/src/native/standalone-tools.js +187 -0
- package/dist/cli/src/native/supervisor.js +831 -0
- package/dist/cli/src/native/target-auth.js +161 -0
- package/dist/cli/src/native/target-state.js +330 -0
- package/dist/cli/src/native/tool-reuse.js +364 -0
- package/dist/cli/src/native/tool-setups.js +336 -0
- package/dist/cli/src/native/world-reuse.js +288 -0
- package/dist/cli/src/onboarding.js +23 -0
- package/dist/cli/src/priority-requests.js +70 -0
- package/dist/cli/src/priority.js +223 -0
- package/dist/cli/src/prompts.js +184 -0
- package/dist/cli/src/real-operations.js +221 -0
- package/dist/cli/src/recipe-intents.js +58 -0
- package/dist/cli/src/recorded-context-intents.js +59 -0
- package/dist/cli/src/recorded-context.js +248 -0
- package/dist/cli/src/recordings.js +69 -0
- package/dist/cli/src/registration-guide.js +39 -0
- package/dist/cli/src/report-exports.js +213 -0
- package/dist/cli/src/request-input.js +47 -0
- package/dist/cli/src/resource-lifecycle.js +141 -0
- package/dist/cli/src/revision-requests.js +55 -0
- package/dist/cli/src/runner-abandon.js +53 -0
- package/dist/cli/src/runner-campaign-controls.js +262 -0
- package/dist/cli/src/runner-claim-reads.js +36 -0
- package/dist/cli/src/runner-completion.js +65 -0
- package/dist/cli/src/runner-configuration.js +546 -0
- package/dist/cli/src/runner-container.js +794 -0
- package/dist/cli/src/runner-evidence-erasure.js +351 -0
- package/dist/cli/src/runner-evidence-privacy.js +37 -0
- package/dist/cli/src/runner-evidence-retention.js +161 -0
- package/dist/cli/src/runner-evidence.js +326 -0
- package/dist/cli/src/runner-execute.js +92 -0
- package/dist/cli/src/runner-execution-deadline.js +25 -0
- package/dist/cli/src/runner-grants.js +307 -0
- package/dist/cli/src/runner-identity.js +615 -0
- package/dist/cli/src/runner-inputs.js +308 -0
- package/dist/cli/src/runner-jobs.js +49 -0
- package/dist/cli/src/runner-model-transport.js +392 -0
- package/dist/cli/src/runner-ownership.js +332 -0
- package/dist/cli/src/runner-process-lock.js +175 -0
- package/dist/cli/src/runner-runtime.js +396 -0
- package/dist/cli/src/runner-session-channel.js +377 -0
- package/dist/cli/src/runner-trial-completion.js +34 -0
- package/dist/cli/src/runner-trial-journal.js +358 -0
- package/dist/cli/src/runner-trial-live.js +393 -0
- package/dist/cli/src/runner-trial-protocol.js +289 -0
- package/dist/cli/src/runner-trial-sync.js +110 -0
- package/dist/cli/src/runner-trial-transfer.js +107 -0
- package/dist/cli/src/runner-voice.js +40 -0
- package/dist/cli/src/runner-world-claim.js +21 -0
- package/dist/cli/src/runner-world-collector.js +159 -0
- package/dist/cli/src/runner-world-command-evidence.js +487 -0
- package/dist/cli/src/runner-world-commands.js +268 -0
- package/dist/cli/src/runner-world-completion.js +76 -0
- package/dist/cli/src/runner-world-erasure.js +95 -0
- package/dist/cli/src/runner-world-evidence-sync.js +91 -0
- package/dist/cli/src/runner-world-journal.js +510 -0
- package/dist/cli/src/runner-world-native-filters.js +134 -0
- package/dist/cli/src/runner-world-observations.js +50 -0
- package/dist/cli/src/runner-world-session-protocol.js +134 -0
- package/dist/cli/src/saved-records.js +40 -0
- package/dist/cli/src/scenario-authoring.js +212 -0
- package/dist/cli/src/scenario-discovery.js +216 -0
- package/dist/cli/src/scenario-paths.js +68 -0
- package/dist/cli/src/scenario-requests.js +4 -0
- package/dist/cli/src/service-receipts.js +105 -0
- package/dist/cli/src/simple/authoring.js +270 -0
- package/dist/cli/src/simple/credit-approval.js +16 -0
- package/dist/cli/src/simple/help.js +98 -0
- package/dist/cli/src/simple/init.js +61 -0
- package/dist/cli/src/simple/library-sharing.js +414 -0
- package/dist/cli/src/simple/metadata-requests.js +46 -0
- package/dist/cli/src/simple/native-report.js +841 -0
- package/dist/cli/src/simple/native-tools.js +780 -0
- package/dist/cli/src/simple/plan-apply.js +506 -0
- package/dist/cli/src/simple/reassessment.js +323 -0
- package/dist/cli/src/simple/register.js +123 -0
- package/dist/cli/src/simple/run.js +122 -0
- package/dist/cli/src/simple/status.js +49 -0
- package/dist/cli/src/simple/tool-renewal.js +308 -0
- package/dist/cli/src/simple/tool-reuse.js +526 -0
- package/dist/cli/src/simple/tool-start-catalog.js +100 -0
- package/dist/cli/src/simple/tool-state.js +426 -0
- package/dist/cli/src/simple/tools-author.js +222 -0
- package/dist/cli/src/simple/watch.js +28 -0
- package/dist/cli/src/simple/world-reuse.js +586 -0
- package/dist/cli/src/simulation-intents.js +406 -0
- package/dist/cli/src/simulation-run.js +1058 -0
- package/dist/cli/src/source-batch.js +322 -0
- package/dist/cli/src/source-connection.js +451 -0
- package/dist/cli/src/source-runtime.js +346 -0
- package/dist/cli/src/source-selection-intents.js +75 -0
- package/dist/cli/src/source-sets.js +214 -0
- package/dist/cli/src/target-models.js +109 -0
- package/dist/cli/src/tool-catalog.js +224 -0
- package/dist/cli/src/tool-command-contracts.js +1 -0
- package/dist/cli/src/tool-command-export.js +245 -0
- package/dist/cli/src/tool-connection-contracts.js +1 -0
- package/dist/cli/src/tool-connection-reads.js +23 -0
- package/dist/cli/src/tool-contributions.js +58 -0
- package/dist/cli/src/tool-data.js +116 -0
- package/dist/cli/src/tool-datasets.js +176 -0
- package/dist/cli/src/tool-group-contracts.js +1 -0
- package/dist/cli/src/tool-group-reads.js +26 -0
- package/dist/cli/src/tool-group-state.js +147 -0
- package/dist/cli/src/tool-groups.js +311 -0
- package/dist/cli/src/tool-instances.js +186 -0
- package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
- package/dist/cli/src/tool-lifecycle-reads.js +30 -0
- package/dist/cli/src/tool-lifecycle.js +323 -0
- package/dist/cli/src/tool-package-contracts.js +1 -0
- package/dist/cli/src/tool-package-reads.js +41 -0
- package/dist/cli/src/tool-records.js +53 -0
- package/dist/cli/src/tool-runtime.js +96 -0
- package/dist/cli/src/tool-state-contracts.js +1 -0
- package/dist/cli/src/tool-state-reads.js +31 -0
- package/dist/cli/src/tools.js +306 -0
- package/dist/cli/src/trial-actions.js +93 -0
- package/dist/cli/src/trial-checkpoints.js +133 -0
- package/dist/cli/src/trial-checks.js +70 -0
- package/dist/cli/src/trial-state.js +197 -0
- package/dist/cli/src/vault.js +266 -0
- package/dist/cli/src/workspace-registration.js +383 -0
- package/dist/cli/src/workspace-team-contracts.js +1 -0
- package/dist/cli/src/world-access.js +231 -0
- package/dist/cli/src/world-agent.js +4 -0
- package/dist/cli/src/world-checkpoint-scopes.js +15 -0
- package/dist/cli/src/world-contracts.js +6 -0
- package/dist/cli/src/world-creation.js +201 -0
- package/dist/cli/src/world-datasets.js +207 -0
- package/dist/cli/src/world-inspection.js +257 -0
- package/dist/cli/src/world-recipes.js +103 -0
- package/dist/cli/src/world-requests.js +241 -0
- package/dist/cli/src/world-session-checkpoints.js +196 -0
- package/dist/cli/src/world-session-lifecycle.js +207 -0
- package/dist/cli/src/world-session-requests.js +215 -0
- package/dist/cli/src/world-session-state.js +145 -0
- package/dist/cli/src/world-session-voice.js +66 -0
- package/dist/cli/src/world-sessions.js +288 -0
- package/dist/cli/src/world-stream.js +99 -0
- package/dist/cli/src/world-time-report.js +58 -0
- package/dist/contracts/agent-change-contracts.js +75 -0
- package/dist/contracts/agent-execution-contracts.js +38 -0
- package/dist/contracts/application-state-contracts.js +26 -0
- package/dist/contracts/assessment-aggregate-contracts.js +15 -0
- package/dist/contracts/assessment-contracts.js +74 -0
- package/dist/contracts/billing-commerce-contracts.js +107 -0
- package/dist/contracts/billing-wallet-contracts.js +43 -0
- package/dist/contracts/campaign-lifecycle.js +1 -0
- package/dist/contracts/campaign-world-contracts.js +80 -0
- package/dist/contracts/catalog-contracts.js +73 -0
- package/dist/contracts/ci-access-contracts.js +68 -0
- package/dist/contracts/ci-configuration-contracts.js +88 -0
- package/dist/contracts/ci-coverage-contracts.js +65 -0
- package/dist/contracts/ci-coverage-labels.js +25 -0
- package/dist/contracts/ci-delivery-contracts.js +61 -0
- package/dist/contracts/ci-result-contracts.js +103 -0
- package/dist/contracts/ci-run-contracts.js +51 -0
- package/dist/contracts/ci-setup-contracts.js +88 -0
- package/dist/contracts/clip-contracts.js +32 -0
- package/dist/contracts/comparison-contracts.js +53 -0
- package/dist/contracts/composite-contracts.js +34 -0
- package/dist/contracts/context-contracts.js +76 -0
- package/dist/contracts/continuation-report-contracts.js +1 -0
- package/dist/contracts/continuity-requirements.js +8 -0
- package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
- package/dist/contracts/dataset-contracts.js +88 -0
- package/dist/contracts/dependency-decision-contracts.js +36 -0
- package/dist/contracts/dependency-protocol-contracts.js +43 -0
- package/dist/contracts/director-execution.js +51 -0
- package/dist/contracts/driver-review-contracts.js +99 -0
- package/dist/contracts/evidence-contracts.js +65 -0
- package/dist/contracts/evidence-coverage-contracts.js +10 -0
- package/dist/contracts/export-contracts.js +44 -0
- package/dist/contracts/github-directory-contracts.js +96 -0
- package/dist/contracts/insight-contracts.js +11 -0
- package/dist/contracts/inspection-contracts.js +1 -0
- package/dist/contracts/local-claim-contracts.js +39 -0
- package/dist/contracts/local-configuration-contracts.js +7 -0
- package/dist/contracts/local-evidence-contracts.js +32 -0
- package/dist/contracts/local-grant-contracts.js +83 -0
- package/dist/contracts/local-input-contracts.js +19 -0
- package/dist/contracts/local-runner-contracts.js +42 -0
- package/dist/contracts/local-trial-live-contracts.js +16 -0
- package/dist/contracts/local-value-source-contracts.js +92 -0
- package/dist/contracts/local-world-native-contracts.js +8 -0
- package/dist/contracts/mcp-contracts.js +42 -0
- package/dist/contracts/media-contracts.js +28 -0
- package/dist/contracts/native-check-contracts.js +31 -0
- package/dist/contracts/native-identity-contracts.js +52 -0
- package/dist/contracts/onboarding-contracts.js +32 -0
- package/dist/contracts/performance-contracts.js +85 -0
- package/dist/contracts/planning-contracts.js +81 -0
- package/dist/contracts/real-operation-contracts.js +30 -0
- package/dist/contracts/recommendation-contracts.js +54 -0
- package/dist/contracts/report-contracts.js +1 -0
- package/dist/contracts/resource-lifecycle-contracts.js +57 -0
- package/dist/contracts/revision-contracts.js +12 -0
- package/dist/contracts/revision-request-contracts.js +39 -0
- package/dist/contracts/saved-record-contracts.js +76 -0
- package/dist/contracts/scenario-draft-comparison.js +91 -0
- package/dist/contracts/scenario-world-contracts.js +14 -0
- package/dist/contracts/service-contracts.js +1 -0
- package/dist/contracts/session-command-contracts.js +56 -0
- package/dist/contracts/session-contracts.js +65 -0
- package/dist/contracts/session-state-contracts.js +67 -0
- package/dist/contracts/session-voice-contracts.js +45 -0
- package/dist/contracts/source-set-contracts.js +26 -0
- package/dist/contracts/source-upload-contracts.js +22 -0
- package/dist/contracts/tool-command-contracts.js +57 -0
- package/dist/contracts/tool-connection-contracts.js +87 -0
- package/dist/contracts/tool-contribution-contracts.js +35 -0
- package/dist/contracts/tool-group-contracts.js +53 -0
- package/dist/contracts/tool-group-state-contracts.js +63 -0
- package/dist/contracts/tool-lifecycle-contracts.js +94 -0
- package/dist/contracts/tool-package-contracts.js +91 -0
- package/dist/contracts/tool-protocol-contracts.js +71 -0
- package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
- package/dist/contracts/tool-setup-proposal.js +59 -0
- package/dist/contracts/tool-state-contracts.js +79 -0
- package/dist/contracts/trial-state-contracts.js +30 -0
- package/dist/contracts/typescript/agent-change.js +1 -0
- package/dist/contracts/typescript/assessment-aggregate.js +1 -0
- package/dist/contracts/typescript/assessment.js +1 -0
- package/dist/contracts/typescript/billing-commerce.js +1 -0
- package/dist/contracts/typescript/campaigns.js +1 -0
- package/dist/contracts/typescript/catalog.js +1 -0
- package/dist/contracts/typescript/ci-access.js +1 -0
- package/dist/contracts/typescript/ci-configuration.js +1 -0
- package/dist/contracts/typescript/ci-delivery.js +1 -0
- package/dist/contracts/typescript/ci-result.js +1 -0
- package/dist/contracts/typescript/ci-runs.js +1 -0
- package/dist/contracts/typescript/clips.js +1 -0
- package/dist/contracts/typescript/comparison.js +1 -0
- package/dist/contracts/typescript/composite.js +1 -0
- package/dist/contracts/typescript/connections.js +1 -0
- package/dist/contracts/typescript/context.js +1 -0
- package/dist/contracts/typescript/datasets.js +1 -0
- package/dist/contracts/typescript/evidence-coverage.js +1 -0
- package/dist/contracts/typescript/export.js +1 -0
- package/dist/contracts/typescript/github-directory.js +1 -0
- package/dist/contracts/typescript/insight.js +1 -0
- package/dist/contracts/typescript/inspection.js +1 -0
- package/dist/contracts/typescript/intake.js +1 -0
- package/dist/contracts/typescript/local-claim.js +1 -0
- package/dist/contracts/typescript/local-configuration.js +1 -0
- package/dist/contracts/typescript/local-evidence.js +1 -0
- package/dist/contracts/typescript/local-grant.js +1 -0
- package/dist/contracts/typescript/local-input.js +1 -0
- package/dist/contracts/typescript/local-runner.js +1 -0
- package/dist/contracts/typescript/local-trial-live.js +1 -0
- package/dist/contracts/typescript/local-value-source.js +1 -0
- package/dist/contracts/typescript/local-world-native.js +1 -0
- package/dist/contracts/typescript/mcp.js +1 -0
- package/dist/contracts/typescript/media.js +1 -0
- package/dist/contracts/typescript/native-check.js +1 -0
- package/dist/contracts/typescript/native-identity.js +1 -0
- package/dist/contracts/typescript/onboarding.js +1 -0
- package/dist/contracts/typescript/performance.js +1 -0
- package/dist/contracts/typescript/planning.js +1 -0
- package/dist/contracts/typescript/preparation.js +1 -0
- package/dist/contracts/typescript/real-operation.js +1 -0
- package/dist/contracts/typescript/recommendation.js +1 -0
- package/dist/contracts/typescript/reports.js +1 -0
- package/dist/contracts/typescript/resource-lifecycle.js +1 -0
- package/dist/contracts/typescript/saved-record.js +1 -0
- package/dist/contracts/typescript/scenarios.js +1 -0
- package/dist/contracts/typescript/services.js +1 -0
- package/dist/contracts/typescript/session-command.js +1 -0
- package/dist/contracts/typescript/session-state.js +1 -0
- package/dist/contracts/typescript/session-voice.js +1 -0
- package/dist/contracts/typescript/session.js +1 -0
- package/dist/contracts/typescript/tool-command.js +1 -0
- package/dist/contracts/typescript/tool-connection.js +1 -0
- package/dist/contracts/typescript/tool-contribution.js +1 -0
- package/dist/contracts/typescript/tool-groups.js +1 -0
- package/dist/contracts/typescript/tool-lifecycle.js +1 -0
- package/dist/contracts/typescript/tool-package.js +1 -0
- package/dist/contracts/typescript/tool-state.js +1 -0
- package/dist/contracts/typescript/trial-state.js +1 -0
- package/dist/contracts/typescript/vault.js +1 -0
- package/dist/contracts/typescript/workspace-team.js +1 -0
- package/dist/contracts/typescript/world-stream.js +1 -0
- package/dist/contracts/workspace-team-contracts.js +55 -0
- package/dist/contracts/world-command.js +65 -0
- package/dist/contracts/world-lifetime-contracts.js +74 -0
- package/dist/contracts/world-recipe-contracts.js +27 -0
- package/dist/contracts/world-stream-contracts.js +27 -0
- package/dist/package.json +1 -0
- package/package.json +16 -16
- package/LICENSE.md +0 -23
- package/PACKAGING.json +0 -50
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The companion's one shared, resumable wake channel (Plan20 §7.3).
|
|
3
|
+
*
|
|
4
|
+
* Root's `firedrill.native-companion-stream.v1` route
|
|
5
|
+
* (`GET /api/workspaces/{workspace}/native/devices/{device}/campaigns/{campaign}/stream`, the terminal bearer,
|
|
6
|
+
* no query) multiplexes invalidation hints for one campaign: its offers, admission and progress, and this
|
|
7
|
+
* device's assignments in it. Root keeps one open channel per runner device, so this client keeps exactly one
|
|
8
|
+
* connection for the selected campaign, shared by the session's offer loop and every drill:
|
|
9
|
+
* - consumers read root and act only while the channel is `live`, that is, once the current connection
|
|
10
|
+
* delivered a valid `snapshot` (first connection) or `resync` (every reconnection). Either one wakes every
|
|
11
|
+
* consumer for a canonical read; `changed` wakes only the consumers of the resources it names (an empty one
|
|
12
|
+
* only moves the cursor);
|
|
13
|
+
* - the cursor (`Last-Event-ID`) is an invalidation watermark, never an acknowledgment. Claims, controls,
|
|
14
|
+
* observations and closes stay signed requests whose own receipts are authoritative, and a wake-up never
|
|
15
|
+
* dispatches anything by itself;
|
|
16
|
+
* - root rotates a connection after 55 s (`retry_later`, or the end of the stream); it is reopened at once.
|
|
17
|
+
* Busy (409), unavailable (503), rate-limited, network, stalled and malformed connections retry with a
|
|
18
|
+
* capped backoff. Until the next valid resync nothing is woken, so nothing reads root on a timer;
|
|
19
|
+
* - `access_changed` and answered refusals (401, 403, 404, 422) put every consumer on hold (`refused`):
|
|
20
|
+
* no claim, step or start until a fresh authenticated connection delivers a valid resync, which root grants
|
|
21
|
+
* only with current session, device, membership and campaign access. Revocation itself is still decided by
|
|
22
|
+
* the device heartbeat;
|
|
23
|
+
* - a refused cursor (`native_stream_cursor_invalid`) is dropped and the channel starts from a snapshot.
|
|
24
|
+
* The bearer stays in memory and request headers (no cookie, redirect, query or log). Frames are bounded
|
|
25
|
+
* (16 KiB, 64 resources) and carry no input.
|
|
26
|
+
*/
|
|
27
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
28
|
+
import { access } from '../auth.js';
|
|
29
|
+
import { CliError, endpoint, responseError } from '../http.js';
|
|
30
|
+
export const COMPANION_STREAM_SCHEMA = 'firedrill.native-companion-stream.v1';
|
|
31
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
32
|
+
const SEQUENCE = /^(0|[1-9][0-9]{0,18})$/;
|
|
33
|
+
const MAX_SEQUENCE = 9223372036854775807n;
|
|
34
|
+
const PAYLOAD_KEYS = ['campaign_id', 'device_id', 'resources', 'schema', 'sequence', 'workspace_id'];
|
|
35
|
+
const MAX_RESOURCES = 64;
|
|
36
|
+
const MAX_FRAME_BYTES = 16_384;
|
|
37
|
+
const MAX_BUFFERED_CHARS = 65_536;
|
|
38
|
+
const MAX_REFUSAL_BYTES = 16_384;
|
|
39
|
+
/** Root writes a frame or a keepalive every 5 s; a connection silent this long is stalled. */
|
|
40
|
+
const STALL_MS = 20_000;
|
|
41
|
+
/** Root rotates a connection after 55 s; one still open after this is ended here. */
|
|
42
|
+
const CONNECTION_MS = 70_000;
|
|
43
|
+
/** A rotated connection is reopened after 250 ms to 1 s. */
|
|
44
|
+
const ROTATE_MS = 250;
|
|
45
|
+
const RETRY_BASE_MS = 1_000;
|
|
46
|
+
const RETRY_CAP_MS = 15_000;
|
|
47
|
+
/** Access is asked for again more slowly after an answered refusal. */
|
|
48
|
+
const REFUSED_BASE_MS = 5_000;
|
|
49
|
+
const REFUSED_CAP_MS = 60_000;
|
|
50
|
+
/** A generation counter with waiters: wake-ups coalesce, and one that lands between a read and its wait is never lost. */
|
|
51
|
+
export class Wakeup {
|
|
52
|
+
#generation = 0;
|
|
53
|
+
#closed = false;
|
|
54
|
+
#waiters = new Set();
|
|
55
|
+
#listeners = new Set();
|
|
56
|
+
get generation() { return this.#generation; }
|
|
57
|
+
get closed() { return this.#closed; }
|
|
58
|
+
wake() {
|
|
59
|
+
if (this.#closed)
|
|
60
|
+
return;
|
|
61
|
+
this.#generation++;
|
|
62
|
+
for (const waiter of [...this.#waiters])
|
|
63
|
+
waiter();
|
|
64
|
+
for (const listener of [...this.#listeners])
|
|
65
|
+
listener();
|
|
66
|
+
}
|
|
67
|
+
onWake(listener) {
|
|
68
|
+
if (this.#closed)
|
|
69
|
+
return () => undefined;
|
|
70
|
+
this.#listeners.add(listener);
|
|
71
|
+
return () => { this.#listeners.delete(listener); };
|
|
72
|
+
}
|
|
73
|
+
close() {
|
|
74
|
+
if (this.#closed)
|
|
75
|
+
return;
|
|
76
|
+
this.#closed = true;
|
|
77
|
+
this.#listeners.clear();
|
|
78
|
+
for (const waiter of [...this.#waiters])
|
|
79
|
+
waiter();
|
|
80
|
+
}
|
|
81
|
+
/** True once the generation passed `after`; false when `signal` aborts or this closed first. Never throws. */
|
|
82
|
+
wait(after, signal) {
|
|
83
|
+
return settled(() => this.#generation > after ? true : this.#closed ? false : null, this.#waiters, signal);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
class Subscription extends Wakeup {
|
|
87
|
+
resource;
|
|
88
|
+
#release;
|
|
89
|
+
constructor(resource, release) {
|
|
90
|
+
super();
|
|
91
|
+
this.resource = resource;
|
|
92
|
+
this.#release = release;
|
|
93
|
+
}
|
|
94
|
+
close() {
|
|
95
|
+
if (this.closed)
|
|
96
|
+
return;
|
|
97
|
+
super.close();
|
|
98
|
+
this.#release(this);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The selected campaign's companion channel. It connects with the first subscription and closes with the
|
|
103
|
+
* last one (or `close()`); subscriptions are the session's offer loop and each running drill.
|
|
104
|
+
*/
|
|
105
|
+
export function companionChannel(scope) {
|
|
106
|
+
const campaign = scope.campaign.toLowerCase();
|
|
107
|
+
const workspace = scope.workspace.toLowerCase();
|
|
108
|
+
if (!UUID.test(campaign))
|
|
109
|
+
throw new CliError('native_campaign_required', 'Select the campaign to run with --campaign <campaign-id>. Nothing was started.', 2);
|
|
110
|
+
const subscriptions = new Set();
|
|
111
|
+
const liveWaiters = new Set();
|
|
112
|
+
const stateListeners = new Set();
|
|
113
|
+
let connection = null;
|
|
114
|
+
let state = 'connecting';
|
|
115
|
+
let stateCode = null;
|
|
116
|
+
const setState = (next, code) => {
|
|
117
|
+
if (state === 'closed' || (state === next && stateCode === code))
|
|
118
|
+
return;
|
|
119
|
+
state = next;
|
|
120
|
+
stateCode = code;
|
|
121
|
+
for (const waiter of [...liveWaiters])
|
|
122
|
+
waiter();
|
|
123
|
+
for (const listener of [...stateListeners])
|
|
124
|
+
listener(next, code);
|
|
125
|
+
};
|
|
126
|
+
/** Wake the named resources' consumers, or every consumer (null). */
|
|
127
|
+
const wake = (resources) => {
|
|
128
|
+
for (const subscription of [...subscriptions]) {
|
|
129
|
+
if (resources === null || resources.some((item) => item.kind === subscription.resource.kind && item.id === subscription.resource.id)) {
|
|
130
|
+
subscription.wake();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const release = (subscription) => {
|
|
135
|
+
subscriptions.delete(subscription);
|
|
136
|
+
if (subscriptions.size === 0 && connection) {
|
|
137
|
+
connection.abort();
|
|
138
|
+
connection = null;
|
|
139
|
+
setState('connecting', null);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
/** One connection, until it rotates, fails or is refused. Never reports `rotated` without a delivered snapshot or resync. */
|
|
143
|
+
const connect = async (cursor, signal, deliver) => {
|
|
144
|
+
const device = scope.device().toLowerCase();
|
|
145
|
+
const token = await access(async (value) => value);
|
|
146
|
+
signal.throwIfAborted();
|
|
147
|
+
const stalled = new AbortController();
|
|
148
|
+
let watchdog = setTimeout(() => stalled.abort(), STALL_MS);
|
|
149
|
+
let reader;
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch(endpoint(`/api/workspaces/${scope.workspace}/native/devices/${device}/campaigns/${campaign}/stream`), {
|
|
152
|
+
method: 'GET', redirect: 'error', credentials: 'omit',
|
|
153
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'text/event-stream', 'X-Firedrill-Workspace': scope.workspace,
|
|
154
|
+
...(scope.actor ? { 'X-Firedrill-Actor': scope.actor } : {}), ...(cursor !== null ? { 'Last-Event-ID': cursor } : {}) },
|
|
155
|
+
signal: AbortSignal.any([signal, stalled.signal, AbortSignal.timeout(CONNECTION_MS)]),
|
|
156
|
+
});
|
|
157
|
+
if (!response.ok)
|
|
158
|
+
throw await refusal(response);
|
|
159
|
+
if (!response.headers.get('Content-Type')?.toLowerCase().startsWith('text/event-stream') || !response.body) {
|
|
160
|
+
await response.body?.cancel().catch(() => undefined);
|
|
161
|
+
throw invalid();
|
|
162
|
+
}
|
|
163
|
+
reader = response.body.getReader();
|
|
164
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
165
|
+
let buffer = '';
|
|
166
|
+
let delivered = false;
|
|
167
|
+
for (;;) {
|
|
168
|
+
const part = await reader.read();
|
|
169
|
+
signal.throwIfAborted();
|
|
170
|
+
if (part.done)
|
|
171
|
+
return delivered ? 'rotated' : 'failed';
|
|
172
|
+
clearTimeout(watchdog);
|
|
173
|
+
watchdog = setTimeout(() => stalled.abort(), STALL_MS);
|
|
174
|
+
buffer = (buffer + decoder.decode(part.value, { stream: true })).replace(/\r\n/g, '\n');
|
|
175
|
+
let boundary;
|
|
176
|
+
while ((boundary = buffer.indexOf('\n\n')) !== -1) {
|
|
177
|
+
const frame = buffer.slice(0, boundary);
|
|
178
|
+
buffer = buffer.slice(boundary + 2);
|
|
179
|
+
if (Buffer.byteLength(frame, 'utf8') > MAX_FRAME_BYTES)
|
|
180
|
+
throw invalid();
|
|
181
|
+
const { event, id, data } = fields(frame);
|
|
182
|
+
if (event === 'access_changed')
|
|
183
|
+
return 'refused';
|
|
184
|
+
if (event === 'retry_later')
|
|
185
|
+
return delivered ? 'rotated' : 'failed';
|
|
186
|
+
// Keepalive comments and unknown events carry no authority and wake nothing.
|
|
187
|
+
if (event !== 'snapshot' && event !== 'resync' && event !== 'changed')
|
|
188
|
+
continue;
|
|
189
|
+
// Root opens every connection with one snapshot (no cursor sent) or resync (cursor sent), which requires a canonical read.
|
|
190
|
+
if ((event === 'changed') === !delivered || (event !== 'changed' && (event === 'snapshot') !== (cursor === null)))
|
|
191
|
+
throw invalid();
|
|
192
|
+
deliver(event, notification(data, id, { workspace, device, campaign }));
|
|
193
|
+
delivered = true;
|
|
194
|
+
}
|
|
195
|
+
if (buffer.length > MAX_BUFFERED_CHARS)
|
|
196
|
+
throw invalid();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
clearTimeout(watchdog);
|
|
201
|
+
await reader?.cancel().catch(() => undefined);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
/** Keep one connection open while anyone is subscribed. Never throws. */
|
|
205
|
+
const maintain = async (signal) => {
|
|
206
|
+
let cursor = null;
|
|
207
|
+
let failures = 0;
|
|
208
|
+
while (!signal.aborted) {
|
|
209
|
+
let ending;
|
|
210
|
+
let code = null;
|
|
211
|
+
try {
|
|
212
|
+
ending = await connect(cursor, signal, (event, value) => {
|
|
213
|
+
// A buffered response from a replaced connection has no authority.
|
|
214
|
+
signal.throwIfAborted();
|
|
215
|
+
if (connection?.signal !== signal)
|
|
216
|
+
throw invalid();
|
|
217
|
+
cursor = value.sequence;
|
|
218
|
+
failures = 0;
|
|
219
|
+
if (event === 'changed') {
|
|
220
|
+
wake(value.resources);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// A valid snapshot or resync: consumers may read and act again, and every one of them reads now.
|
|
224
|
+
setState('live', null);
|
|
225
|
+
wake(null);
|
|
226
|
+
});
|
|
227
|
+
if (ending === 'refused')
|
|
228
|
+
code = 'native_stream_access_changed';
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (signal.aborted)
|
|
232
|
+
return;
|
|
233
|
+
code = error instanceof CliError ? error.code : 'native_stream_disconnected';
|
|
234
|
+
if (code === 'native_stream_cursor_invalid')
|
|
235
|
+
cursor = null;
|
|
236
|
+
ending = refused(error) ? 'refused' : 'failed';
|
|
237
|
+
}
|
|
238
|
+
if (signal.aborted)
|
|
239
|
+
return;
|
|
240
|
+
if (ending === 'rotated') {
|
|
241
|
+
setState('reconnecting', null);
|
|
242
|
+
await pause(ROTATE_MS + Math.floor(Math.random() * ROTATE_MS * 3), signal);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// Hold every consumer until a later connection delivers a valid resync; nothing is woken meanwhile.
|
|
246
|
+
if (ending === 'refused')
|
|
247
|
+
cursor = null;
|
|
248
|
+
setState(ending === 'refused' ? 'refused' : 'reconnecting', code);
|
|
249
|
+
await pause(ending === 'refused' ? backoff(failures, REFUSED_BASE_MS, REFUSED_CAP_MS) : backoff(failures, RETRY_BASE_MS, RETRY_CAP_MS), signal);
|
|
250
|
+
failures++;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
campaign_id: campaign,
|
|
255
|
+
get state() { return state; },
|
|
256
|
+
live(signal) {
|
|
257
|
+
return settled(() => state === 'live' ? true : state === 'closed' ? false : null, liveWaiters, signal);
|
|
258
|
+
},
|
|
259
|
+
onState(listener) {
|
|
260
|
+
stateListeners.add(listener);
|
|
261
|
+
return () => { stateListeners.delete(listener); };
|
|
262
|
+
},
|
|
263
|
+
subscribe(resource) {
|
|
264
|
+
const id = typeof resource?.id === 'string' ? resource.id.toLowerCase() : '';
|
|
265
|
+
if ((resource?.kind !== 'campaign' && resource?.kind !== 'assignment') || !UUID.test(id) || (resource.kind === 'campaign' && id !== campaign)) {
|
|
266
|
+
throw new CliError('native_stream_subscription_invalid', 'The wake-up subscription names no assignment of the selected campaign. Nothing was opened.');
|
|
267
|
+
}
|
|
268
|
+
const subscription = new Subscription({ kind: resource.kind, id }, release);
|
|
269
|
+
if (state === 'closed') {
|
|
270
|
+
subscription.close();
|
|
271
|
+
return subscription;
|
|
272
|
+
}
|
|
273
|
+
subscriptions.add(subscription);
|
|
274
|
+
if (!connection) {
|
|
275
|
+
const controller = new AbortController();
|
|
276
|
+
connection = controller;
|
|
277
|
+
void maintain(controller.signal).finally(() => { if (connection === controller)
|
|
278
|
+
connection = null; });
|
|
279
|
+
}
|
|
280
|
+
return subscription;
|
|
281
|
+
},
|
|
282
|
+
close() {
|
|
283
|
+
connection?.abort();
|
|
284
|
+
connection = null;
|
|
285
|
+
for (const subscription of [...subscriptions])
|
|
286
|
+
subscription.close();
|
|
287
|
+
setState('closed', null);
|
|
288
|
+
stateListeners.clear();
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Resolve with `outcome()` once it is not null, or false when `signal` aborts first. `waiters` holds the
|
|
294
|
+
* re-check until the owner calls it on a change. Never throws.
|
|
295
|
+
*/
|
|
296
|
+
function settled(outcome, waiters, signal) {
|
|
297
|
+
return new Promise((resolve) => {
|
|
298
|
+
const check = () => {
|
|
299
|
+
const value = signal.aborted ? false : outcome();
|
|
300
|
+
if (value === null)
|
|
301
|
+
return;
|
|
302
|
+
waiters.delete(check);
|
|
303
|
+
signal.removeEventListener('abort', check);
|
|
304
|
+
resolve(value);
|
|
305
|
+
};
|
|
306
|
+
const first = signal.aborted ? false : outcome();
|
|
307
|
+
if (first !== null) {
|
|
308
|
+
resolve(first);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
waiters.add(check);
|
|
312
|
+
signal.addEventListener('abort', check, { once: true });
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/** SSE fields of one frame; comment lines (keepalives) are skipped. */
|
|
316
|
+
function fields(frame) {
|
|
317
|
+
let event = '';
|
|
318
|
+
let id = null;
|
|
319
|
+
const data = [];
|
|
320
|
+
for (const line of frame.split('\n')) {
|
|
321
|
+
if (!line || line.startsWith(':'))
|
|
322
|
+
continue;
|
|
323
|
+
const colon = line.indexOf(':');
|
|
324
|
+
const name = colon === -1 ? line : line.slice(0, colon);
|
|
325
|
+
let value = colon === -1 ? '' : line.slice(colon + 1);
|
|
326
|
+
if (value.startsWith(' '))
|
|
327
|
+
value = value.slice(1);
|
|
328
|
+
if (name === 'event')
|
|
329
|
+
event = value;
|
|
330
|
+
else if (name === 'id')
|
|
331
|
+
id = value;
|
|
332
|
+
else if (name === 'data')
|
|
333
|
+
data.push(value);
|
|
334
|
+
}
|
|
335
|
+
return { event, id, data: data.join('\n') };
|
|
336
|
+
}
|
|
337
|
+
/** One `snapshot`, `resync` or `changed` payload (root's companion-stream.v1 schema), confirmed to be this device's channel for this campaign. */
|
|
338
|
+
function notification(data, id, expected) {
|
|
339
|
+
let value;
|
|
340
|
+
try {
|
|
341
|
+
value = JSON.parse(data);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
throw invalid();
|
|
345
|
+
}
|
|
346
|
+
const body = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
347
|
+
if (!body || Object.keys(body).sort().join() !== PAYLOAD_KEYS.join() || body.schema !== COMPANION_STREAM_SCHEMA
|
|
348
|
+
|| lower(body.workspace_id) !== expected.workspace || lower(body.device_id) !== expected.device
|
|
349
|
+
|| lower(body.campaign_id) !== expected.campaign || !position(body.sequence) || (id !== null && id !== body.sequence)
|
|
350
|
+
|| !Array.isArray(body.resources) || body.resources.length > MAX_RESOURCES)
|
|
351
|
+
throw invalid();
|
|
352
|
+
const seen = new Set();
|
|
353
|
+
const resources = body.resources.map((item) => {
|
|
354
|
+
const row = item !== null && typeof item === 'object' && !Array.isArray(item) ? item : null;
|
|
355
|
+
const resourceId = lower(row?.id);
|
|
356
|
+
if (!row || Object.keys(row).sort().join() !== 'id,kind' || (row.kind !== 'campaign' && row.kind !== 'assignment')
|
|
357
|
+
|| resourceId === null || !UUID.test(resourceId) || (row.kind === 'campaign' && resourceId !== expected.campaign)
|
|
358
|
+
|| seen.has(`${row.kind}:${resourceId}`))
|
|
359
|
+
throw invalid();
|
|
360
|
+
seen.add(`${row.kind}:${resourceId}`);
|
|
361
|
+
return { kind: row.kind, id: resourceId };
|
|
362
|
+
});
|
|
363
|
+
return { sequence: body.sequence, resources };
|
|
364
|
+
}
|
|
365
|
+
function lower(value) {
|
|
366
|
+
return typeof value === 'string' ? value.toLowerCase() : null;
|
|
367
|
+
}
|
|
368
|
+
function position(value) {
|
|
369
|
+
return typeof value === 'string' && SEQUENCE.test(value) && BigInt(value) <= MAX_SEQUENCE;
|
|
370
|
+
}
|
|
371
|
+
function invalid() {
|
|
372
|
+
return new CliError('native_stream_invalid', 'The companion channel sent a frame that could not be confirmed. Consumers wait for a valid resync; nothing was dispatched.');
|
|
373
|
+
}
|
|
374
|
+
/** Answered refusals of this device's access; busy (409), unavailable (503), rate limits and network failures are retried. */
|
|
375
|
+
function refused(error) {
|
|
376
|
+
if (!(error instanceof CliError))
|
|
377
|
+
return false;
|
|
378
|
+
if (error.code === 'native_stream_access_changed')
|
|
379
|
+
return true;
|
|
380
|
+
const status = error.httpStatus;
|
|
381
|
+
return status !== null && status >= 400 && status < 500 && ![408, 409, 425, 429].includes(status);
|
|
382
|
+
}
|
|
383
|
+
/** Root's coded refusal, read from a bounded body. */
|
|
384
|
+
async function refusal(response) {
|
|
385
|
+
let body = null;
|
|
386
|
+
const reader = response.body?.getReader();
|
|
387
|
+
if (reader) {
|
|
388
|
+
const chunks = [];
|
|
389
|
+
let size = 0;
|
|
390
|
+
try {
|
|
391
|
+
for (;;) {
|
|
392
|
+
const part = await reader.read();
|
|
393
|
+
if (part.done)
|
|
394
|
+
break;
|
|
395
|
+
size += part.value.byteLength;
|
|
396
|
+
if (size > MAX_REFUSAL_BYTES) {
|
|
397
|
+
chunks.length = 0;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
chunks.push(part.value);
|
|
401
|
+
}
|
|
402
|
+
if (chunks.length)
|
|
403
|
+
body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
body = null;
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
await reader.cancel().catch(() => undefined);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return responseError(body, response.status);
|
|
413
|
+
}
|
|
414
|
+
/** Capped exponential backoff with jitter (half to all of the step). */
|
|
415
|
+
function backoff(attempt, base, cap) {
|
|
416
|
+
const ceiling = Math.min(cap, base * 2 ** Math.min(attempt, 16));
|
|
417
|
+
return Math.floor(ceiling / 2 + Math.random() * (ceiling / 2));
|
|
418
|
+
}
|
|
419
|
+
async function pause(ms, signal) {
|
|
420
|
+
await delay(ms, undefined, { signal }).catch(() => undefined);
|
|
421
|
+
}
|