@superbee/core 0.2.0-pre.2 → 0.2.0-pre.21
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 +1000 -0
- package/dist/artifact-contract.d.ts +90 -0
- package/dist/artifact-contract.d.ts.map +1 -0
- package/dist/artifact-contract.js +205 -0
- package/dist/artifact-contract.js.map +1 -0
- package/dist/backend.d.ts +19 -1
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +83 -36
- package/dist/backend.js.map +1 -1
- package/dist/blob-contract.d.ts +80 -0
- package/dist/blob-contract.d.ts.map +1 -0
- package/dist/blob-contract.js +131 -0
- package/dist/blob-contract.js.map +1 -0
- package/dist/blob-journal.d.ts +302 -0
- package/dist/blob-journal.d.ts.map +1 -0
- package/dist/blob-journal.js +540 -0
- package/dist/blob-journal.js.map +1 -0
- package/dist/body-replace-guards.d.ts +50 -0
- package/dist/body-replace-guards.d.ts.map +1 -0
- package/dist/body-replace-guards.js +158 -0
- package/dist/body-replace-guards.js.map +1 -0
- package/dist/builtin-recipes/content.d.ts +93 -0
- package/dist/builtin-recipes/content.d.ts.map +1 -0
- package/dist/builtin-recipes/content.js +288 -0
- package/dist/builtin-recipes/content.js.map +1 -0
- package/dist/builtin-recipes.d.ts +10 -0
- package/dist/builtin-recipes.d.ts.map +1 -0
- package/dist/builtin-recipes.js +97 -0
- package/dist/builtin-recipes.js.map +1 -0
- package/dist/bundle-health/index.d.ts +14 -0
- package/dist/bundle-health/index.d.ts.map +1 -0
- package/dist/bundle-health/index.js +12 -0
- package/dist/bundle-health/index.js.map +1 -0
- package/dist/bundle-health/kind-findings.d.ts +44 -0
- package/dist/bundle-health/kind-findings.d.ts.map +1 -0
- package/dist/bundle-health/kind-findings.js +68 -0
- package/dist/bundle-health/kind-findings.js.map +1 -0
- package/dist/bundle-health/legacy-page.d.ts +39 -0
- package/dist/bundle-health/legacy-page.d.ts.map +1 -0
- package/dist/bundle-health/legacy-page.js +50 -0
- package/dist/bundle-health/legacy-page.js.map +1 -0
- package/dist/bundle-health/report.d.ts +202 -0
- package/dist/bundle-health/report.d.ts.map +1 -0
- package/dist/bundle-health/report.js +438 -0
- package/dist/bundle-health/report.js.map +1 -0
- package/dist/bundle-ops.d.ts +1 -1
- package/dist/bundle-ops.d.ts.map +1 -1
- package/dist/bundle-ops.js +2 -2
- package/dist/bundle-ops.js.map +1 -1
- package/dist/bundle.d.ts +2 -1
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +3 -3
- package/dist/bundle.js.map +1 -1
- package/dist/code-point-order.d.ts +8 -0
- package/dist/code-point-order.d.ts.map +1 -0
- package/dist/code-point-order.js +20 -0
- package/dist/code-point-order.js.map +1 -0
- package/dist/connectors.d.ts +230 -0
- package/dist/connectors.d.ts.map +1 -0
- package/dist/connectors.js +1150 -0
- package/dist/connectors.js.map +1 -0
- package/dist/cross-bundle.d.ts +135 -0
- package/dist/cross-bundle.d.ts.map +1 -0
- package/dist/cross-bundle.js +186 -0
- package/dist/cross-bundle.js.map +1 -0
- package/dist/dependencies.d.ts +94 -0
- package/dist/dependencies.d.ts.map +1 -0
- package/dist/dependencies.js +180 -0
- package/dist/dependencies.js.map +1 -0
- package/dist/document-codec.d.ts +6 -1
- package/dist/document-codec.d.ts.map +1 -1
- package/dist/document-codec.js +8 -4
- package/dist/document-codec.js.map +1 -1
- package/dist/document-field-actions.d.ts +31 -1
- package/dist/document-field-actions.d.ts.map +1 -1
- package/dist/document-field-actions.js +137 -34
- package/dist/document-field-actions.js.map +1 -1
- package/dist/document-mutation.d.ts +22 -10
- package/dist/document-mutation.d.ts.map +1 -1
- package/dist/document-mutation.js +58 -27
- package/dist/document-mutation.js.map +1 -1
- package/dist/document-write-policy.d.ts.map +1 -1
- package/dist/document-write-policy.js +4 -2
- package/dist/document-write-policy.js.map +1 -1
- package/dist/engine.d.ts +9 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +11 -4
- package/dist/engine.js.map +1 -1
- package/dist/file-journaled-backend-internal.d.ts +21 -0
- package/dist/file-journaled-backend-internal.d.ts.map +1 -0
- package/dist/file-journaled-backend-internal.js +12 -0
- package/dist/file-journaled-backend-internal.js.map +1 -0
- package/dist/file-journaled-backend.d.ts +200 -0
- package/dist/file-journaled-backend.d.ts.map +1 -0
- package/dist/file-journaled-backend.js +1334 -0
- package/dist/file-journaled-backend.js.map +1 -0
- package/dist/filesystem-host.d.ts +15 -0
- package/dist/filesystem-host.d.ts.map +1 -0
- package/dist/filesystem-host.js +20 -0
- package/dist/filesystem-host.js.map +1 -0
- package/dist/filesystem-identity.d.ts +86 -4
- package/dist/filesystem-identity.d.ts.map +1 -1
- package/dist/filesystem-identity.js +338 -102
- package/dist/filesystem-identity.js.map +1 -1
- package/dist/filesystem-lock.d.ts +16 -3
- package/dist/filesystem-lock.d.ts.map +1 -1
- package/dist/filesystem-lock.js +323 -77
- package/dist/filesystem-lock.js.map +1 -1
- package/dist/filesystem-push-role.d.ts +77 -0
- package/dist/filesystem-push-role.d.ts.map +1 -0
- package/dist/filesystem-push-role.js +165 -0
- package/dist/filesystem-push-role.js.map +1 -0
- package/dist/filesystem.d.ts +17 -0
- package/dist/filesystem.d.ts.map +1 -0
- package/dist/filesystem.js +18 -0
- package/dist/filesystem.js.map +1 -0
- package/dist/frontmatter.d.ts +8 -0
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +14 -4
- package/dist/frontmatter.js.map +1 -1
- package/dist/governed-body-write.d.ts +106 -0
- package/dist/governed-body-write.d.ts.map +1 -0
- package/dist/governed-body-write.js +251 -0
- package/dist/governed-body-write.js.map +1 -0
- package/dist/host-policy-snapshot.d.ts +9 -0
- package/dist/host-policy-snapshot.d.ts.map +1 -0
- package/dist/host-policy-snapshot.js +33 -0
- package/dist/host-policy-snapshot.js.map +1 -0
- package/dist/host-text.d.ts +31 -0
- package/dist/host-text.d.ts.map +1 -0
- package/dist/host-text.js +77 -0
- package/dist/host-text.js.map +1 -0
- package/dist/hosted-client/binding.d.ts +7 -0
- package/dist/hosted-client/binding.d.ts.map +1 -0
- package/dist/hosted-client/binding.js +10 -0
- package/dist/hosted-client/binding.js.map +1 -0
- package/dist/hosted-client/blob-delivery.d.ts +67 -0
- package/dist/hosted-client/blob-delivery.d.ts.map +1 -0
- package/dist/hosted-client/blob-delivery.js +169 -0
- package/dist/hosted-client/blob-delivery.js.map +1 -0
- package/dist/hosted-client/blob-write.d.ts +386 -0
- package/dist/hosted-client/blob-write.d.ts.map +1 -0
- package/dist/hosted-client/blob-write.js +770 -0
- package/dist/hosted-client/blob-write.js.map +1 -0
- package/dist/hosted-client/bundle-listing.d.ts +46 -0
- package/dist/hosted-client/bundle-listing.d.ts.map +1 -0
- package/dist/hosted-client/bundle-listing.js +36 -0
- package/dist/hosted-client/bundle-listing.js.map +1 -0
- package/dist/hosted-client/client.d.ts +219 -0
- package/dist/hosted-client/client.d.ts.map +1 -0
- package/dist/hosted-client/client.js +309 -0
- package/dist/hosted-client/client.js.map +1 -0
- package/dist/hosted-client/directory.d.ts +46 -0
- package/dist/hosted-client/directory.d.ts.map +1 -0
- package/dist/hosted-client/directory.js +74 -0
- package/dist/hosted-client/directory.js.map +1 -0
- package/dist/hosted-client/errors.d.ts +26 -0
- package/dist/hosted-client/errors.d.ts.map +1 -0
- package/dist/hosted-client/errors.js +35 -0
- package/dist/hosted-client/errors.js.map +1 -0
- package/dist/hosted-client/inbox.d.ts +320 -0
- package/dist/hosted-client/inbox.d.ts.map +1 -0
- package/dist/hosted-client/inbox.js +379 -0
- package/dist/hosted-client/inbox.js.map +1 -0
- package/dist/hosted-client/index.d.ts +30 -0
- package/dist/hosted-client/index.d.ts.map +1 -0
- package/dist/hosted-client/index.js +32 -0
- package/dist/hosted-client/index.js.map +1 -0
- package/dist/hosted-client/journal.d.ts +103 -0
- package/dist/hosted-client/journal.d.ts.map +1 -0
- package/dist/hosted-client/journal.js +464 -0
- package/dist/hosted-client/journal.js.map +1 -0
- package/dist/hosted-client/node.d.ts +74 -0
- package/dist/hosted-client/node.d.ts.map +1 -0
- package/dist/hosted-client/node.js +219 -0
- package/dist/hosted-client/node.js.map +1 -0
- package/dist/hosted-client/reference.d.ts +12 -0
- package/dist/hosted-client/reference.d.ts.map +1 -0
- package/dist/hosted-client/reference.js +29 -0
- package/dist/hosted-client/reference.js.map +1 -0
- package/dist/hosted-client/write.d.ts +217 -0
- package/dist/hosted-client/write.d.ts.map +1 -0
- package/dist/hosted-client/write.js +418 -0
- package/dist/hosted-client/write.js.map +1 -0
- package/dist/hosted-transport/answer-rows.d.ts +243 -0
- package/dist/hosted-transport/answer-rows.d.ts.map +1 -0
- package/dist/hosted-transport/answer-rows.js +454 -0
- package/dist/hosted-transport/answer-rows.js.map +1 -0
- package/dist/hosted-transport/blob-reads.d.ts +35 -0
- package/dist/hosted-transport/blob-reads.d.ts.map +1 -0
- package/dist/hosted-transport/blob-reads.js +179 -0
- package/dist/hosted-transport/blob-reads.js.map +1 -0
- package/dist/hosted-transport/blob-write.d.ts +34 -0
- package/dist/hosted-transport/blob-write.d.ts.map +1 -0
- package/dist/hosted-transport/blob-write.js +115 -0
- package/dist/hosted-transport/blob-write.js.map +1 -0
- package/dist/hosted-transport/carrier.d.ts +151 -0
- package/dist/hosted-transport/carrier.d.ts.map +1 -0
- package/dist/hosted-transport/carrier.js +260 -0
- package/dist/hosted-transport/carrier.js.map +1 -0
- package/dist/hosted-transport/history-pages.d.ts +55 -0
- package/dist/hosted-transport/history-pages.d.ts.map +1 -0
- package/dist/hosted-transport/history-pages.js +69 -0
- package/dist/hosted-transport/history-pages.js.map +1 -0
- package/dist/hosted-transport/index.d.ts +18 -0
- package/dist/hosted-transport/index.d.ts.map +1 -0
- package/dist/hosted-transport/index.js +18 -0
- package/dist/hosted-transport/index.js.map +1 -0
- package/dist/hosted-transport/operations.d.ts +85 -0
- package/dist/hosted-transport/operations.d.ts.map +1 -0
- package/dist/hosted-transport/operations.js +128 -0
- package/dist/hosted-transport/operations.js.map +1 -0
- package/dist/hosted-transport/paged-reads.d.ts +87 -0
- package/dist/hosted-transport/paged-reads.d.ts.map +1 -0
- package/dist/hosted-transport/paged-reads.js +238 -0
- package/dist/hosted-transport/paged-reads.js.map +1 -0
- package/dist/hosted-transport/read-adapter.d.ts +256 -0
- package/dist/hosted-transport/read-adapter.d.ts.map +1 -0
- package/dist/hosted-transport/read-adapter.js +568 -0
- package/dist/hosted-transport/read-adapter.js.map +1 -0
- package/dist/hosted-transport/root-write.d.ts +114 -0
- package/dist/hosted-transport/root-write.d.ts.map +1 -0
- package/dist/hosted-transport/root-write.js +171 -0
- package/dist/hosted-transport/root-write.js.map +1 -0
- package/dist/hosted-transport/whole-document-transport.d.ts +204 -0
- package/dist/hosted-transport/whole-document-transport.d.ts.map +1 -0
- package/dist/hosted-transport/whole-document-transport.js +447 -0
- package/dist/hosted-transport/whole-document-transport.js.map +1 -0
- package/dist/index.d.ts +34 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -8
- package/dist/index.js.map +1 -1
- package/dist/indexeddb-backend.d.ts +28 -6
- package/dist/indexeddb-backend.d.ts.map +1 -1
- package/dist/indexeddb-backend.js +471 -92
- package/dist/indexeddb-backend.js.map +1 -1
- package/dist/journaled-backend.d.ts +247 -10
- package/dist/journaled-backend.d.ts.map +1 -1
- package/dist/journaled-backend.js +302 -1
- package/dist/journaled-backend.js.map +1 -1
- package/dist/kind-authoring.d.ts +170 -0
- package/dist/kind-authoring.d.ts.map +1 -0
- package/dist/kind-authoring.js +451 -0
- package/dist/kind-authoring.js.map +1 -0
- package/dist/kind-conformance-error.d.ts +15 -0
- package/dist/kind-conformance-error.d.ts.map +1 -0
- package/dist/kind-conformance-error.js +20 -0
- package/dist/kind-conformance-error.js.map +1 -0
- package/dist/kind-coverage.d.ts +99 -0
- package/dist/kind-coverage.d.ts.map +1 -0
- package/dist/kind-coverage.js +186 -0
- package/dist/kind-coverage.js.map +1 -0
- package/dist/kind-field-mutation.d.ts +4 -0
- package/dist/kind-field-mutation.d.ts.map +1 -1
- package/dist/kind-field-mutation.js +28 -3
- package/dist/kind-field-mutation.js.map +1 -1
- package/dist/kinds-load.js +4 -4
- package/dist/kinds-load.js.map +1 -1
- package/dist/kinds.d.ts +65 -2
- package/dist/kinds.d.ts.map +1 -1
- package/dist/kinds.js +341 -21
- package/dist/kinds.js.map +1 -1
- package/dist/link-types.d.ts +26 -0
- package/dist/link-types.d.ts.map +1 -0
- package/dist/link-types.js +57 -0
- package/dist/link-types.js.map +1 -0
- package/dist/links.d.ts +21 -3
- package/dist/links.d.ts.map +1 -1
- package/dist/links.js +213 -14
- package/dist/links.js.map +1 -1
- package/dist/managed-document-metadata.d.ts +2 -0
- package/dist/managed-document-metadata.d.ts.map +1 -0
- package/dist/managed-document-metadata.js +9 -0
- package/dist/managed-document-metadata.js.map +1 -0
- package/dist/memory-backend.d.ts +2 -2
- package/dist/memory-backend.d.ts.map +1 -1
- package/dist/memory-backend.js +26 -11
- package/dist/memory-backend.js.map +1 -1
- package/dist/paths.d.ts +16 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +25 -0
- package/dist/paths.js.map +1 -1
- package/dist/platform.d.ts +13 -2
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +16 -0
- package/dist/platform.js.map +1 -1
- package/dist/portable-export.d.ts +119 -0
- package/dist/portable-export.d.ts.map +1 -0
- package/dist/portable-export.js +501 -0
- package/dist/portable-export.js.map +1 -0
- package/dist/publication-filesystem.d.ts +1 -1
- package/dist/publication-filesystem.d.ts.map +1 -1
- package/dist/publication-filesystem.js +1 -1
- package/dist/publication-filesystem.js.map +1 -1
- package/dist/qualified-links.d.ts +67 -0
- package/dist/qualified-links.d.ts.map +1 -0
- package/dist/qualified-links.js +117 -0
- package/dist/qualified-links.js.map +1 -0
- package/dist/query-filter.d.ts +16 -2
- package/dist/query-filter.d.ts.map +1 -1
- package/dist/query-filter.js +40 -1
- package/dist/query-filter.js.map +1 -1
- package/dist/query-order.d.ts +22 -0
- package/dist/query-order.d.ts.map +1 -0
- package/dist/query-order.js +42 -0
- package/dist/query-order.js.map +1 -0
- package/dist/recipe-lifecycle/errors.d.ts +96 -0
- package/dist/recipe-lifecycle/errors.d.ts.map +1 -0
- package/dist/recipe-lifecycle/errors.js +122 -0
- package/dist/recipe-lifecycle/errors.js.map +1 -0
- package/dist/recipe-lifecycle/evolution.d.ts +108 -0
- package/dist/recipe-lifecycle/evolution.d.ts.map +1 -0
- package/dist/recipe-lifecycle/evolution.js +884 -0
- package/dist/recipe-lifecycle/evolution.js.map +1 -0
- package/dist/recipe-lifecycle/index.d.ts +26 -0
- package/dist/recipe-lifecycle/index.d.ts.map +1 -0
- package/dist/recipe-lifecycle/index.js +22 -0
- package/dist/recipe-lifecycle/index.js.map +1 -0
- package/dist/recipe-lifecycle/install.d.ts +127 -0
- package/dist/recipe-lifecycle/install.d.ts.map +1 -0
- package/dist/recipe-lifecycle/install.js +526 -0
- package/dist/recipe-lifecycle/install.js.map +1 -0
- package/dist/recipe-lifecycle/legacy-aliases.d.ts +16 -0
- package/dist/recipe-lifecycle/legacy-aliases.d.ts.map +1 -0
- package/dist/recipe-lifecycle/legacy-aliases.js +51 -0
- package/dist/recipe-lifecycle/legacy-aliases.js.map +1 -0
- package/dist/recipe-lifecycle/stamp.d.ts +136 -0
- package/dist/recipe-lifecycle/stamp.d.ts.map +1 -0
- package/dist/recipe-lifecycle/stamp.js +326 -0
- package/dist/recipe-lifecycle/stamp.js.map +1 -0
- package/dist/recipes.d.ts +29 -1
- package/dist/recipes.d.ts.map +1 -1
- package/dist/recipes.js +137 -1
- package/dist/recipes.js.map +1 -1
- package/dist/record-overlay/apply.d.ts +43 -0
- package/dist/record-overlay/apply.d.ts.map +1 -0
- package/dist/record-overlay/apply.js +527 -0
- package/dist/record-overlay/apply.js.map +1 -0
- package/dist/record-overlay/canonical.d.ts +22 -0
- package/dist/record-overlay/canonical.d.ts.map +1 -0
- package/dist/record-overlay/canonical.js +47 -0
- package/dist/record-overlay/canonical.js.map +1 -0
- package/dist/record-overlay/digest.d.ts +4 -0
- package/dist/record-overlay/digest.d.ts.map +1 -0
- package/dist/record-overlay/digest.js +11 -0
- package/dist/record-overlay/digest.js.map +1 -0
- package/dist/record-overlay/errors.d.ts +44 -0
- package/dist/record-overlay/errors.d.ts.map +1 -0
- package/dist/record-overlay/errors.js +55 -0
- package/dist/record-overlay/errors.js.map +1 -0
- package/dist/record-overlay/findings.d.ts +5 -0
- package/dist/record-overlay/findings.d.ts.map +1 -0
- package/dist/record-overlay/findings.js +83 -0
- package/dist/record-overlay/findings.js.map +1 -0
- package/dist/record-overlay/index.d.ts +23 -0
- package/dist/record-overlay/index.d.ts.map +1 -0
- package/dist/record-overlay/index.js +21 -0
- package/dist/record-overlay/index.js.map +1 -0
- package/dist/record-overlay/plan.d.ts +12 -0
- package/dist/record-overlay/plan.d.ts.map +1 -0
- package/dist/record-overlay/plan.js +285 -0
- package/dist/record-overlay/plan.js.map +1 -0
- package/dist/record-overlay/policy.d.ts +69 -0
- package/dist/record-overlay/policy.d.ts.map +1 -0
- package/dist/record-overlay/policy.js +228 -0
- package/dist/record-overlay/policy.js.map +1 -0
- package/dist/record-overlay/redact.d.ts +3 -0
- package/dist/record-overlay/redact.d.ts.map +1 -0
- package/dist/record-overlay/redact.js +27 -0
- package/dist/record-overlay/redact.js.map +1 -0
- package/dist/record-overlay/signal-kind.d.ts +30 -0
- package/dist/record-overlay/signal-kind.d.ts.map +1 -0
- package/dist/record-overlay/signal-kind.js +38 -0
- package/dist/record-overlay/signal-kind.js.map +1 -0
- package/dist/record-overlay/signal-text.d.ts +45 -0
- package/dist/record-overlay/signal-text.d.ts.map +1 -0
- package/dist/record-overlay/signal-text.js +116 -0
- package/dist/record-overlay/signal-text.js.map +1 -0
- package/dist/record-overlay/source-index.d.ts +19 -0
- package/dist/record-overlay/source-index.d.ts.map +1 -0
- package/dist/record-overlay/source-index.js +102 -0
- package/dist/record-overlay/source-index.js.map +1 -0
- package/dist/record-overlay/types.d.ts +291 -0
- package/dist/record-overlay/types.d.ts.map +1 -0
- package/dist/record-overlay/types.js +2 -0
- package/dist/record-overlay/types.js.map +1 -0
- package/dist/remote-backend.d.ts +67 -67
- package/dist/remote-backend.d.ts.map +1 -1
- package/dist/remote-backend.js +169 -201
- package/dist/remote-backend.js.map +1 -1
- package/dist/remote-document-codec.d.ts +15 -0
- package/dist/remote-document-codec.d.ts.map +1 -0
- package/dist/remote-document-codec.js +103 -0
- package/dist/remote-document-codec.js.map +1 -0
- package/dist/remote-error.d.ts +38 -0
- package/dist/remote-error.d.ts.map +1 -0
- package/dist/remote-error.js +59 -0
- package/dist/remote-error.js.map +1 -0
- package/dist/remote-operations.d.ts +6 -3
- package/dist/remote-operations.d.ts.map +1 -1
- package/dist/remote-operations.js +23 -5
- package/dist/remote-operations.js.map +1 -1
- package/dist/remote-parsers.d.ts +86 -0
- package/dist/remote-parsers.d.ts.map +1 -0
- package/dist/remote-parsers.js +181 -0
- package/dist/remote-parsers.js.map +1 -0
- package/dist/remote.d.ts +5 -2
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +5 -1
- package/dist/remote.js.map +1 -1
- package/dist/search/backend-source.d.ts +6 -0
- package/dist/search/backend-source.d.ts.map +1 -0
- package/dist/search/backend-source.js +33 -0
- package/dist/search/backend-source.js.map +1 -0
- package/dist/search/contract.d.ts +151 -0
- package/dist/search/contract.d.ts.map +1 -0
- package/dist/search/contract.js +78 -0
- package/dist/search/contract.js.map +1 -0
- package/dist/search/errors.d.ts +30 -0
- package/dist/search/errors.d.ts.map +1 -0
- package/dist/search/errors.js +50 -0
- package/dist/search/errors.js.map +1 -0
- package/dist/search/identity.d.ts +12 -0
- package/dist/search/identity.d.ts.map +1 -0
- package/dist/search/identity.js +90 -0
- package/dist/search/identity.js.map +1 -0
- package/dist/search/index.d.ts +16 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +15 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/memory-source.d.ts +36 -0
- package/dist/search/memory-source.d.ts.map +1 -0
- package/dist/search/memory-source.js +235 -0
- package/dist/search/memory-source.js.map +1 -0
- package/dist/search/rank.d.ts +34 -0
- package/dist/search/rank.d.ts.map +1 -0
- package/dist/search/rank.js +129 -0
- package/dist/search/rank.js.map +1 -0
- package/dist/search/row-text.d.ts +38 -0
- package/dist/search/row-text.d.ts.map +1 -0
- package/dist/search/row-text.js +88 -0
- package/dist/search/row-text.js.map +1 -0
- package/dist/search/search.d.ts +28 -0
- package/dist/search/search.d.ts.map +1 -0
- package/dist/search/search.js +257 -0
- package/dist/search/search.js.map +1 -0
- package/dist/search/source.d.ts +67 -0
- package/dist/search/source.d.ts.map +1 -0
- package/dist/search/source.js +2 -0
- package/dist/search/source.js.map +1 -0
- package/dist/search/text.d.ts +47 -0
- package/dist/search/text.d.ts.map +1 -0
- package/dist/search/text.js +148 -0
- package/dist/search/text.js.map +1 -0
- package/dist/site-capabilities.d.ts +339 -0
- package/dist/site-capabilities.d.ts.map +1 -0
- package/dist/site-capabilities.js +717 -0
- package/dist/site-capabilities.js.map +1 -0
- package/dist/source-engine/guard.d.ts +82 -0
- package/dist/source-engine/guard.d.ts.map +1 -0
- package/dist/source-engine/guard.js +396 -0
- package/dist/source-engine/guard.js.map +1 -0
- package/dist/source-engine/index.d.ts +24 -0
- package/dist/source-engine/index.d.ts.map +1 -0
- package/dist/source-engine/index.js +20 -0
- package/dist/source-engine/index.js.map +1 -0
- package/dist/source-engine/machine-state.d.ts +74 -0
- package/dist/source-engine/machine-state.d.ts.map +1 -0
- package/dist/source-engine/machine-state.js +48 -0
- package/dist/source-engine/machine-state.js.map +1 -0
- package/dist/source-engine/observe.d.ts +18 -0
- package/dist/source-engine/observe.d.ts.map +1 -0
- package/dist/source-engine/observe.js +598 -0
- package/dist/source-engine/observe.js.map +1 -0
- package/dist/source-engine/robots.d.ts +53 -0
- package/dist/source-engine/robots.d.ts.map +1 -0
- package/dist/source-engine/robots.js +158 -0
- package/dist/source-engine/robots.js.map +1 -0
- package/dist/source-engine/types.d.ts +285 -0
- package/dist/source-engine/types.d.ts.map +1 -0
- package/dist/source-engine/types.js +2 -0
- package/dist/source-engine/types.js.map +1 -0
- package/dist/source-harness/conformance.d.ts +16 -0
- package/dist/source-harness/conformance.d.ts.map +1 -0
- package/dist/source-harness/conformance.js +265 -0
- package/dist/source-harness/conformance.js.map +1 -0
- package/dist/source-harness/fixture.d.ts +95 -0
- package/dist/source-harness/fixture.d.ts.map +1 -0
- package/dist/source-harness/fixture.js +261 -0
- package/dist/source-harness/fixture.js.map +1 -0
- package/dist/source-harness/index.d.ts +22 -0
- package/dist/source-harness/index.d.ts.map +1 -0
- package/dist/source-harness/index.js +19 -0
- package/dist/source-harness/index.js.map +1 -0
- package/dist/source-harness/peer.d.ts +74 -0
- package/dist/source-harness/peer.d.ts.map +1 -0
- package/dist/source-harness/peer.js +106 -0
- package/dist/source-harness/peer.js.map +1 -0
- package/dist/storage.d.ts +6 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +4 -1
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +33 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/uncertain-write.d.ts +156 -19
- package/dist/uncertain-write.d.ts.map +1 -1
- package/dist/uncertain-write.js +90 -31
- package/dist/uncertain-write.js.map +1 -1
- package/dist/versioning.d.ts +9 -4
- package/dist/versioning.d.ts.map +1 -1
- package/dist/versioning.js +9 -4
- package/dist/versioning.js.map +1 -1
- package/dist/write-identity.d.ts +5 -0
- package/dist/write-identity.d.ts.map +1 -0
- package/dist/write-identity.js +9 -0
- package/dist/write-identity.js.map +1 -0
- package/package.json +94 -2
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
import { BlobAdmissionConflict, blobIntentResendable, blobNonExecutionPatch, checkBlobAdmission, decodePreparedBlobIntent, prepareBlobIntent, } from "../blob-journal.js";
|
|
2
|
+
import { isHostedBundleId } from "../dependencies.js";
|
|
3
|
+
import { captureJournalValue, equalJournalValue, JournalGuardConflict } from "../journaled-backend.js";
|
|
4
|
+
import { RemoteError } from "../remote-error.js";
|
|
5
|
+
import { sha256HexOfBytes } from "../sha256.js";
|
|
6
|
+
import { mintRequestId } from "../uncertain-write.js";
|
|
7
|
+
import { isContentVersion } from "../version-transport.js";
|
|
8
|
+
import { isAgentLabelVia, isWriteBinding, isWriteRequestId } from "../write-identity.js";
|
|
9
|
+
import { createHostedBlobReadAdapter, createHostedBlobWriteAdapter, HostedCarrierError, HostedReadAdapterError, isAcceptedDeletionCount, } from "../hosted-transport/index.js";
|
|
10
|
+
import { performHostedBlobDelivery } from "./blob-delivery.js";
|
|
11
|
+
import { HostedBlobWritesNotServed, HostedRouteNotServed } from "./errors.js";
|
|
12
|
+
import { dropWriteSide, untilAborted, writeSide, writeSignal } from "./write.js";
|
|
13
|
+
/**
|
|
14
|
+
* A held deletion's continuation named fewer deletions than the host holds. Nothing was claimed
|
|
15
|
+
* or sent; the record stays held.
|
|
16
|
+
*/
|
|
17
|
+
export class HostedBlobDeletionsNotAccepted extends Error {
|
|
18
|
+
code = "DELETIONS_NOT_ACCEPTED";
|
|
19
|
+
requestId;
|
|
20
|
+
deletions;
|
|
21
|
+
baseline;
|
|
22
|
+
accepted;
|
|
23
|
+
constructor(requestId, hold, accepted) {
|
|
24
|
+
super(`the host holds ${hold.deletions} deletions of ${hold.baseline} files; acceptDeletes must be at least ${hold.deletions}. Nothing was sent.`);
|
|
25
|
+
this.name = "HostedBlobDeletionsNotAccepted";
|
|
26
|
+
this.requestId = requestId;
|
|
27
|
+
this.deletions = hold.deletions;
|
|
28
|
+
this.baseline = hold.baseline;
|
|
29
|
+
this.accepted = accepted;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** How long an aborted send waits for its carrier call to settle before it counts as unknown. */
|
|
33
|
+
const CARRIER_SETTLE_MS = 1000;
|
|
34
|
+
/** `work` settled, or `ms` passed: whichever is first, never rejecting. */
|
|
35
|
+
async function settledWithin(work, ms) {
|
|
36
|
+
let timer;
|
|
37
|
+
await Promise.race([work, new Promise((resolve) => { timer = setTimeout(resolve, ms); })]);
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
/** How many times a transaction reads its key again after a guard conflict, before refusing. */
|
|
41
|
+
const GUARD_ATTEMPTS = 8;
|
|
42
|
+
function mutationOptions(snapshot) {
|
|
43
|
+
return {
|
|
44
|
+
guard: snapshot.guard,
|
|
45
|
+
...(snapshot.meta === undefined ? {} : { meta: [...snapshot.meta] }),
|
|
46
|
+
...(snapshot.removeMeta === undefined ? {} : { removeMeta: [...snapshot.removeMeta] }),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function once(load) {
|
|
50
|
+
let answer;
|
|
51
|
+
return () => (answer ??= Promise.resolve().then(load));
|
|
52
|
+
}
|
|
53
|
+
function admissionCheck(prepared, bytes, capabilities) {
|
|
54
|
+
const { intent } = prepared;
|
|
55
|
+
const operation = intent.operationId === "blobs.create.v1" ? "create" : intent.operationId === "blobs.replace.v1" ? "replace" : "delete";
|
|
56
|
+
return checkBlobAdmission({
|
|
57
|
+
operation,
|
|
58
|
+
key: intent.key,
|
|
59
|
+
...(intent.operationId === "blobs.delete.v1" ? {} : { contentType: intent.contentType }),
|
|
60
|
+
...(operation === "create" ? {} : { expectedContentType: intent.expectedContentType }),
|
|
61
|
+
...(bytes === undefined ? {} : { byteLength: bytes }),
|
|
62
|
+
maximumBytes: capabilities.blobWriteBytes ?? null,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** The raw length of a prepared intent's bytes, from its canonical base64. */
|
|
66
|
+
function preparedByteLength(prepared) {
|
|
67
|
+
const text = prepared.bytesBase64;
|
|
68
|
+
if (text === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
return (text.length / 4) * 3 - (text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0);
|
|
71
|
+
}
|
|
72
|
+
/** A held receipt as the outcome its delivery selected. */
|
|
73
|
+
function outcomeOfReceipt(receipt) {
|
|
74
|
+
if (receipt.result.ok)
|
|
75
|
+
return { kind: "committed", receipt: receipt };
|
|
76
|
+
const failed = receipt;
|
|
77
|
+
if (failed.result.error.code === "version_conflict")
|
|
78
|
+
return { kind: "conflict", receipt: failed };
|
|
79
|
+
return { kind: "refused", code: failed.result.error.code, message: failed.result.error.message, receipt: failed };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Drive one journaled blob intent: admit it (`admit`), make the fresh durable claim, deliver it
|
|
83
|
+
* through `performHostedBlobDelivery`, persist the patch and retire a terminal record.
|
|
84
|
+
*
|
|
85
|
+
* - Every check that could fail inside the adapter's send (the capability, the advertised byte
|
|
86
|
+
* bound, the key and MIME grammar) runs before admission and again before a claim, against the
|
|
87
|
+
* one capabilities answer the adapter is handed.
|
|
88
|
+
* - A claim proven never dispatched is rolled back to `pending` (typed `not_dispatched`), so it
|
|
89
|
+
* stays resendable: nothing reached the carrier (credentials, an aborted signal, a malformed
|
|
90
|
+
* identity, a local precondition), or the carrier refused it `denied`, which by its contract
|
|
91
|
+
* means nothing left. Anything after the carrier was invoked is `unknown`, lookup-only.
|
|
92
|
+
* - A failure to admit or to claim is thrown with nothing sent. A failure to record the outcome
|
|
93
|
+
* is not thrown: the record stays for a later lookup and `persisted` is false.
|
|
94
|
+
*/
|
|
95
|
+
export function driveJournaledBlob(journal, context, requestId, mode) {
|
|
96
|
+
return drive(journal, context, requestId, mode, undefined);
|
|
97
|
+
}
|
|
98
|
+
/** The owner; `answered` replays one lookup answer already obtained (a forget's), so it is not asked twice. */
|
|
99
|
+
async function drive(journal, context, requestId, mode, answered) {
|
|
100
|
+
const scope = captureJournalValue({ ...context.scope });
|
|
101
|
+
const { carrier, premises } = context;
|
|
102
|
+
const capabilities = once(context.capabilities);
|
|
103
|
+
const delivery = { ...(context.delivery ?? {}) };
|
|
104
|
+
const signal = context.signal;
|
|
105
|
+
const copied = { ...mode };
|
|
106
|
+
if (typeof requestId !== "string" || !isWriteRequestId(requestId))
|
|
107
|
+
throw new TypeError("a blob drive names a request identity");
|
|
108
|
+
if (!["admit", "settle", "lookup"].includes(copied.start))
|
|
109
|
+
throw new TypeError("unknown blob drive start");
|
|
110
|
+
if (copied.acceptDeletes !== undefined && (copied.start === "lookup" || !isAcceptedDeletionCount(copied.acceptDeletes)))
|
|
111
|
+
throw new TypeError("acceptDeletes is a deletion count from 1 to 100,000, for a submission");
|
|
112
|
+
async function guarded(key, step, run) {
|
|
113
|
+
for (let attempt = 1;; attempt += 1) {
|
|
114
|
+
const snapshot = premises ? await premises.snapshot(key, step) : { guard: (await journal.readBlobWithJournal(key)).guard };
|
|
115
|
+
try {
|
|
116
|
+
return await run(mutationOptions(snapshot), snapshot);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// The guard covers every intent on the key, of any bundle: a write of another bundle or
|
|
120
|
+
// another client sharing the journal moves it without touching this record. Read again.
|
|
121
|
+
if (!(error instanceof JournalGuardConflict) || error instanceof BlobAdmissionConflict || attempt >= GUARD_ATTEMPTS)
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function retire(record) {
|
|
127
|
+
try {
|
|
128
|
+
await guarded(record.target, { kind: "retire", record }, (options) => journal.retireBlobIntents(record.target, [record.requestId], options));
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// The receipt is durable; a later lookup, settle or heldBlobWrites sweeps it.
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Persist a delivery's patch (unless it changes nothing), then retire a terminal record. */
|
|
137
|
+
async function persist(row, result, submitted) {
|
|
138
|
+
const { patch } = result;
|
|
139
|
+
const unchanged = patch.receipt === undefined && patch.notAppliedThrough === undefined && patch.state === row.state && patch.attempts === row.attempts;
|
|
140
|
+
let saved = row;
|
|
141
|
+
let persisted = true;
|
|
142
|
+
if (!unchanged) {
|
|
143
|
+
try {
|
|
144
|
+
saved = await guarded(row.target, { kind: "persist", record: row, patch }, (options) => journal.updateBlobIntent(row.requestId, row.state, patch, options));
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
persisted = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const retired = persisted && saved.receipt !== undefined ? await retire(saved) : false;
|
|
151
|
+
return { kind: "delivered", record: saved, outcome: result.outcome, submitted, lookups: result.lookups, persisted, retired };
|
|
152
|
+
}
|
|
153
|
+
async function rollBack(claimed, code, cause) {
|
|
154
|
+
const patch = blobNonExecutionPatch(claimed, { kind: "not_dispatched", code });
|
|
155
|
+
try {
|
|
156
|
+
const saved = await guarded(claimed.target, { kind: "persist", record: claimed, patch }, (options) => journal.updateBlobIntent(claimed.requestId, "in_flight", patch, options));
|
|
157
|
+
return { kind: "not-dispatched", record: saved, code, ...(cause === undefined ? {} : { cause }), rolledBack: true };
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return { kind: "not-dispatched", record: claimed, code, ...(cause === undefined ? {} : { cause }), rolledBack: false };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Deliver `row` through an adapter that records whether its one send reached the carrier, and
|
|
165
|
+
* how the carrier answered, before the send returned.
|
|
166
|
+
*/
|
|
167
|
+
async function deliver(row, start, acceptDeletes) {
|
|
168
|
+
const prepared = decodePreparedBlobIntent(row.prepared);
|
|
169
|
+
const halt = new AbortController();
|
|
170
|
+
const userAborted = () => signal?.aborted === true;
|
|
171
|
+
const probe = {
|
|
172
|
+
sending: false,
|
|
173
|
+
calls: 0,
|
|
174
|
+
denied: undefined,
|
|
175
|
+
/** The send's one carrier call, settled either way (never rejecting). */
|
|
176
|
+
pending: undefined,
|
|
177
|
+
/** The send wrapper's own completion: a cancelled delivery returns before it records its evidence. */
|
|
178
|
+
finished: undefined,
|
|
179
|
+
settled: undefined,
|
|
180
|
+
/** The send's own lifetime, which the delivery's deadline as well as the caller can end. */
|
|
181
|
+
lifetime: undefined,
|
|
182
|
+
};
|
|
183
|
+
const identity = prepared.identity;
|
|
184
|
+
const identityValid = isWriteRequestId(identity.requestId) && isWriteBinding(identity.binding) && (identity.via === undefined || isAgentLabelVia(identity.via));
|
|
185
|
+
/** Why a send that never reached the host did not: `undefined` when it may have. */
|
|
186
|
+
const notDispatched = () => {
|
|
187
|
+
const seen = probe.settled ?? { calls: probe.calls, denied: undefined, ended: false };
|
|
188
|
+
if (seen.calls === 0)
|
|
189
|
+
return userAborted() || seen.ended ? "aborted" : identityValid ? "precondition_failed" : "invalid_identity";
|
|
190
|
+
// The carrier contract: `denied` means nothing left (no credential, or refused before sending).
|
|
191
|
+
if (seen.calls === 1 && seen.denied) {
|
|
192
|
+
if (userAborted() || seen.ended)
|
|
193
|
+
return "aborted";
|
|
194
|
+
if (!identityValid)
|
|
195
|
+
return "invalid_identity";
|
|
196
|
+
return seen.denied.cause !== undefined ? "credentials_unavailable" : "credentials_denied";
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
};
|
|
200
|
+
const watch = (call) => {
|
|
201
|
+
const counted = probe.sending;
|
|
202
|
+
if (counted)
|
|
203
|
+
probe.calls += 1;
|
|
204
|
+
const answer = Promise.resolve().then(call);
|
|
205
|
+
if (counted) {
|
|
206
|
+
probe.pending = answer.then(() => undefined, (error) => {
|
|
207
|
+
if (probe.sending && error instanceof HostedCarrierError && error.code === "denied")
|
|
208
|
+
probe.denied ??= error;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return answer;
|
|
212
|
+
};
|
|
213
|
+
const watched = {
|
|
214
|
+
json: (route, input, s, options) => watch(() => carrier.json(route, input, s, options)),
|
|
215
|
+
stream: (route, input, s) => watch(() => carrier.stream(route, input, s)),
|
|
216
|
+
...(carrier.bytes ? { bytes: (route, body, s, options) => watch(() => carrier.bytes(route, body, s, options)) } : {}),
|
|
217
|
+
};
|
|
218
|
+
const adapterFor = (lifetime) => {
|
|
219
|
+
probe.lifetime = lifetime;
|
|
220
|
+
const inner = createHostedBlobWriteAdapter({ carrier: watched, bundleId: scope.bundleId, prefix: scope.prefix, signal: lifetime, capabilities });
|
|
221
|
+
return {
|
|
222
|
+
async send(intent, id, bytes) {
|
|
223
|
+
probe.sending = true;
|
|
224
|
+
let finish;
|
|
225
|
+
probe.finished = new Promise((resolve) => { finish = resolve; });
|
|
226
|
+
try {
|
|
227
|
+
return await inner.send(intent, id, bytes);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
// An abort ends the send before the carrier answers; the carrier sees the same abort
|
|
231
|
+
// and settles at once, and its own answer (denied: nothing left) is the evidence.
|
|
232
|
+
if ((userAborted() || probe.lifetime?.aborted) && probe.calls === 1 && probe.pending)
|
|
233
|
+
await settledWithin(probe.pending, CARRIER_SETTLE_MS);
|
|
234
|
+
probe.sending = false;
|
|
235
|
+
probe.settled = { calls: probe.calls, denied: probe.denied, ended: probe.lifetime?.aborted === true };
|
|
236
|
+
// Proven not sent: stop here, with no lookup of an identity the host never saw.
|
|
237
|
+
if (notDispatched() !== undefined)
|
|
238
|
+
halt.abort();
|
|
239
|
+
finish();
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
lookup: (intent, id) => (answered === undefined ? inner.lookup(intent, id) : Promise.resolve(answered)),
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
let result;
|
|
246
|
+
try {
|
|
247
|
+
result = await performHostedBlobDelivery({ scope, adapterFor }, row, {
|
|
248
|
+
start,
|
|
249
|
+
signal: signal ? AbortSignal.any([signal, halt.signal]) : halt.signal,
|
|
250
|
+
...(acceptDeletes === undefined ? {} : { acceptDeletes }),
|
|
251
|
+
...(delivery.deadlineMs === undefined ? {} : { deadlineMs: delivery.deadlineMs }),
|
|
252
|
+
...(delivery.maxLookups === undefined ? {} : { maxLookups: delivery.maxLookups }),
|
|
253
|
+
...(delivery.lookupDelayMs === undefined ? {} : { lookupDelayMs: delivery.lookupDelayMs }),
|
|
254
|
+
...(delivery.sleep === undefined ? {} : { sleep: delivery.sleep }),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
await probe.finished;
|
|
259
|
+
if (start === "submit-claimed" && probe.calls === 0)
|
|
260
|
+
return rollBack(row, notDispatched() ?? "precondition_failed", error);
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
// The send ends promptly once its lifetime is over (the adapter races it); wait for its evidence.
|
|
264
|
+
await probe.finished;
|
|
265
|
+
if (start === "submit-claimed" && result.outcome.kind === "unknown") {
|
|
266
|
+
const code = notDispatched();
|
|
267
|
+
if (code !== undefined)
|
|
268
|
+
return rollBack(row, code, probe.settled?.denied);
|
|
269
|
+
}
|
|
270
|
+
return persist(row, result, start === "submit-claimed");
|
|
271
|
+
}
|
|
272
|
+
async function claimAndSubmit(record, acceptDeletes) {
|
|
273
|
+
const claimed = await guarded(record.target, { kind: "claim", record }, (options) => journal.updateBlobIntent(record.requestId, record.state, { state: "in_flight", attempts: record.attempts + 1 }, options));
|
|
274
|
+
const deletion = decodePreparedBlobIntent(claimed.prepared).intent.operationId === "blobs.delete.v1";
|
|
275
|
+
return deliver(claimed, "submit-claimed", deletion ? acceptDeletes : undefined);
|
|
276
|
+
}
|
|
277
|
+
if (copied.start === "admit") {
|
|
278
|
+
if (typeof copied.prepared !== "string")
|
|
279
|
+
throw new TypeError("an admitted blob drive carries its prepared intent");
|
|
280
|
+
const decoded = decodePreparedBlobIntent(copied.prepared);
|
|
281
|
+
if (decoded.identity.requestId !== requestId)
|
|
282
|
+
throw new TypeError("the prepared intent names another request identity");
|
|
283
|
+
if (!equalJournalValue(decoded.scope, scope))
|
|
284
|
+
throw new TypeError("the prepared intent's target does not match the trusted scope");
|
|
285
|
+
if (copied.acceptDeletes !== undefined && decoded.intent.operationId !== "blobs.delete.v1")
|
|
286
|
+
throw new TypeError("acceptDeletes belongs only to a delete");
|
|
287
|
+
// Nothing is journaled until the one answer the adapter will use admits the input.
|
|
288
|
+
const answer = await capabilities();
|
|
289
|
+
if (answer.blobWrites !== "allowed")
|
|
290
|
+
return { kind: "not-served" };
|
|
291
|
+
const check = admissionCheck(decoded, preparedByteLength(decoded), answer);
|
|
292
|
+
if (!check.ok)
|
|
293
|
+
return { kind: "not-admitted", reason: "precondition", check };
|
|
294
|
+
// An abort before admission admits nothing; one after it is a claim rolled back, still held.
|
|
295
|
+
signal?.throwIfAborted();
|
|
296
|
+
let admitted;
|
|
297
|
+
try {
|
|
298
|
+
admitted = await guarded(decoded.intent.key, { kind: "admit", prepared: copied.prepared }, (options, snapshot) => journal.admitPreparedBlob(copied.prepared, { ...options, ...(snapshot.unsettled === undefined ? {} : { unsettled: [...snapshot.unsettled] }) }));
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (error instanceof BlobAdmissionConflict)
|
|
302
|
+
return { kind: "not-admitted", reason: "unsettled", unsettled: error.unsettled };
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
return claimAndSubmit(admitted, copied.acceptDeletes);
|
|
306
|
+
}
|
|
307
|
+
const record = await journal.readBlobIntent(requestId);
|
|
308
|
+
if (!record)
|
|
309
|
+
return { kind: "unrecorded" };
|
|
310
|
+
const prepared = decodePreparedBlobIntent(record.prepared);
|
|
311
|
+
if (!equalJournalValue(prepared.scope, scope))
|
|
312
|
+
return { kind: "scope-mismatch", record };
|
|
313
|
+
if (record.receipt !== undefined) {
|
|
314
|
+
// Settled before a crash kept it from retiring: sweep it now.
|
|
315
|
+
return { kind: "delivered", record, outcome: outcomeOfReceipt(record.receipt), submitted: false, lookups: 0, persisted: true, retired: await retire(record) };
|
|
316
|
+
}
|
|
317
|
+
if (blobIntentResendable(record)) {
|
|
318
|
+
if (copied.start === "lookup")
|
|
319
|
+
return record.deletionHold ? { kind: "held", record, hold: record.deletionHold } : { kind: "unsent", record };
|
|
320
|
+
if (record.deletionHold) {
|
|
321
|
+
if (copied.acceptDeletes === undefined)
|
|
322
|
+
return { kind: "held", record, hold: record.deletionHold };
|
|
323
|
+
if (copied.acceptDeletes < record.deletionHold.deletions)
|
|
324
|
+
throw new HostedBlobDeletionsNotAccepted(requestId, record.deletionHold, copied.acceptDeletes);
|
|
325
|
+
}
|
|
326
|
+
const answer = await capabilities();
|
|
327
|
+
if (answer.blobWrites !== "allowed")
|
|
328
|
+
return { kind: "not-served", record };
|
|
329
|
+
const check = admissionCheck(prepared, preparedByteLength(prepared), answer);
|
|
330
|
+
if (!check.ok)
|
|
331
|
+
return { kind: "not-sendable", record, check };
|
|
332
|
+
return claimAndSubmit(record, copied.acceptDeletes);
|
|
333
|
+
}
|
|
334
|
+
// Once an attempt may have left, the identity is only ever looked up: absence is never a resend.
|
|
335
|
+
return deliver(record, "lookup", undefined);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Forget an `unknown` record after one fresh lookup answers `absent`. The record is removed, not
|
|
339
|
+
* settled: the earlier delivery may still commit, and only the host's compare-and-swap keeps a
|
|
340
|
+
* later write from losing that update. A lookup that finds the outcome settles it instead.
|
|
341
|
+
*/
|
|
342
|
+
export async function forgetJournaledBlob(journal, context, requestId) {
|
|
343
|
+
const scope = captureJournalValue({ ...context.scope });
|
|
344
|
+
const { carrier, premises } = context;
|
|
345
|
+
if (typeof journal.forgetBlobIntent !== "function")
|
|
346
|
+
throw new TypeError("this blob journal cannot forget an intent");
|
|
347
|
+
const record = typeof requestId === "string" ? await journal.readBlobIntent(requestId) : undefined;
|
|
348
|
+
if (!record)
|
|
349
|
+
return { kind: "unrecorded" };
|
|
350
|
+
const prepared = decodePreparedBlobIntent(record.prepared);
|
|
351
|
+
if (!equalJournalValue(prepared.scope, scope))
|
|
352
|
+
return { kind: "scope-mismatch", record };
|
|
353
|
+
if (record.state !== "unknown" || record.receipt !== undefined || blobIntentResendable(record))
|
|
354
|
+
return { kind: "not-unknown", record };
|
|
355
|
+
const lifetime = new AbortController();
|
|
356
|
+
const lifetimeSignal = context.signal ? AbortSignal.any([context.signal, lifetime.signal]) : lifetime.signal;
|
|
357
|
+
let answer;
|
|
358
|
+
try {
|
|
359
|
+
const adapter = createHostedBlobWriteAdapter({ carrier, bundleId: scope.bundleId, prefix: scope.prefix, signal: lifetimeSignal, capabilities: once(context.capabilities) });
|
|
360
|
+
answer = await adapter.lookup(prepared.intent, prepared.identity);
|
|
361
|
+
}
|
|
362
|
+
catch (cause) {
|
|
363
|
+
return { kind: "lookup-unavailable", record, cause };
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
lifetime.abort();
|
|
367
|
+
}
|
|
368
|
+
if (answer.status !== "absent") {
|
|
369
|
+
// The host recorded it: settle with that very answer, through the delivery's own validation.
|
|
370
|
+
const settled = await drive(journal, { ...context, delivery: { maxLookups: 1 } }, requestId, { start: "lookup" }, answer);
|
|
371
|
+
if (settled.kind === "delivered" && settled.outcome.kind !== "unknown")
|
|
372
|
+
return settled;
|
|
373
|
+
return { kind: "lookup-unavailable", record, cause: new Error("the host's recorded outcome does not answer this intent") };
|
|
374
|
+
}
|
|
375
|
+
const target = record.target;
|
|
376
|
+
for (let attempt = 1;; attempt += 1) {
|
|
377
|
+
const snapshot = premises ? await premises.snapshot(target, { kind: "forget", record }) : { guard: (await journal.readBlobWithJournal(target)).guard };
|
|
378
|
+
try {
|
|
379
|
+
await journal.forgetBlobIntent(requestId, mutationOptions(snapshot));
|
|
380
|
+
return { kind: "forgotten", record };
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
if (!(error instanceof JournalGuardConflict) || attempt >= GUARD_ATTEMPTS)
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Nothing was sent, and the write stays held under `requestId`: `held` is true when it is
|
|
390
|
+
* resendable (`settleBlobWrite` or the same `writeBlob` call sends it), false when the journal
|
|
391
|
+
* could not record that and only a lookup may settle it. `reason` says why it was not sent.
|
|
392
|
+
*/
|
|
393
|
+
export class HostedBlobWriteNotSent extends Error {
|
|
394
|
+
code = "BLOB_WRITE_NOT_SENT";
|
|
395
|
+
requestId;
|
|
396
|
+
reason;
|
|
397
|
+
held;
|
|
398
|
+
constructor(requestId, reason, held, options = {}) {
|
|
399
|
+
super(`blob write ${requestId} was not sent (${reason}${options.detail ? `: ${options.detail}` : ""})`, "cause" in options ? { cause: options.cause } : undefined);
|
|
400
|
+
this.name = "HostedBlobWriteNotSent";
|
|
401
|
+
this.requestId = requestId;
|
|
402
|
+
this.reason = reason;
|
|
403
|
+
this.held = held;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const OPERATIONS = { "blobs.create.v1": "create", "blobs.replace.v1": "replace", "blobs.delete.v1": "delete" };
|
|
407
|
+
const ROUTES = { "blobs.create.v1": "blob-create", "blobs.replace.v1": "blob-replace", "blobs.delete.v1": "blob-delete" };
|
|
408
|
+
/** Refusals after which a held capabilities answer may be what went wrong. */
|
|
409
|
+
const DROPPING_BLOB_REFUSALS = new Set(["insufficient_scope", "bundle_not_found", "invalid_input"]);
|
|
410
|
+
// `request_capacity` keeps the held answer: a quota says nothing about the bundle's capabilities.
|
|
411
|
+
/** Run `task` after every earlier task on `name` in `locks`. */
|
|
412
|
+
async function chained(locks, name, task) {
|
|
413
|
+
const previous = locks.get(name) ?? Promise.resolve();
|
|
414
|
+
const run = previous.then(task, task);
|
|
415
|
+
const settled = run.then(() => undefined, () => undefined);
|
|
416
|
+
locks.set(name, settled);
|
|
417
|
+
try {
|
|
418
|
+
return await run;
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
if (locks.get(name) === settled)
|
|
422
|
+
locks.delete(name);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const byRequest = (context, requestId, task) => chained(context.blobLocks, `request:${requestId}`, task);
|
|
426
|
+
// The journal guard is keyed by key alone, across bundles: one chain per key keeps this client's
|
|
427
|
+
// own calls from moving each other's guard.
|
|
428
|
+
const byKey = (context, key, task) => chained(context.blobLocks, `key:${key}`, task);
|
|
429
|
+
function baseOf(record) {
|
|
430
|
+
const { intent, identity } = decodePreparedBlobIntent(record.prepared);
|
|
431
|
+
return { requestId: identity.requestId, key: intent.key, operation: OPERATIONS[intent.operationId] };
|
|
432
|
+
}
|
|
433
|
+
function resultOf(base, outcome) {
|
|
434
|
+
switch (outcome.kind) {
|
|
435
|
+
case "committed": {
|
|
436
|
+
const { receipt } = outcome;
|
|
437
|
+
const result = receipt.result;
|
|
438
|
+
return { ...base, status: "committed", head: receipt.head, ...(result.operationId === "blobs.delete.v1" ? {} : { changed: result.data.changed }) };
|
|
439
|
+
}
|
|
440
|
+
case "conflict": {
|
|
441
|
+
const { error } = outcome.receipt.result;
|
|
442
|
+
return { ...base, status: "conflict", reason: "version_conflict", actual: error.actual === null || error.actual === undefined ? null : { version: error.actual, contentType: error.actualContentType } };
|
|
443
|
+
}
|
|
444
|
+
case "refused":
|
|
445
|
+
if (outcome.code === "concurrent_change")
|
|
446
|
+
return { ...base, status: "conflict", reason: "concurrent_change", message: outcome.message };
|
|
447
|
+
return { ...base, status: "refused", code: outcome.code, message: outcome.message };
|
|
448
|
+
case "not-executed":
|
|
449
|
+
return { ...base, status: "held", deletions: outcome.details.deletions, baseline: outcome.details.baseline };
|
|
450
|
+
case "unknown":
|
|
451
|
+
return { ...base, status: "unknown" };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/** The bundle's write side, read only when the owner first needs it. */
|
|
455
|
+
function lazySide(context, workspace, bundleId, signal) {
|
|
456
|
+
let side;
|
|
457
|
+
return {
|
|
458
|
+
capabilities: async () => (await untilAborted((side ??= writeSide(context, workspace, bundleId)).answer, signal)).capabilities,
|
|
459
|
+
drop: () => {
|
|
460
|
+
if (side)
|
|
461
|
+
dropWriteSide(context, workspace, bundleId, side);
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function ownerContext(context, bundleId, workspace, capabilities, signal) {
|
|
466
|
+
return {
|
|
467
|
+
scope: { origin: context.origin, prefix: context.prefix, bundleId, workspaceSlug: workspace },
|
|
468
|
+
// A recorded write is sent and looked up in the workspace it was recorded in, whichever client asks.
|
|
469
|
+
carrier: context.carrierFor(workspace),
|
|
470
|
+
capabilities,
|
|
471
|
+
signal,
|
|
472
|
+
...(context.lookupDelayMs === undefined ? {} : { delivery: { lookupDelayMs: context.lookupDelayMs } }),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/** The client's answer for one drive; throws only when nothing was sent. */
|
|
476
|
+
function answerOf(context, side, base, route, drive) {
|
|
477
|
+
switch (drive.kind) {
|
|
478
|
+
case "not-admitted":
|
|
479
|
+
return drive.reason === "precondition"
|
|
480
|
+
? { ...base, status: "refused", code: "invalid_input", message: `${drive.check.message}. Nothing was sent.` }
|
|
481
|
+
: { ...base, status: "refused", code: "unsettled_write", message: `An earlier write of this key in this bundle is still unsettled (${drive.unsettled.join(", ")}); settle or forget it first. Nothing was sent.` };
|
|
482
|
+
case "not-served":
|
|
483
|
+
side.drop();
|
|
484
|
+
throw new HostedBlobWritesNotServed(route);
|
|
485
|
+
case "unrecorded":
|
|
486
|
+
return { status: "unrecorded", requestId: base.requestId };
|
|
487
|
+
case "scope-mismatch":
|
|
488
|
+
return { ...base, status: "refused", code: "scope_mismatch", message: "This write was recorded for another host or sync prefix than this client's; settle it from a client of that target. Nothing was sent." };
|
|
489
|
+
case "held":
|
|
490
|
+
return { ...base, status: "held", deletions: drive.hold.deletions, baseline: drive.hold.baseline };
|
|
491
|
+
case "unsent":
|
|
492
|
+
return { ...base, status: "pending" };
|
|
493
|
+
case "not-sendable":
|
|
494
|
+
side.drop();
|
|
495
|
+
throw new HostedBlobWriteNotSent(base.requestId, "precondition_failed", true, { detail: drive.check.message });
|
|
496
|
+
case "not-dispatched":
|
|
497
|
+
throw new HostedBlobWriteNotSent(base.requestId, drive.code, drive.rolledBack, drive.cause === undefined ? {} : { cause: drive.cause });
|
|
498
|
+
case "delivered": {
|
|
499
|
+
const result = resultOf(base, drive.outcome);
|
|
500
|
+
if (result.status === "unknown" || (result.status === "refused" && DROPPING_BLOB_REFUSALS.has(result.code)))
|
|
501
|
+
side.drop();
|
|
502
|
+
return result;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function assertPin(pin, name) {
|
|
507
|
+
if (typeof pin !== "object" || pin === null || !isContentVersion(pin.version) || typeof pin.contentType !== "string")
|
|
508
|
+
throw new TypeError(`${name} is a pin: { version, contentType }`);
|
|
509
|
+
}
|
|
510
|
+
function assertKey(key) {
|
|
511
|
+
if (typeof key !== "string" || key === "")
|
|
512
|
+
throw new TypeError("a blob write names its key");
|
|
513
|
+
}
|
|
514
|
+
function assertBundle(bundleId) {
|
|
515
|
+
if (typeof bundleId !== "string" || !isHostedBundleId(bundleId))
|
|
516
|
+
throw new TypeError("a blob write names a bare hosted bundle id");
|
|
517
|
+
}
|
|
518
|
+
function assertRequestId(requestId) {
|
|
519
|
+
if (requestId !== undefined && (typeof requestId !== "string" || !isWriteRequestId(requestId)))
|
|
520
|
+
throw new TypeError("a request identity is a lowercase UUID v4");
|
|
521
|
+
}
|
|
522
|
+
/** Submit a new write, or resume the one held under the same identity. */
|
|
523
|
+
async function submit(context, bundleId, intent, bytes, options) {
|
|
524
|
+
const requestId = options.requestId ?? mintRequestId();
|
|
525
|
+
const base = { requestId, key: intent.key, operation: OPERATIONS[intent.operationId] };
|
|
526
|
+
const route = `${context.prefix}/${ROUTES[intent.operationId]}`;
|
|
527
|
+
// The input alone (key policy, content types, the 16 MiB bound) is checked before any request.
|
|
528
|
+
const input = checkBlobAdmission({
|
|
529
|
+
operation: base.operation,
|
|
530
|
+
key: intent.key,
|
|
531
|
+
...(intent.operationId === "blobs.delete.v1" ? {} : { contentType: intent.contentType }),
|
|
532
|
+
...(intent.operationId === "blobs.create.v1" ? {} : { expectedContentType: intent.expectedContentType }),
|
|
533
|
+
...(bytes === undefined ? {} : { byteLength: bytes.byteLength }),
|
|
534
|
+
});
|
|
535
|
+
if (!input.ok)
|
|
536
|
+
return { ...base, status: "refused", code: "invalid_input", message: `${input.message}. Nothing was sent.` };
|
|
537
|
+
const signal = writeSignal(context, options.signal);
|
|
538
|
+
signal.throwIfAborted();
|
|
539
|
+
return byRequest(context, requestId, () => byKey(context, intent.key, async () => {
|
|
540
|
+
signal.throwIfAborted();
|
|
541
|
+
const known = await context.blobJournal.readBlobIntent(requestId);
|
|
542
|
+
if (known) {
|
|
543
|
+
const prior = decodePreparedBlobIntent(known.prepared).scope;
|
|
544
|
+
if (prior.origin !== context.origin || prior.prefix !== context.prefix) {
|
|
545
|
+
return { ...base, status: "refused", code: "scope_mismatch", message: "This request identity names a write recorded for another host or sync prefix than this client's; settle it from a client of that target. Nothing was sent." };
|
|
546
|
+
}
|
|
547
|
+
const sameTarget = prior.origin === context.origin && prior.prefix === context.prefix && prior.bundleId === bundleId;
|
|
548
|
+
const sameWrite = sameTarget && equalJournalValue(decodePreparedBlobIntent(known.prepared).intent, intent);
|
|
549
|
+
if (!sameWrite || prior.workspaceSlug !== context.workspace) {
|
|
550
|
+
const message = sameWrite
|
|
551
|
+
? `This request identity names the same write in ${prior.workspaceSlug === null ? "no workspace" : `workspace '${prior.workspaceSlug}'`}; confirm it from that client. Nothing was sent.`
|
|
552
|
+
: "This request identity already names a different blob write; nothing was sent.";
|
|
553
|
+
return { ...base, status: "refused", code: "invalid_input", message };
|
|
554
|
+
}
|
|
555
|
+
return settleKnown(context, known, signal, options.acceptDeletes, "settle");
|
|
556
|
+
}
|
|
557
|
+
// Nothing has left yet: a failure to read the capabilities is the caller's typed error, and
|
|
558
|
+
// the gate, the checks before admission and the adapter all use this one answer.
|
|
559
|
+
const side = writeSide(context, context.workspace, bundleId);
|
|
560
|
+
const { capabilities } = await untilAborted(side.answer, signal);
|
|
561
|
+
const drop = { drop: () => dropWriteSide(context, context.workspace, bundleId, side) };
|
|
562
|
+
if (capabilities.blobWrites !== "allowed")
|
|
563
|
+
throw new HostedBlobWritesNotServed(route);
|
|
564
|
+
const check = checkBlobAdmission({
|
|
565
|
+
operation: base.operation,
|
|
566
|
+
key: intent.key,
|
|
567
|
+
...(intent.operationId === "blobs.delete.v1" ? {} : { contentType: intent.contentType }),
|
|
568
|
+
...(intent.operationId === "blobs.create.v1" ? {} : { expectedContentType: intent.expectedContentType }),
|
|
569
|
+
...(bytes === undefined ? {} : { byteLength: bytes.byteLength }),
|
|
570
|
+
maximumBytes: capabilities.blobWriteBytes ?? null,
|
|
571
|
+
});
|
|
572
|
+
if (!check.ok)
|
|
573
|
+
return { ...base, status: "refused", code: "invalid_input", message: `${check.message}. Nothing was sent.` };
|
|
574
|
+
// An abort before admission admits nothing: once admitted, a write is held until it settles.
|
|
575
|
+
signal.throwIfAborted();
|
|
576
|
+
const prepared = prepareBlobIntent({ scope: { origin: context.origin, prefix: context.prefix, bundleId, workspaceSlug: context.workspace }, intent, identity: { requestId, binding: context.binding }, createdAt: new Date().toISOString() }, bytes);
|
|
577
|
+
const drive = await driveJournaledBlob(context.blobJournal, ownerContext(context, bundleId, context.workspace, async () => capabilities, signal), requestId, {
|
|
578
|
+
start: "admit",
|
|
579
|
+
prepared,
|
|
580
|
+
...(options.acceptDeletes === undefined ? {} : { acceptDeletes: options.acceptDeletes }),
|
|
581
|
+
});
|
|
582
|
+
return answerOf(context, drop, base, route, drive);
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
/** Settle or look up a held record, under its key's chain. */
|
|
586
|
+
async function settleKnown(context, record, signal, acceptDeletes, start) {
|
|
587
|
+
const { scope, intent } = decodePreparedBlobIntent(record.prepared);
|
|
588
|
+
const base = baseOf(record);
|
|
589
|
+
const side = lazySide(context, scope.workspaceSlug, scope.bundleId, signal);
|
|
590
|
+
const deletion = intent.operationId === "blobs.delete.v1";
|
|
591
|
+
const drive = await driveJournaledBlob(context.blobJournal, ownerContext(context, scope.bundleId, scope.workspaceSlug, side.capabilities, signal), record.requestId, {
|
|
592
|
+
start,
|
|
593
|
+
// A confirmation belongs to a delete's submission only; a lookup never carries one.
|
|
594
|
+
...(start === "settle" && deletion && acceptDeletes !== undefined ? { acceptDeletes } : {}),
|
|
595
|
+
});
|
|
596
|
+
return answerOf(context, side, base, `${context.prefix}/${ROUTES[intent.operationId]}`, drive);
|
|
597
|
+
}
|
|
598
|
+
/** Run `task` on a held record under its request and key chains, reading it again under both. */
|
|
599
|
+
async function withHeld(context, requestId, signal, task, absent) {
|
|
600
|
+
signal.throwIfAborted();
|
|
601
|
+
return byRequest(context, requestId, async () => {
|
|
602
|
+
signal.throwIfAborted();
|
|
603
|
+
const first = await context.blobJournal.readBlobIntent(requestId);
|
|
604
|
+
if (!first)
|
|
605
|
+
return absent();
|
|
606
|
+
return byKey(context, first.target, async () => {
|
|
607
|
+
const record = await context.blobJournal.readBlobIntent(requestId);
|
|
608
|
+
return record ? task(record) : absent();
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/** `HostedClient.writeBlob`. */
|
|
613
|
+
export function hostedWriteBlob(context, bundleId, input, options = {}) {
|
|
614
|
+
assertBundle(bundleId);
|
|
615
|
+
if (typeof input !== "object" || input === null)
|
|
616
|
+
throw new TypeError("a blob write takes { key, bytes, contentType } with expectAbsent: true or expected");
|
|
617
|
+
const { key, bytes, contentType } = input;
|
|
618
|
+
assertKey(key);
|
|
619
|
+
if (!(bytes instanceof Uint8Array))
|
|
620
|
+
throw new TypeError("a blob write's bytes are a Uint8Array");
|
|
621
|
+
if (typeof contentType !== "string")
|
|
622
|
+
throw new TypeError("a blob write names its content type");
|
|
623
|
+
const creates = input.expectAbsent === true;
|
|
624
|
+
if (creates === (input.expected !== undefined))
|
|
625
|
+
throw new TypeError("a blob write takes exactly one of expectAbsent: true and expected");
|
|
626
|
+
if (!creates)
|
|
627
|
+
assertPin(input.expected, "expected");
|
|
628
|
+
assertRequestId(options.requestId);
|
|
629
|
+
if (options.acceptDeletes !== undefined)
|
|
630
|
+
throw new TypeError("acceptDeletes belongs only to a delete");
|
|
631
|
+
// Copied before any await, so a caller mutating its buffer cannot change what is recorded.
|
|
632
|
+
const copy = new Uint8Array(bytes);
|
|
633
|
+
const version = `sha256:${sha256HexOfBytes(copy)}`;
|
|
634
|
+
const intent = creates
|
|
635
|
+
? { operationId: "blobs.create.v1", key, expectedVersion: null, expectedContentType: null, version, contentType }
|
|
636
|
+
: { operationId: "blobs.replace.v1", key, expectedVersion: input.expected.version, expectedContentType: input.expected.contentType, version, contentType };
|
|
637
|
+
return submit(context, bundleId, intent, copy, options);
|
|
638
|
+
}
|
|
639
|
+
/** `HostedClient.deleteBlob`. */
|
|
640
|
+
export function hostedDeleteBlob(context, bundleId, input, options = {}) {
|
|
641
|
+
assertBundle(bundleId);
|
|
642
|
+
if (typeof input !== "object" || input === null)
|
|
643
|
+
throw new TypeError("a blob delete takes { key, expected }");
|
|
644
|
+
assertKey(input.key);
|
|
645
|
+
assertPin(input.expected, "expected");
|
|
646
|
+
assertRequestId(options.requestId);
|
|
647
|
+
if (options.acceptDeletes !== undefined && !isAcceptedDeletionCount(options.acceptDeletes))
|
|
648
|
+
throw new TypeError("acceptDeletes is a deletion count from 1 to 100,000");
|
|
649
|
+
const intent = { operationId: "blobs.delete.v1", key: input.key, expectedVersion: input.expected.version, expectedContentType: input.expected.contentType };
|
|
650
|
+
return submit(context, bundleId, intent, undefined, options);
|
|
651
|
+
}
|
|
652
|
+
/** `HostedClient.lookupBlobWrite`: one outcome lookup of a held write; it never sends the write. */
|
|
653
|
+
export function hostedLookupBlobWrite(context, requestId, options = {}) {
|
|
654
|
+
const signal = writeSignal(context, options.signal);
|
|
655
|
+
return withHeld(context, requestId, signal, (record) => settleKnown(context, record, signal, undefined, "lookup"), () => ({ status: "unrecorded", requestId }));
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* `HostedClient.settleBlobWrite`: submit a held write whose every attempt is proven not applied
|
|
659
|
+
* (pending or unknown, `attempts === notAppliedThrough`), otherwise look it up. An absent lookup
|
|
660
|
+
* is `unknown`, never a resend.
|
|
661
|
+
*/
|
|
662
|
+
export function hostedSettleBlobWrite(context, requestId, options = {}) {
|
|
663
|
+
if (options.acceptDeletes !== undefined && !isAcceptedDeletionCount(options.acceptDeletes))
|
|
664
|
+
throw new TypeError("acceptDeletes is a deletion count from 1 to 100,000");
|
|
665
|
+
const signal = writeSignal(context, options.signal);
|
|
666
|
+
return withHeld(context, requestId, signal, (record) => settleKnown(context, record, signal, options.acceptDeletes, "settle"), () => ({ status: "unrecorded", requestId }));
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* `HostedClient.forgetBlobWrite`: drop an `unknown` write after one fresh lookup answers `absent`.
|
|
670
|
+
* The earlier delivery may still commit; the host's compare-and-swap prevents a lost update.
|
|
671
|
+
*/
|
|
672
|
+
export function hostedForgetBlobWrite(context, requestId, options = {}) {
|
|
673
|
+
const signal = writeSignal(context, options.signal);
|
|
674
|
+
return withHeld(context, requestId, signal, async (record) => {
|
|
675
|
+
const { scope, intent } = decodePreparedBlobIntent(record.prepared);
|
|
676
|
+
const base = baseOf(record);
|
|
677
|
+
const side = lazySide(context, scope.workspaceSlug, scope.bundleId, signal);
|
|
678
|
+
const forgot = await forgetJournaledBlob(context.blobJournal, ownerContext(context, scope.bundleId, scope.workspaceSlug, side.capabilities, signal), requestId);
|
|
679
|
+
switch (forgot.kind) {
|
|
680
|
+
case "forgotten":
|
|
681
|
+
return { ...base, status: "forgotten" };
|
|
682
|
+
case "not-unknown":
|
|
683
|
+
return { ...base, status: "kept", reason: "not_unknown", state: forgot.record.state };
|
|
684
|
+
case "lookup-unavailable":
|
|
685
|
+
return { ...base, status: "kept", reason: "lookup_unavailable", state: forgot.record.state };
|
|
686
|
+
default:
|
|
687
|
+
return answerOf(context, side, base, `${context.prefix}/${ROUTES[intent.operationId]}`, forgot);
|
|
688
|
+
}
|
|
689
|
+
}, () => ({ status: "unrecorded", requestId }));
|
|
690
|
+
}
|
|
691
|
+
/** `HostedClient.heldBlobWrites`: every unsettled blob write, after sweeping settled ones left by a crash. */
|
|
692
|
+
export async function hostedHeldBlobWrites(context) {
|
|
693
|
+
for (const row of await context.blobJournal.listBlobIntents(["acknowledged", "conflict", "refused"])) {
|
|
694
|
+
await withHeld(context, row.requestId, context.signal, (record) => settleKnown(context, record, context.signal, undefined, "lookup"), () => undefined).catch(() => undefined);
|
|
695
|
+
}
|
|
696
|
+
const held = [];
|
|
697
|
+
for (const row of await context.blobJournal.listBlobIntents()) {
|
|
698
|
+
if (row.receipt !== undefined)
|
|
699
|
+
continue;
|
|
700
|
+
const { scope, intent, createdAt } = decodePreparedBlobIntent(row.prepared);
|
|
701
|
+
held.push({
|
|
702
|
+
requestId: row.requestId,
|
|
703
|
+
bundleId: scope.bundleId,
|
|
704
|
+
workspace: scope.workspaceSlug,
|
|
705
|
+
key: intent.key,
|
|
706
|
+
operation: OPERATIONS[intent.operationId],
|
|
707
|
+
state: row.state,
|
|
708
|
+
attempts: row.attempts,
|
|
709
|
+
resendable: blobIntentResendable(row),
|
|
710
|
+
...(row.deletionHold ? { hold: { deletions: row.deletionHold.deletions, baseline: row.deletionHold.baseline } } : {}),
|
|
711
|
+
createdAt,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return held;
|
|
715
|
+
}
|
|
716
|
+
// ── reads ───────────────────────────────────────────────────────────────────────────────────
|
|
717
|
+
function blobReader(context, bundleId, signal) {
|
|
718
|
+
return createHostedBlobReadAdapter({
|
|
719
|
+
carrier: context.carrierFor(context.workspace),
|
|
720
|
+
bundleId,
|
|
721
|
+
prefix: context.prefix,
|
|
722
|
+
signal,
|
|
723
|
+
capabilities: async () => (await writeSide(context, context.workspace, bundleId).answer).capabilities,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
/** A host serving no blob reads is the route it lacks, as for writes. */
|
|
727
|
+
async function served(route, read) {
|
|
728
|
+
try {
|
|
729
|
+
return await read();
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
if (error instanceof HostedReadAdapterError && error.code === "unsupported")
|
|
733
|
+
throw new HostedRouteNotServed(route);
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/** `HostedClient.blobHeads`: the bundle's whole blob listing, verified against its digest. */
|
|
738
|
+
export function hostedBlobHeads(context, bundleId, options = {}) {
|
|
739
|
+
assertBundle(bundleId);
|
|
740
|
+
const signal = writeSignal(context, options.signal);
|
|
741
|
+
return served(`${context.prefix}/blob-heads`, () => blobReader(context, bundleId, signal).heads());
|
|
742
|
+
}
|
|
743
|
+
/** `HostedClient.blobHead`: one key's head from the listing, or null when the key is absent. */
|
|
744
|
+
export async function hostedBlobHead(context, bundleId, key, options = {}) {
|
|
745
|
+
assertKey(key);
|
|
746
|
+
const listing = await hostedBlobHeads(context, bundleId, options);
|
|
747
|
+
return listing.heads.find((head) => head.key === key) ?? null;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* `HostedClient.readBlob`: the bytes at exactly `pin`, verified against its length, digest and
|
|
751
|
+
* content type; null when the key is no longer at that pin (the host's `409`).
|
|
752
|
+
*/
|
|
753
|
+
export async function hostedReadBlob(context, bundleId, key, options) {
|
|
754
|
+
assertBundle(bundleId);
|
|
755
|
+
assertKey(key);
|
|
756
|
+
if (typeof options !== "object" || options === null)
|
|
757
|
+
throw new TypeError("readBlob takes { pin }: a read names the version and content type it expects");
|
|
758
|
+
assertPin(options.pin, "pin");
|
|
759
|
+
const signal = writeSignal(context, options.signal);
|
|
760
|
+
const pin = { key, version: options.pin.version, contentType: options.pin.contentType };
|
|
761
|
+
try {
|
|
762
|
+
return await served(`${context.prefix}/blob-read`, () => blobReader(context, bundleId, signal).read(pin));
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
if (error instanceof RemoteError && error.status === 409 && error.code === "version_conflict")
|
|
766
|
+
return null;
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
//# sourceMappingURL=blob-write.js.map
|