@superbee/core 0.2.0-pre.3 → 0.2.0-pre.31
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 +1084 -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 +52 -0
- package/dist/body-replace-guards.d.ts.map +1 -0
- package/dist/body-replace-guards.js +176 -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/connector-request.d.ts +87 -0
- package/dist/connector-request.d.ts.map +1 -0
- package/dist/connector-request.js +370 -0
- package/dist/connector-request.js.map +1 -0
- package/dist/connectors.d.ts +262 -0
- package/dist/connectors.d.ts.map +1 -0
- package/dist/connectors.js +1179 -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 +5 -1
- package/dist/document-codec.d.ts.map +1 -1
- package/dist/document-codec.js +7 -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 +61 -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/domain-model.d.ts +73 -0
- package/dist/domain-model.d.ts.map +1 -0
- package/dist/domain-model.js +169 -0
- package/dist/domain-model.js.map +1 -0
- package/dist/engine.d.ts +16 -8
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +42 -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 +160 -0
- package/dist/file-journaled-backend.d.ts.map +1 -0
- package/dist/file-journaled-backend.js +1345 -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/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 -37
- package/dist/indexeddb-backend.d.ts.map +1 -1
- package/dist/indexeddb-backend.js +467 -113
- package/dist/indexeddb-backend.js.map +1 -1
- package/dist/journaled-backend.d.ts +232 -15
- package/dist/journaled-backend.d.ts.map +1 -1
- package/dist/journaled-backend.js +254 -55
- 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.map +1 -1
- package/dist/memory-backend.js +6 -2
- 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 +112 -0
- package/dist/recipe-lifecycle/evolution.d.ts.map +1 -0
- package/dist/recipe-lifecycle/evolution.js +941 -0
- package/dist/recipe-lifecycle/evolution.js.map +1 -0
- package/dist/recipe-lifecycle/index.d.ts +30 -0
- package/dist/recipe-lifecycle/index.d.ts.map +1 -0
- package/dist/recipe-lifecycle/index.js +26 -0
- package/dist/recipe-lifecycle/index.js.map +1 -0
- package/dist/recipe-lifecycle/install.d.ts +155 -0
- package/dist/recipe-lifecycle/install.d.ts.map +1 -0
- package/dist/recipe-lifecycle/install.js +594 -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 +151 -0
- package/dist/recipe-lifecycle/stamp.d.ts.map +1 -0
- package/dist/recipe-lifecycle/stamp.js +364 -0
- package/dist/recipe-lifecycle/stamp.js.map +1 -0
- package/dist/recipe-set-preflight.d.ts +139 -0
- package/dist/recipe-set-preflight.d.ts.map +1 -0
- package/dist/recipe-set-preflight.js +536 -0
- package/dist/recipe-set-preflight.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 +168 -201
- package/dist/remote-backend.js.map +1 -1
- package/dist/remote-document-codec.d.ts +2 -1
- package/dist/remote-document-codec.d.ts.map +1 -1
- package/dist/remote-document-codec.js +3 -2
- package/dist/remote-document-codec.js.map +1 -1
- 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 +0 -40
- package/dist/remote-operations.d.ts.map +1 -1
- package/dist/remote-operations.js +26 -6
- 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 +150 -0
- package/dist/search/text.js.map +1 -0
- package/dist/site-capabilities.d.ts +345 -0
- package/dist/site-capabilities.d.ts.map +1 -0
- package/dist/site-capabilities.js +724 -0
- package/dist/site-capabilities.js.map +1 -0
- package/dist/source-engine/guard.d.ts +78 -0
- package/dist/source-engine/guard.d.ts.map +1 -0
- package/dist/source-engine/guard.js +423 -0
- package/dist/source-engine/guard.js.map +1 -0
- package/dist/source-engine/index.d.ts +26 -0
- package/dist/source-engine/index.d.ts.map +1 -0
- package/dist/source-engine/index.js +21 -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 +620 -0
- package/dist/source-engine/observe.js.map +1 -0
- package/dist/source-engine/overlay.d.ts +49 -0
- package/dist/source-engine/overlay.d.ts.map +1 -0
- package/dist/source-engine/overlay.js +114 -0
- package/dist/source-engine/overlay.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 +287 -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 +266 -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 +273 -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/source-profiles/index.d.ts +31 -0
- package/dist/source-profiles/index.d.ts.map +1 -0
- package/dist/source-profiles/index.js +206 -0
- package/dist/source-profiles/index.js.map +1 -0
- package/dist/source-profiles/link-header.d.ts +6 -0
- package/dist/source-profiles/link-header.d.ts.map +1 -0
- package/dist/source-profiles/link-header.js +131 -0
- package/dist/source-profiles/link-header.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 +38 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -10
- package/dist/types.js.map +1 -1
- package/dist/uncertain-write.d.ts +161 -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 +107 -2
|
@@ -0,0 +1,1345 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { captureNewDocumentIntent } from "./journaled-backend.js";
|
|
3
|
+
/**
|
|
4
|
+
* The API is experimental (`0.2.0-pre`) and may change before a stable release. The on-disk
|
|
5
|
+
* format is a supported compatibility promise, because the hosted client's durable journal
|
|
6
|
+
* (`openHostedClientJournal`) keeps app state in it: a format bump ships a reader for the older
|
|
7
|
+
* layout, or a migration.
|
|
8
|
+
*
|
|
9
|
+
* A Node {@link JournaledBackend}: the private store behind a CLI working copy, proven by the
|
|
10
|
+
* same contract kit as the IndexedDB adapter. It is an append-only, checksummed transaction log
|
|
11
|
+
* over an in-memory index, in one directory the store owns:
|
|
12
|
+
*
|
|
13
|
+
* - `store.log` holds one record per transaction. A record is the transaction's effect as a list
|
|
14
|
+
* of absolute puts and removals over the five record families (documents, reserved files,
|
|
15
|
+
* blobs, intents, meta) plus the intent sequence, framed as magic, format version, length,
|
|
16
|
+
* SHA-256 of the payload, payload. The payload is tagged JSON this module owns, so the bytes
|
|
17
|
+
* never depend on the Node version that wrote them. Every check a verb makes runs synchronously against the index before its
|
|
18
|
+
* record is written; the record is written at the log's end and fsynced, and only then applied
|
|
19
|
+
* to the index and the verb resolved. So a resolved write survives a crash, a rejected one
|
|
20
|
+
* left nothing behind, and a record that was never fsynced was never acknowledged.
|
|
21
|
+
* - `store.snapshot` is the whole index at one transaction number, written by {@link compact}:
|
|
22
|
+
* write a temporary file, fsync it, rename it over the snapshot, fsync the directory, then
|
|
23
|
+
* truncate the log. A crash at any point in that sequence leaves either the old snapshot and
|
|
24
|
+
* the whole log, or the new snapshot and log records it already covers, which open skips by
|
|
25
|
+
* transaction number.
|
|
26
|
+
*
|
|
27
|
+
* At open the snapshot is loaded and the log replayed. Bytes after the last whole record are a
|
|
28
|
+
* torn write (the one append that can be interrupted, never acknowledged, its pages persisted in
|
|
29
|
+
* any order) and are truncated away, unless a whole record begins inside them: that is
|
|
30
|
+
* corruption, and open refuses without touching the files. One transaction spans documents, intents and meta, which is what the seam requires:
|
|
31
|
+
* the engine settles an intent together with the global pause flag, and writes a receipt, a
|
|
32
|
+
* base and a body record at once.
|
|
33
|
+
*
|
|
34
|
+
* The store holds an exclusive same-user cross-process lock on its directory from open to
|
|
35
|
+
* {@link close}, so a second process cannot replay a log another process is appending to, and
|
|
36
|
+
* compaction always runs under that lock. Within the process, mutations are serialized; reads
|
|
37
|
+
* never wait for them and always see the last committed transaction.
|
|
38
|
+
*
|
|
39
|
+
* Durability is `fsync`: a resolved mutation's record was fsynced, and so was the directory
|
|
40
|
+
* entry of every file the store created. On macOS `fsync` does not force the drive's own cache
|
|
41
|
+
* (Node exposes no `F_FULLFSYNC`), which is the same promise every Node program gets there.
|
|
42
|
+
*/
|
|
43
|
+
import { promises as fs } from "node:fs";
|
|
44
|
+
import path from "node:path";
|
|
45
|
+
import { assertBlobKey } from "./blob-contract.js";
|
|
46
|
+
import { BLOB_JOURNAL_BOUNDS, captureBlobAdmission, captureBlobForget, captureBlobIntentRecord, captureBlobMetaWrite, captureBlobRetirement, captureBlobUpdate, decideBlobAdmission, decideBlobForget, decideBlobMetaWrite, decideBlobRetirement, decideBlobUpdate, readBlobJournal, } from "./blob-journal.js";
|
|
47
|
+
import { resolveContentType } from "./content-type.js";
|
|
48
|
+
import { OPEN_LOG, STORE_RAW, STORE_BYTE_LIMIT } from "./file-journaled-backend-internal.js";
|
|
49
|
+
import { captureFilesystemHostPolicy } from "./filesystem-host.js";
|
|
50
|
+
import { acquireFilesystemMutationLock } from "./filesystem-lock.js";
|
|
51
|
+
import { MalformedDocumentError, parseMarkdown, stringifyDoc } from "./frontmatter.js";
|
|
52
|
+
import { assertDeletionIntent, assertJournalGuard, assertJournalIntentChanges, assertJournalMetaChanges, assertJournalResolutionOptions, assertJournalSnapshot, assertMetaWrite, captureIntentUpdate, captureJournalDeleteOptions, captureJournalGuardOption, captureJournalValue, captureJournalWriteOptions, captureMetaWrite, captureRetireAcknowledged, deletionIntentRecord, IntentHoldConflict, IntentStateConflict, JournalGuardConflict, JournalSnapshotConflict, } from "./journaled-backend.js";
|
|
53
|
+
import { mutationActorFromFrontmatter } from "./mutation-attribution.js";
|
|
54
|
+
import { assertSafeBlobKey, assertSafeConceptId, assertSafeReservedDir, assertSafeReservedFilename, compareStorageKeys, pathFromConceptId } from "./paths.js";
|
|
55
|
+
import { parseLeadingFrontmatter } from "./portable-frontmatter.js";
|
|
56
|
+
import { sha256HexOfBytes } from "./sha256.js";
|
|
57
|
+
import { blobVersion, defaultActor, VersionConflict, versionOfBytes } from "./versioning.js";
|
|
58
|
+
// ── on-disk format ─────────────────────────────────────────────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* The newest layout this adapter writes and reads; it reads every earlier one too. A store stays
|
|
61
|
+
* at format 1 until it holds a blob intent (format 2), and at 2 until a blob intent carries
|
|
62
|
+
* not-dispatched evidence (format 3); the store keeps the highest format it has written. Bumping
|
|
63
|
+
* this requires a reader for the older layouts; open refuses any newer format.
|
|
64
|
+
*/
|
|
65
|
+
export const FILE_JOURNAL_FORMAT_VERSION = 3;
|
|
66
|
+
export const FILE_JOURNAL_LOG = "store.log";
|
|
67
|
+
export const FILE_JOURNAL_SNAPSHOT = "store.snapshot";
|
|
68
|
+
const SNAPSHOT_TEMP = "store.snapshot.tmp";
|
|
69
|
+
const RECORD_MAGIC = 0x53424a52; // "SBJR"
|
|
70
|
+
const RECORD_MAGIC_BYTES = Buffer.from([0x53, 0x42, 0x4a, 0x52]);
|
|
71
|
+
const SNAPSHOT_MAGIC = 0x53424a53; // "SBJS"
|
|
72
|
+
/** Every record and the snapshot: magic (4) + format version (4) + payload length (4) + SHA-256 of the payload (32). */
|
|
73
|
+
const HEADER = 44;
|
|
74
|
+
/** A record larger than this is refused at write and read as corruption at open. */
|
|
75
|
+
const MAXIMUM_RECORD_BYTES = 256 * 1024 * 1024;
|
|
76
|
+
const DEFAULT_COMPACT_AFTER_BYTES = 8 * 1024 * 1024;
|
|
77
|
+
const isDocumentIntent = (row) => !("recordFamily" in row);
|
|
78
|
+
const documentIntents = (state) => [...state.intents.values()].filter(isDocumentIntent);
|
|
79
|
+
const documentIntent = (state, id) => {
|
|
80
|
+
const row = state.intents.get(id);
|
|
81
|
+
return row && isDocumentIntent(row) ? row : undefined;
|
|
82
|
+
};
|
|
83
|
+
function emptyState() {
|
|
84
|
+
return { format: 1, documents: new Map(), reserved: new Map(), blobs: new Map(), intents: new Map(), meta: new Map(), sequence: 0 };
|
|
85
|
+
}
|
|
86
|
+
function applyChanges(state, changes) {
|
|
87
|
+
for (const change of changes) {
|
|
88
|
+
switch (change.family) {
|
|
89
|
+
case "document":
|
|
90
|
+
if (change.value)
|
|
91
|
+
state.documents.set(change.key, change.value);
|
|
92
|
+
else
|
|
93
|
+
state.documents.delete(change.key);
|
|
94
|
+
break;
|
|
95
|
+
case "reserved":
|
|
96
|
+
if (change.value)
|
|
97
|
+
state.reserved.set(change.key, change.value);
|
|
98
|
+
else
|
|
99
|
+
state.reserved.delete(change.key);
|
|
100
|
+
break;
|
|
101
|
+
case "blob":
|
|
102
|
+
if (change.value)
|
|
103
|
+
state.blobs.set(change.key, change.value);
|
|
104
|
+
else
|
|
105
|
+
state.blobs.delete(change.key);
|
|
106
|
+
break;
|
|
107
|
+
case "intent":
|
|
108
|
+
if (change.value)
|
|
109
|
+
state.intents.set(change.key, change.value);
|
|
110
|
+
else
|
|
111
|
+
state.intents.delete(change.key);
|
|
112
|
+
break;
|
|
113
|
+
case "meta":
|
|
114
|
+
if (change.present)
|
|
115
|
+
state.meta.set(change.key, change.value);
|
|
116
|
+
else
|
|
117
|
+
state.meta.delete(change.key);
|
|
118
|
+
break;
|
|
119
|
+
case "sequence":
|
|
120
|
+
state.sequence = change.value;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** The store's files do not hold a log or snapshot this adapter wrote; open changes nothing. */
|
|
126
|
+
export class FileJournalCorruptError extends Error {
|
|
127
|
+
name = "FileJournalCorruptError";
|
|
128
|
+
file;
|
|
129
|
+
offset;
|
|
130
|
+
constructor(file, message, offset) {
|
|
131
|
+
super(`${file}${offset === undefined ? "" : ` at byte ${offset}`}: ${message}`);
|
|
132
|
+
this.file = file;
|
|
133
|
+
this.offset = offset;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** A whole, checksummed record or snapshot in a format this adapter does not read (a newer store wrote it). */
|
|
137
|
+
export class FileJournalFormatError extends FileJournalCorruptError {
|
|
138
|
+
name = "FileJournalFormatError";
|
|
139
|
+
format;
|
|
140
|
+
constructor(file, format, offset) {
|
|
141
|
+
super(file, `written in store format ${format}; this adapter reads formats 1 to ${FILE_JOURNAL_FORMAT_VERSION}`, offset);
|
|
142
|
+
this.format = format;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The store cannot tell whether its last append reached the disk (the append failed and so did
|
|
147
|
+
* removing it), or it was closed. Every later call rejects; reopening decides from the files.
|
|
148
|
+
*/
|
|
149
|
+
export class FileJournalUnavailableError extends Error {
|
|
150
|
+
name = "FileJournalUnavailableError";
|
|
151
|
+
}
|
|
152
|
+
// ── the payload codec ──────────────────────────────────────────────────────────────────────
|
|
153
|
+
//
|
|
154
|
+
// Payloads are UTF-8 JSON in a tagged form that carries what the journal seam's opaque meta
|
|
155
|
+
// rows may hold (the structured-clone values: undefined, special numbers, bigint, Date, Map,
|
|
156
|
+
// Set, RegExp, typed arrays), so the bytes on disk depend on this module alone and never on the
|
|
157
|
+
// Node or V8 version that wrote them. A JSON string, boolean, null or finite number stands for
|
|
158
|
+
// itself; every other value is an array whose first element is its tag.
|
|
159
|
+
const TYPED_ARRAYS = {
|
|
160
|
+
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array,
|
|
161
|
+
Float32Array, Float64Array, BigInt64Array, BigUint64Array,
|
|
162
|
+
};
|
|
163
|
+
/** A value the codec cannot persist; the mutation carrying it is refused before anything is written. */
|
|
164
|
+
export class FileJournalValueError extends TypeError {
|
|
165
|
+
name = "FileJournalValueError";
|
|
166
|
+
}
|
|
167
|
+
const bytesOf = (view) => Buffer.from(view.buffer, view.byteOffset, view.byteLength).toString("base64");
|
|
168
|
+
function encodeValue(value, ancestors) {
|
|
169
|
+
switch (typeof value) {
|
|
170
|
+
case "string":
|
|
171
|
+
case "boolean":
|
|
172
|
+
return value;
|
|
173
|
+
case "undefined":
|
|
174
|
+
return ["U"];
|
|
175
|
+
case "bigint":
|
|
176
|
+
return ["I", value.toString()];
|
|
177
|
+
case "number":
|
|
178
|
+
if (Number.isNaN(value))
|
|
179
|
+
return ["N", "NaN"];
|
|
180
|
+
if (value === Infinity)
|
|
181
|
+
return ["N", "Infinity"];
|
|
182
|
+
if (value === -Infinity)
|
|
183
|
+
return ["N", "-Infinity"];
|
|
184
|
+
if (Object.is(value, -0))
|
|
185
|
+
return ["N", "-0"];
|
|
186
|
+
return value;
|
|
187
|
+
case "object":
|
|
188
|
+
break;
|
|
189
|
+
default:
|
|
190
|
+
throw new FileJournalValueError(`a ${typeof value} cannot be stored`);
|
|
191
|
+
}
|
|
192
|
+
if (value === null)
|
|
193
|
+
return null;
|
|
194
|
+
if (ancestors.has(value))
|
|
195
|
+
throw new FileJournalValueError("a cyclic value cannot be stored");
|
|
196
|
+
ancestors.add(value);
|
|
197
|
+
try {
|
|
198
|
+
if (Array.isArray(value)) {
|
|
199
|
+
const out = ["A"];
|
|
200
|
+
for (let index = 0; index < value.length; index++)
|
|
201
|
+
out.push(index in value ? encodeValue(value[index], ancestors) : ["H"]);
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
if (value instanceof Date)
|
|
205
|
+
return ["D", encodeValue(value.getTime(), ancestors)];
|
|
206
|
+
if (value instanceof Map)
|
|
207
|
+
return ["M", ...[...value].flatMap(([key, entry]) => [encodeValue(key, ancestors), encodeValue(entry, ancestors)])];
|
|
208
|
+
if (value instanceof Set)
|
|
209
|
+
return ["S", ...[...value].map((entry) => encodeValue(entry, ancestors))];
|
|
210
|
+
if (value instanceof RegExp)
|
|
211
|
+
throw new FileJournalValueError("a regular expression cannot be stored");
|
|
212
|
+
if (value instanceof ArrayBuffer)
|
|
213
|
+
return ["B", bytesOf(new Uint8Array(value))];
|
|
214
|
+
if (value instanceof DataView)
|
|
215
|
+
return ["V", bytesOf(value)];
|
|
216
|
+
if (ArrayBuffer.isView(value)) {
|
|
217
|
+
const name = Object.keys(TYPED_ARRAYS).find((candidate) => value instanceof TYPED_ARRAYS[candidate]);
|
|
218
|
+
if (!name)
|
|
219
|
+
throw new FileJournalValueError("an unsupported typed array cannot be stored");
|
|
220
|
+
return ["T", name, bytesOf(value)];
|
|
221
|
+
}
|
|
222
|
+
const prototype = Object.getPrototypeOf(value);
|
|
223
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
224
|
+
throw new FileJournalValueError("only plain objects and structured-clone built-ins can be stored");
|
|
225
|
+
const out = ["O"];
|
|
226
|
+
for (const [key, entry] of Object.entries(value))
|
|
227
|
+
out.push(key, encodeValue(entry, ancestors));
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
ancestors.delete(value);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** A typed array of a fixed, named kind over `buffer`; any other name is not a value this store wrote. */
|
|
235
|
+
function typedArray(name, buffer) {
|
|
236
|
+
switch (name) {
|
|
237
|
+
case "Int8Array": return new Int8Array(buffer);
|
|
238
|
+
case "Uint8Array": return new Uint8Array(buffer);
|
|
239
|
+
case "Uint8ClampedArray": return new Uint8ClampedArray(buffer);
|
|
240
|
+
case "Int16Array": return new Int16Array(buffer);
|
|
241
|
+
case "Uint16Array": return new Uint16Array(buffer);
|
|
242
|
+
case "Int32Array": return new Int32Array(buffer);
|
|
243
|
+
case "Uint32Array": return new Uint32Array(buffer);
|
|
244
|
+
case "Float32Array": return new Float32Array(buffer);
|
|
245
|
+
case "Float64Array": return new Float64Array(buffer);
|
|
246
|
+
case "BigInt64Array": return new BigInt64Array(buffer);
|
|
247
|
+
case "BigUint64Array": return new BigUint64Array(buffer);
|
|
248
|
+
default: throw new TypeError("unknown typed array");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function decodeValue(value) {
|
|
252
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number")
|
|
253
|
+
return value;
|
|
254
|
+
if (!Array.isArray(value) || typeof value[0] !== "string")
|
|
255
|
+
throw new TypeError("untagged value");
|
|
256
|
+
const [tag, ...rest] = value;
|
|
257
|
+
const bytes = (text) => {
|
|
258
|
+
if (typeof text !== "string")
|
|
259
|
+
throw new TypeError("bytes are not base64");
|
|
260
|
+
return Buffer.from(text, "base64");
|
|
261
|
+
};
|
|
262
|
+
switch (tag) {
|
|
263
|
+
case "U":
|
|
264
|
+
return undefined;
|
|
265
|
+
case "I":
|
|
266
|
+
return BigInt(rest[0]);
|
|
267
|
+
case "N":
|
|
268
|
+
return { NaN, Infinity, "-Infinity": -Infinity, "-0": -0 }[rest[0]] ?? (() => { throw new TypeError("unknown number"); })();
|
|
269
|
+
case "A": {
|
|
270
|
+
const out = new Array(rest.length);
|
|
271
|
+
rest.forEach((entry, index) => {
|
|
272
|
+
if (!(Array.isArray(entry) && entry.length === 1 && entry[0] === "H"))
|
|
273
|
+
out[index] = decodeValue(entry);
|
|
274
|
+
});
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
case "D":
|
|
278
|
+
return new Date(decodeValue(rest[0]));
|
|
279
|
+
case "M": {
|
|
280
|
+
const out = new Map();
|
|
281
|
+
for (let index = 0; index < rest.length; index += 2)
|
|
282
|
+
out.set(decodeValue(rest[index]), decodeValue(rest[index + 1]));
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
case "S":
|
|
286
|
+
return new Set(rest.map(decodeValue));
|
|
287
|
+
case "B": {
|
|
288
|
+
const source = bytes(rest[0]);
|
|
289
|
+
return new Uint8Array(source).buffer;
|
|
290
|
+
}
|
|
291
|
+
case "V": {
|
|
292
|
+
const source = bytes(rest[0]);
|
|
293
|
+
return new DataView(new Uint8Array(source).buffer);
|
|
294
|
+
}
|
|
295
|
+
case "T": {
|
|
296
|
+
return typedArray(rest[0], new Uint8Array(bytes(rest[1])).buffer);
|
|
297
|
+
}
|
|
298
|
+
case "O": {
|
|
299
|
+
const entries = [];
|
|
300
|
+
for (let index = 0; index < rest.length; index += 2) {
|
|
301
|
+
const key = rest[index];
|
|
302
|
+
if (typeof key !== "string")
|
|
303
|
+
throw new TypeError("object key is not a string");
|
|
304
|
+
entries.push([key, decodeValue(rest[index + 1])]);
|
|
305
|
+
}
|
|
306
|
+
// Own data properties, as structured clone makes them: a `__proto__` key stays a key.
|
|
307
|
+
return Object.fromEntries(entries);
|
|
308
|
+
}
|
|
309
|
+
default:
|
|
310
|
+
throw new TypeError(`unknown tag ${tag}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const utf8 = new TextEncoder();
|
|
314
|
+
const encodePayload = (value) => utf8.encode(JSON.stringify(encodeValue(value, new Set())));
|
|
315
|
+
const decodePayload = (bytes) => decodeValue(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
|
|
316
|
+
// ── framing ────────────────────────────────────────────────────────────────────────────────
|
|
317
|
+
/** magic (4) + format version (4) + payload length (4) + SHA-256 of the payload (32), then the payload. */
|
|
318
|
+
function frame(magic, payload, format) {
|
|
319
|
+
const out = Buffer.alloc(HEADER + payload.byteLength);
|
|
320
|
+
out.writeUInt32BE(magic, 0);
|
|
321
|
+
out.writeUInt32BE(format, 4);
|
|
322
|
+
out.writeUInt32BE(payload.byteLength, 8);
|
|
323
|
+
Buffer.from(sha256HexOfBytes(payload), "hex").copy(out, 12);
|
|
324
|
+
Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength).copy(out, HEADER);
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
/** A whole, checksummed frame starting at `at`, or `null` when the bytes there are not one. */
|
|
328
|
+
function frameAt(bytes, at, magic) {
|
|
329
|
+
if (bytes.byteLength - at < HEADER || bytes.readUInt32BE(at) !== magic)
|
|
330
|
+
return null;
|
|
331
|
+
const length = bytes.readUInt32BE(at + 8);
|
|
332
|
+
if (length > MAXIMUM_RECORD_BYTES)
|
|
333
|
+
return null;
|
|
334
|
+
const end = at + HEADER + length;
|
|
335
|
+
if (end > bytes.byteLength)
|
|
336
|
+
return null;
|
|
337
|
+
const payload = bytes.subarray(at + HEADER, end);
|
|
338
|
+
if (!Buffer.from(sha256HexOfBytes(payload), "hex").equals(bytes.subarray(at + 12, at + HEADER)))
|
|
339
|
+
return null;
|
|
340
|
+
return { format: bytes.readUInt32BE(at + 4), payload, end };
|
|
341
|
+
}
|
|
342
|
+
/** Whether a whole, checksummed record begins anywhere at or after `from`. */
|
|
343
|
+
function wholeRecordFrom(bytes, from) {
|
|
344
|
+
for (let at = bytes.indexOf(RECORD_MAGIC_BYTES, from); at !== -1; at = bytes.indexOf(RECORD_MAGIC_BYTES, at + 1)) {
|
|
345
|
+
if (frameAt(bytes, at, RECORD_MAGIC))
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Split a log into whole records. Every record but the last was fsynced before the next was
|
|
352
|
+
* written, so only the last append can be incomplete, and its pages may have reached the disk
|
|
353
|
+
* in any order: cut short, zero-filled, zero at the front with data behind, or with a header
|
|
354
|
+
* whose length was never written. So the bytes after the last whole record are a torn tail
|
|
355
|
+
* exactly when no whole, checksummed record begins anywhere inside them; when one does, a record
|
|
356
|
+
* the store acknowledged is damaged, and that is corruption.
|
|
357
|
+
*/
|
|
358
|
+
function scanLog(bytes, file) {
|
|
359
|
+
const records = [];
|
|
360
|
+
let at = 0;
|
|
361
|
+
while (at < bytes.byteLength) {
|
|
362
|
+
const found = frameAt(bytes, at, RECORD_MAGIC);
|
|
363
|
+
if (!found) {
|
|
364
|
+
if (wholeRecordFrom(bytes, at + 1))
|
|
365
|
+
throw new FileJournalCorruptError(file, "a damaged record is followed by a whole one", at);
|
|
366
|
+
return { records, validBytes: at, totalBytes: bytes.byteLength };
|
|
367
|
+
}
|
|
368
|
+
if (!readableFormat(found.format))
|
|
369
|
+
throw new FileJournalFormatError(file, found.format, at);
|
|
370
|
+
let record;
|
|
371
|
+
try {
|
|
372
|
+
record = decodePayload(found.payload);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
throw new FileJournalCorruptError(file, "record checksum matches but its payload does not decode", at);
|
|
376
|
+
}
|
|
377
|
+
if (!record || !Number.isSafeInteger(record.txn) || !Array.isArray(record.changes))
|
|
378
|
+
throw new FileJournalCorruptError(file, "record has no transaction number or changes", at);
|
|
379
|
+
if ((found.format >= 2 ? record.format !== found.format : record.format !== undefined && record.format !== 1))
|
|
380
|
+
throw new FileJournalCorruptError(file, "record format does not match its frame", at);
|
|
381
|
+
validateIntentChanges(record.changes, found.format, file);
|
|
382
|
+
record.format = found.format;
|
|
383
|
+
records.push(record);
|
|
384
|
+
at = found.end;
|
|
385
|
+
}
|
|
386
|
+
return { records, validBytes: at, totalBytes: bytes.byteLength };
|
|
387
|
+
}
|
|
388
|
+
function decodeSnapshot(bytes, file) {
|
|
389
|
+
if (bytes.byteLength < HEADER || bytes.readUInt32BE(0) !== SNAPSHOT_MAGIC)
|
|
390
|
+
throw new FileJournalCorruptError(file, "not a store snapshot");
|
|
391
|
+
const found = frameAt(bytes, 0, SNAPSHOT_MAGIC);
|
|
392
|
+
if (!found || found.end !== bytes.byteLength)
|
|
393
|
+
throw new FileJournalCorruptError(file, "snapshot checksum or length does not match");
|
|
394
|
+
if (!readableFormat(found.format))
|
|
395
|
+
throw new FileJournalFormatError(file, found.format);
|
|
396
|
+
let decoded;
|
|
397
|
+
try {
|
|
398
|
+
decoded = decodePayload(found.payload);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
throw new FileJournalCorruptError(file, "snapshot checksum matches but its payload does not decode");
|
|
402
|
+
}
|
|
403
|
+
if (!decoded || decoded.format !== found.format || !Number.isSafeInteger(decoded.txn) || !Number.isSafeInteger(decoded.sequence))
|
|
404
|
+
throw new FileJournalCorruptError(file, "snapshot has no transaction number");
|
|
405
|
+
if (!Array.isArray(decoded.intents))
|
|
406
|
+
throw new FileJournalCorruptError(file, "snapshot has no intent records");
|
|
407
|
+
validateIntentChanges(decoded.intents.map(([key, value]) => ({ family: "intent", key, value })), found.format, file);
|
|
408
|
+
return decoded;
|
|
409
|
+
}
|
|
410
|
+
const readableFormat = (format) => Number.isSafeInteger(format) && format >= 1 && format <= FILE_JOURNAL_FORMAT_VERSION;
|
|
411
|
+
/** The layout a blob intent needs: not-dispatched evidence first appeared in format 3. */
|
|
412
|
+
const blobIntentFormat = (row) => ("notDispatched" in row ? 3 : 2);
|
|
413
|
+
function validateIntentChanges(changes, format, file) {
|
|
414
|
+
for (const change of changes) {
|
|
415
|
+
if (change.family !== "intent" || change.value === null)
|
|
416
|
+
continue;
|
|
417
|
+
const row = change.value;
|
|
418
|
+
if (!("recordFamily" in row) && row.bodyReplace !== undefined) {
|
|
419
|
+
try {
|
|
420
|
+
captureNewDocumentIntent(row);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
throw new FileJournalCorruptError(file, "invalid document edit intent");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if ("recordFamily" in row) {
|
|
427
|
+
if (row.recordFamily !== "blob" || format < 2)
|
|
428
|
+
throw new FileJournalCorruptError(file, "blob intent is not allowed in this format");
|
|
429
|
+
if (blobIntentFormat(row) > format)
|
|
430
|
+
throw new FileJournalCorruptError(file, "blob not-dispatched evidence is not allowed in this format");
|
|
431
|
+
try {
|
|
432
|
+
captureBlobIntentRecord(row);
|
|
433
|
+
assertReceiptBound(row);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
throw new FileJournalCorruptError(file, "invalid blob intent payload");
|
|
437
|
+
}
|
|
438
|
+
if (row.requestId !== change.key)
|
|
439
|
+
throw new FileJournalCorruptError(file, "blob identity index mismatch");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function snapshotPayload(state, txn) {
|
|
444
|
+
return { format: state.format, txn, sequence: state.sequence, documents: [...state.documents], reserved: [...state.reserved], blobs: [...state.blobs], intents: [...state.intents], meta: [...state.meta] };
|
|
445
|
+
}
|
|
446
|
+
/** Terminal evidence must fit the space protected when its pending record was admitted. */
|
|
447
|
+
function assertReceiptBound(row) {
|
|
448
|
+
if (row.receipt !== undefined)
|
|
449
|
+
assertByteBound(encodePayload(row.receipt), BLOB_JOURNAL_BOUNDS.terminalReceiptBytes, "blob terminal receipt");
|
|
450
|
+
}
|
|
451
|
+
/** Derived from existing records only: no persisted reservation map or quota ledger. The
|
|
452
|
+
* mutable projection (including transaction-number width) is subtracted so claiming an
|
|
453
|
+
* attempt/state cannot consume its own reserve.
|
|
454
|
+
* The 4 KiB structural allowance covers the terminal record envelope/tagging and counter/time
|
|
455
|
+
* changes; the additional 16 KiB is aggregate encoded growth of ordinary baseline metadata.
|
|
456
|
+
* Arbitrary extra metadata growth still requires unreserved store capacity. */
|
|
457
|
+
function settlementReserve(state, txn) {
|
|
458
|
+
let reserved = 0;
|
|
459
|
+
for (const row of state.intents.values()) {
|
|
460
|
+
if (isDocumentIntent(row))
|
|
461
|
+
continue;
|
|
462
|
+
// All stored blob rows passed captureBlobIntentRecord and assertReceiptBound, including
|
|
463
|
+
// replay. Unknown/nonterminal evidence can never release this allocation.
|
|
464
|
+
if (row.receipt !== undefined && ["acknowledged", "refused", "conflict"].includes(row.state))
|
|
465
|
+
continue;
|
|
466
|
+
const mutableBytes = encodePayload({ state: row.state, attempts: row.attempts, updatedAt: row.updatedAt, transaction: txn, ...(row.notAppliedThrough === undefined ? {} : { notAppliedThrough: row.notAppliedThrough }), ...(row.deletionHold === undefined ? {} : { deletionHold: row.deletionHold }), ...(row.notDispatched === undefined ? {} : { notDispatched: row.notDispatched }) }).byteLength;
|
|
467
|
+
const remaining = BLOB_JOURNAL_BOUNDS.terminalReceiptBytes + BLOB_JOURNAL_BOUNDS.settlementStructureBytes + BLOB_JOURNAL_BOUNDS.baselineBytes - mutableBytes;
|
|
468
|
+
if (!Number.isSafeInteger(remaining) || remaining < 0 || !Number.isSafeInteger(reserved + remaining))
|
|
469
|
+
throw new RangeError("blob settlement reserve is not representable");
|
|
470
|
+
reserved += remaining;
|
|
471
|
+
}
|
|
472
|
+
return reserved;
|
|
473
|
+
}
|
|
474
|
+
function assertByteBound(payload, maximum, family) {
|
|
475
|
+
if (payload.byteLength > maximum)
|
|
476
|
+
throw new RangeError(`a ${family} of ${payload.byteLength} bytes exceeds the store's ${maximum} byte bound`);
|
|
477
|
+
}
|
|
478
|
+
// ── helpers shared with the other adapters' record handling ────────────────────────────────
|
|
479
|
+
function notFound(id) {
|
|
480
|
+
const err = new Error(`no concept document '${id}'`);
|
|
481
|
+
err.code = "ENOENT";
|
|
482
|
+
return err;
|
|
483
|
+
}
|
|
484
|
+
function reservedKey(dir, name) {
|
|
485
|
+
return dir === "" ? name : `${dir}/${name}`;
|
|
486
|
+
}
|
|
487
|
+
function firstString(...vals) {
|
|
488
|
+
for (const v of vals) {
|
|
489
|
+
if (typeof v === "string" && v.trim() !== "")
|
|
490
|
+
return v;
|
|
491
|
+
}
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
function sortedKeys(keys, prefix) {
|
|
495
|
+
const out = [...keys].filter((key) => !prefix || key.startsWith(prefix));
|
|
496
|
+
out.sort(compareStorageKeys);
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
const bySequence = (a, b) => a.sequence - b.sequence;
|
|
500
|
+
/** The okf edition declared by the bundle root's `index.md`; malformed metadata is no edition. */
|
|
501
|
+
function editionOf(index) {
|
|
502
|
+
if (!index)
|
|
503
|
+
return undefined;
|
|
504
|
+
try {
|
|
505
|
+
const value = parseLeadingFrontmatter(index.content, "index.md").okf_version;
|
|
506
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
if (error instanceof MalformedDocumentError)
|
|
510
|
+
return undefined;
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function headOf(id, record, intents, meta, edition) {
|
|
515
|
+
const head = { id, version: record.version, updatedBy: record.updatedBy, updatedAt: record.updatedAt, raw: record.raw, intents, meta };
|
|
516
|
+
let frontmatter;
|
|
517
|
+
try {
|
|
518
|
+
frontmatter = parseMarkdown(record.raw, pathFromConceptId(id), { okfVersion: edition }).frontmatter;
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
if (!(error instanceof MalformedDocumentError))
|
|
522
|
+
throw error;
|
|
523
|
+
return { ...head, frontmatter: null, malformed: error };
|
|
524
|
+
}
|
|
525
|
+
return { ...head, frontmatter };
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Make a directory entry durable. Windows cannot open a directory as a file handle to flush it
|
|
529
|
+
* (NTFS journals its own metadata), so there the open or flush refusal is expected and skipped;
|
|
530
|
+
* on every other host it propagates.
|
|
531
|
+
*/
|
|
532
|
+
async function syncDirectory(directory) {
|
|
533
|
+
let handle;
|
|
534
|
+
try {
|
|
535
|
+
handle = await fs.open(directory, "r");
|
|
536
|
+
await handle.sync();
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
const code = error.code;
|
|
540
|
+
if (!(process.platform === "win32" && (code === "EISDIR" || code === "EPERM" || code === "EACCES" || code === "EINVAL" || code === "ENOTSUP")))
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
finally {
|
|
544
|
+
await handle?.close();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/** Rename over `to`, retrying what the host classifies as a transient replacement conflict (a scanner holding the file open). */
|
|
548
|
+
async function renameOver(from, to, policy, waitMs, pollMs) {
|
|
549
|
+
const started = Date.now();
|
|
550
|
+
for (;;) {
|
|
551
|
+
try {
|
|
552
|
+
await fs.rename(from, to);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
if (!policy.isReplacementConflict(error) || Date.now() - started >= waitMs)
|
|
557
|
+
throw error;
|
|
558
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async function readIfPresent(file) {
|
|
563
|
+
try {
|
|
564
|
+
return await fs.readFile(file);
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
if (error.code === "ENOENT")
|
|
568
|
+
return null;
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async function writeAll(handle, bytes, position) {
|
|
573
|
+
let written = 0;
|
|
574
|
+
while (written < bytes.byteLength) {
|
|
575
|
+
const { bytesWritten } = await handle.write(bytes, written, bytes.byteLength - written, position + written);
|
|
576
|
+
if (bytesWritten <= 0)
|
|
577
|
+
throw new Error("the log accepted no bytes");
|
|
578
|
+
written += bytesWritten;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/** The log handle of a read-only open: nothing is opened, so closing it is all it does. */
|
|
582
|
+
const READ_ONLY_LOG = Object.freeze({
|
|
583
|
+
write: () => Promise.reject(new FileJournalUnavailableError("the store is open read-only")),
|
|
584
|
+
sync: () => Promise.reject(new FileJournalUnavailableError("the store is open read-only")),
|
|
585
|
+
truncate: () => Promise.reject(new FileJournalUnavailableError("the store is open read-only")),
|
|
586
|
+
close: () => Promise.resolve(),
|
|
587
|
+
});
|
|
588
|
+
/**
|
|
589
|
+
* The Node working copy's persistent store: one directory, one exclusive lock, one log. Open it
|
|
590
|
+
* with {@link FileJournaledBackend.open}; {@link close} releases the lock.
|
|
591
|
+
*/
|
|
592
|
+
export class FileJournaledBackend {
|
|
593
|
+
journalSnapshotCas = true;
|
|
594
|
+
/** A resolved mutation's log record, and every directory entry the store created, were fsynced. */
|
|
595
|
+
durability = "fsync";
|
|
596
|
+
directory;
|
|
597
|
+
#state;
|
|
598
|
+
#log;
|
|
599
|
+
#release;
|
|
600
|
+
#policy;
|
|
601
|
+
#txn;
|
|
602
|
+
#maximumBytes = MAXIMUM_RECORD_BYTES;
|
|
603
|
+
#logBytes;
|
|
604
|
+
#queue = Promise.resolve();
|
|
605
|
+
/** Blob claims this handle made, by request id, at their attempt count. The exclusive lock means
|
|
606
|
+
* any `in_flight` row found at open was claimed by a process that has since died. */
|
|
607
|
+
#claims = new Map();
|
|
608
|
+
#unavailable = null;
|
|
609
|
+
#closing = null;
|
|
610
|
+
#readOnly = false;
|
|
611
|
+
constructor(directory, state, log, release, txn, logBytes, policy) {
|
|
612
|
+
this.directory = directory;
|
|
613
|
+
this.#policy = policy;
|
|
614
|
+
this.#state = state;
|
|
615
|
+
this.#log = log;
|
|
616
|
+
this.#release = release;
|
|
617
|
+
this.#txn = txn;
|
|
618
|
+
this.#logBytes = logBytes;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Claim the directory's lock, load the snapshot, replay the log, and truncate a torn tail. A
|
|
622
|
+
* corrupt snapshot or a corrupt record before the log's end refuses the open and changes no
|
|
623
|
+
* file. Compacts when the log is larger than `compactAfterBytes`.
|
|
624
|
+
*/
|
|
625
|
+
static open(options) {
|
|
626
|
+
return _a.#open(options, (file) => fs.open(file, "r+"));
|
|
627
|
+
}
|
|
628
|
+
/** @internal The test seam: open with a substitute log handle. Reachable only through {@link OPEN_LOG}. */
|
|
629
|
+
static [OPEN_LOG](options, openLog) {
|
|
630
|
+
return _a.#open(options, openLog);
|
|
631
|
+
}
|
|
632
|
+
static async #open(options, openLog) {
|
|
633
|
+
if (typeof options.directory !== "string" || options.directory.trim() === "")
|
|
634
|
+
throw new TypeError("FileJournaledBackend requires a directory.");
|
|
635
|
+
const directory = path.resolve(options.directory);
|
|
636
|
+
const compactAfter = options.compactAfterBytes ?? DEFAULT_COMPACT_AFTER_BYTES;
|
|
637
|
+
if (!(compactAfter >= 0))
|
|
638
|
+
throw new TypeError("compactAfterBytes must be a non-negative number");
|
|
639
|
+
let created = false;
|
|
640
|
+
try {
|
|
641
|
+
const stat = await fs.stat(directory);
|
|
642
|
+
if (!stat.isDirectory())
|
|
643
|
+
throw new Error(`'${directory}' is not a directory`);
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
if (error.code !== "ENOENT" || options.readOnly)
|
|
647
|
+
throw error;
|
|
648
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
649
|
+
created = true;
|
|
650
|
+
}
|
|
651
|
+
const policy = captureFilesystemHostPolicy(options.hostPolicy);
|
|
652
|
+
const release = await acquireFilesystemMutationLock(directory, { ...options.lock, hostPolicy: policy });
|
|
653
|
+
let log;
|
|
654
|
+
try {
|
|
655
|
+
if (created)
|
|
656
|
+
await syncDirectory(path.dirname(directory));
|
|
657
|
+
const logFile = path.join(directory, FILE_JOURNAL_LOG);
|
|
658
|
+
const snapshotFile = path.join(directory, FILE_JOURNAL_SNAPSHOT);
|
|
659
|
+
// Read and validate everything before changing any file, so a refusal leaves the evidence as it was.
|
|
660
|
+
const snapshotBytes = await readIfPresent(snapshotFile);
|
|
661
|
+
const snapshot = snapshotBytes ? decodeSnapshot(snapshotBytes, snapshotFile) : null;
|
|
662
|
+
const logRead = await readIfPresent(logFile);
|
|
663
|
+
const logBytes = logRead ?? Buffer.alloc(0);
|
|
664
|
+
const scanned = scanLog(logBytes, logFile);
|
|
665
|
+
const maximumBytes = options[STORE_BYTE_LIMIT] ?? MAXIMUM_RECORD_BYTES;
|
|
666
|
+
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1 || maximumBytes > MAXIMUM_RECORD_BYTES)
|
|
667
|
+
throw new TypeError("invalid store byte bound");
|
|
668
|
+
const state = emptyState();
|
|
669
|
+
let txn = 0;
|
|
670
|
+
if (snapshot) {
|
|
671
|
+
state.format = snapshot.format;
|
|
672
|
+
state.documents = new Map(snapshot.documents);
|
|
673
|
+
state.reserved = new Map(snapshot.reserved);
|
|
674
|
+
state.blobs = new Map(snapshot.blobs);
|
|
675
|
+
state.intents = new Map(snapshot.intents);
|
|
676
|
+
state.meta = new Map(snapshot.meta);
|
|
677
|
+
state.sequence = snapshot.sequence;
|
|
678
|
+
txn = snapshot.txn;
|
|
679
|
+
}
|
|
680
|
+
for (const record of scanned.records) {
|
|
681
|
+
// Records the snapshot already covers are the log compaction had not yet truncated.
|
|
682
|
+
if (record.txn <= txn)
|
|
683
|
+
continue;
|
|
684
|
+
if (record.txn !== txn + 1)
|
|
685
|
+
throw new FileJournalCorruptError(logFile, `transaction ${record.txn} follows ${txn}`);
|
|
686
|
+
if (record.format < state.format)
|
|
687
|
+
throw new FileJournalCorruptError(logFile, "store format regressed");
|
|
688
|
+
state.format = record.format;
|
|
689
|
+
applyChanges(state, record.changes);
|
|
690
|
+
txn = record.txn;
|
|
691
|
+
}
|
|
692
|
+
if (options.readOnly) {
|
|
693
|
+
const backend = new _a(directory, state, READ_ONLY_LOG, release, txn, scanned.validBytes, policy);
|
|
694
|
+
backend.#maximumBytes = maximumBytes;
|
|
695
|
+
backend.#readOnly = true;
|
|
696
|
+
return backend;
|
|
697
|
+
}
|
|
698
|
+
await fs.rm(path.join(directory, SNAPSHOT_TEMP), { force: true });
|
|
699
|
+
if (logRead === null) {
|
|
700
|
+
await (await fs.open(logFile, "a", 0o600)).close();
|
|
701
|
+
await syncDirectory(directory);
|
|
702
|
+
}
|
|
703
|
+
log = await openLog(logFile);
|
|
704
|
+
if (scanned.validBytes !== scanned.totalBytes) {
|
|
705
|
+
await log.truncate(scanned.validBytes);
|
|
706
|
+
await log.sync();
|
|
707
|
+
}
|
|
708
|
+
const backend = new _a(directory, state, log, release, txn, scanned.validBytes, policy);
|
|
709
|
+
backend.#maximumBytes = maximumBytes;
|
|
710
|
+
if (scanned.validBytes > compactAfter)
|
|
711
|
+
await backend.compact();
|
|
712
|
+
return backend;
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
await log?.close().catch(() => { });
|
|
716
|
+
await release().catch(() => { });
|
|
717
|
+
throw error;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
capabilities() {
|
|
721
|
+
return { history: false, enforced_cas: true, blobs: true, projections: false, backlinks: false };
|
|
722
|
+
}
|
|
723
|
+
/** The number of the last committed transaction; it grows by one per mutation that wrote. */
|
|
724
|
+
get transaction() {
|
|
725
|
+
return this.#txn;
|
|
726
|
+
}
|
|
727
|
+
/** Let queued mutations finish, close the log, and release the directory's lock. Later calls reject. */
|
|
728
|
+
close() {
|
|
729
|
+
this.#closing ??= (async () => {
|
|
730
|
+
await this.#queue;
|
|
731
|
+
try {
|
|
732
|
+
await this.#log.close();
|
|
733
|
+
}
|
|
734
|
+
finally {
|
|
735
|
+
await this.#release();
|
|
736
|
+
}
|
|
737
|
+
})();
|
|
738
|
+
return this.#closing;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Write the whole index as the snapshot and empty the log: a temporary file, fsynced, renamed
|
|
742
|
+
* over the snapshot, the directory fsynced, then the log truncated. Serialized with mutations,
|
|
743
|
+
* under the directory lock the store holds.
|
|
744
|
+
*/
|
|
745
|
+
compact() {
|
|
746
|
+
return this.#serial(async () => {
|
|
747
|
+
const state = this.#state;
|
|
748
|
+
const encoded = encodePayload(snapshotPayload(state, this.#txn));
|
|
749
|
+
assertByteBound(encoded, this.#maximumBytes, "snapshot");
|
|
750
|
+
const bytes = frame(SNAPSHOT_MAGIC, encoded, state.format);
|
|
751
|
+
const temp = path.join(this.directory, SNAPSHOT_TEMP);
|
|
752
|
+
const handle = await fs.open(temp, "w", 0o600);
|
|
753
|
+
try {
|
|
754
|
+
await writeAll(handle, bytes, 0);
|
|
755
|
+
await handle.sync();
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
await handle.close().catch(() => { });
|
|
759
|
+
await fs.rm(temp, { force: true }).catch(() => { });
|
|
760
|
+
throw error;
|
|
761
|
+
}
|
|
762
|
+
await handle.close();
|
|
763
|
+
await renameOver(temp, path.join(this.directory, FILE_JOURNAL_SNAPSHOT), this.#policy, 5_000, 25);
|
|
764
|
+
await syncDirectory(this.directory);
|
|
765
|
+
// From here the snapshot covers every record, so a failure to empty the log costs space, not
|
|
766
|
+
// state. Once the truncation lands, the next append goes to offset zero whether or not the
|
|
767
|
+
// fsync that follows succeeds, so no append can leave a hole before its record.
|
|
768
|
+
await this.#log.truncate(0);
|
|
769
|
+
this.#logBytes = 0;
|
|
770
|
+
await this.#log.sync();
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
// ── the one mutation path ───────────────────────────────────────────────────────────────
|
|
774
|
+
/** Run `fn` after every earlier mutation settles; mutations never interleave. */
|
|
775
|
+
#serial(fn) {
|
|
776
|
+
if (this.#closing)
|
|
777
|
+
return Promise.reject(new FileJournalUnavailableError("the store is closed"));
|
|
778
|
+
if (this.#readOnly)
|
|
779
|
+
return Promise.reject(new FileJournalUnavailableError("the store is open read-only"));
|
|
780
|
+
const run = this.#queue.then(() => {
|
|
781
|
+
if (this.#unavailable)
|
|
782
|
+
throw this.#unavailable;
|
|
783
|
+
return fn();
|
|
784
|
+
});
|
|
785
|
+
this.#queue = run.catch(() => { });
|
|
786
|
+
return run;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Decide against the index, make the decision durable, then apply it. `decide` runs with no
|
|
790
|
+
* await between its checks and the append that follows, and nothing else mutates the index
|
|
791
|
+
* meanwhile, so its checks still hold when the record lands. A decision that changes nothing
|
|
792
|
+
* writes nothing. A failed append is removed from the log before the rejection; when removing
|
|
793
|
+
* it also fails, the store refuses every later call, because only a reopen can tell whether
|
|
794
|
+
* the record reached the disk.
|
|
795
|
+
*/
|
|
796
|
+
#mutate(decide) {
|
|
797
|
+
return this.#serial(async () => {
|
|
798
|
+
const { changes, result, format, afterCommit } = decide(this.#state);
|
|
799
|
+
if (changes.length === 0) {
|
|
800
|
+
afterCommit?.();
|
|
801
|
+
return result;
|
|
802
|
+
}
|
|
803
|
+
const nextFormat = Math.max(this.#state.format, format ?? 1);
|
|
804
|
+
const record = { ...(nextFormat >= 2 ? { format: nextFormat } : {}), txn: this.#txn + 1, changes };
|
|
805
|
+
const payload = encodePayload(record);
|
|
806
|
+
assertByteBound(payload, this.#maximumBytes, "transaction");
|
|
807
|
+
const projected = { ...this.#state, format: nextFormat, documents: new Map(this.#state.documents), reserved: new Map(this.#state.reserved), blobs: new Map(this.#state.blobs), intents: new Map(this.#state.intents), meta: new Map(this.#state.meta) };
|
|
808
|
+
applyChanges(projected, changes);
|
|
809
|
+
const snapshotBytes = encodePayload(snapshotPayload(projected, record.txn));
|
|
810
|
+
assertByteBound(snapshotBytes, this.#maximumBytes, "projected snapshot");
|
|
811
|
+
if (nextFormat >= 2) {
|
|
812
|
+
const reserved = settlementReserve(projected, record.txn);
|
|
813
|
+
if (snapshotBytes.byteLength + reserved > this.#maximumBytes)
|
|
814
|
+
throw new RangeError(`a projected snapshot of ${snapshotBytes.byteLength} bytes plus ${reserved} bytes of blob settlement reserve exceeds the store's ${this.#maximumBytes} byte bound`);
|
|
815
|
+
}
|
|
816
|
+
const bytes = frame(RECORD_MAGIC, payload, nextFormat);
|
|
817
|
+
const at = this.#logBytes;
|
|
818
|
+
try {
|
|
819
|
+
await writeAll(this.#log, bytes, at);
|
|
820
|
+
await this.#log.sync();
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
try {
|
|
824
|
+
await this.#log.truncate(at);
|
|
825
|
+
await this.#log.sync();
|
|
826
|
+
}
|
|
827
|
+
catch (cleanup) {
|
|
828
|
+
this.#unavailable = new FileJournalUnavailableError(`the store's last append failed and could not be removed; reopen the store to learn whether it committed (${String(cleanup?.message ?? cleanup)})`, { cause: error });
|
|
829
|
+
}
|
|
830
|
+
throw error;
|
|
831
|
+
}
|
|
832
|
+
this.#logBytes = at + bytes.byteLength;
|
|
833
|
+
this.#txn = record.txn;
|
|
834
|
+
this.#state.format = nextFormat;
|
|
835
|
+
applyChanges(this.#state, changes);
|
|
836
|
+
afterCommit?.();
|
|
837
|
+
return result;
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
#assertOpen() {
|
|
841
|
+
if (this.#closing)
|
|
842
|
+
throw new FileJournalUnavailableError("the store is closed");
|
|
843
|
+
if (this.#unavailable)
|
|
844
|
+
throw this.#unavailable;
|
|
845
|
+
}
|
|
846
|
+
// ── documents ─────────────────────────────────────────────────────────────────────────
|
|
847
|
+
async read(id) {
|
|
848
|
+
const [result] = await this.readMany([id]);
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
851
|
+
async readMany(ids) {
|
|
852
|
+
for (const id of ids)
|
|
853
|
+
assertSafeConceptId(id);
|
|
854
|
+
this.#assertOpen();
|
|
855
|
+
const records = ids.map((id) => this.#state.documents.get(id));
|
|
856
|
+
const edition = editionOf(this.#state.reserved.get(reservedKey("", "index.md")));
|
|
857
|
+
return ids.map((id, position) => {
|
|
858
|
+
const record = records[position];
|
|
859
|
+
if (!record)
|
|
860
|
+
throw notFound(id);
|
|
861
|
+
const { frontmatter, body } = parseMarkdown(record.raw, pathFromConceptId(id), { okfVersion: edition });
|
|
862
|
+
return { doc: { id, frontmatter, body }, version: record.version };
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
async write(id, doc, options = {}) {
|
|
866
|
+
assertSafeConceptId(id);
|
|
867
|
+
const raw = stringifyDoc(doc.frontmatter, doc.body ?? "");
|
|
868
|
+
const version = versionOfBytes(raw);
|
|
869
|
+
const updatedBy = options.actor?.trim() || defaultActor();
|
|
870
|
+
const expected = options.expectedVersion;
|
|
871
|
+
const updatedAt = new Date().toISOString();
|
|
872
|
+
return this.#mutate((state) => {
|
|
873
|
+
const current = state.documents.get(id)?.version ?? null;
|
|
874
|
+
if (expected !== undefined && expected !== current)
|
|
875
|
+
throw new VersionConflict(id, expected, current);
|
|
876
|
+
return { changes: [{ family: "document", key: id, value: { raw, version, updatedBy, updatedAt } }], result: version };
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
async delete(id, options = {}) {
|
|
880
|
+
assertSafeConceptId(id);
|
|
881
|
+
const expected = options.expectedVersion;
|
|
882
|
+
return this.#mutate((state) => {
|
|
883
|
+
const current = state.documents.get(id);
|
|
884
|
+
if (!current)
|
|
885
|
+
return { changes: [], result: false };
|
|
886
|
+
if (expected !== undefined && expected !== current.version)
|
|
887
|
+
throw new VersionConflict(id, expected, current.version);
|
|
888
|
+
return { changes: [{ family: "document", key: id, value: null }], result: true };
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
async exists(id) {
|
|
892
|
+
assertSafeConceptId(id);
|
|
893
|
+
this.#assertOpen();
|
|
894
|
+
return this.#state.documents.has(id);
|
|
895
|
+
}
|
|
896
|
+
async list(prefix) {
|
|
897
|
+
this.#assertOpen();
|
|
898
|
+
return sortedKeys(this.#state.documents.keys(), prefix);
|
|
899
|
+
}
|
|
900
|
+
async versions(id) {
|
|
901
|
+
assertSafeConceptId(id);
|
|
902
|
+
this.#assertOpen();
|
|
903
|
+
const record = this.#state.documents.get(id);
|
|
904
|
+
if (!record)
|
|
905
|
+
return [];
|
|
906
|
+
let frontmatter = {};
|
|
907
|
+
try {
|
|
908
|
+
frontmatter = parseMarkdown(record.raw, pathFromConceptId(id)).frontmatter;
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
if (!(error instanceof MalformedDocumentError))
|
|
912
|
+
throw error;
|
|
913
|
+
}
|
|
914
|
+
const actor = mutationActorFromFrontmatter(frontmatter) ?? record.updatedBy;
|
|
915
|
+
const timestamp = firstString(frontmatter.timestamp) ?? record.updatedAt;
|
|
916
|
+
return [{ version: record.version, actor, timestamp }];
|
|
917
|
+
}
|
|
918
|
+
// ── reserved files ─────────────────────────────────────────────────────────────────────
|
|
919
|
+
async readReserved(dir, name) {
|
|
920
|
+
assertSafeReservedDir(dir);
|
|
921
|
+
assertSafeReservedFilename(name);
|
|
922
|
+
this.#assertOpen();
|
|
923
|
+
const record = this.#state.reserved.get(reservedKey(dir, name));
|
|
924
|
+
return record ? { content: record.content, version: record.version } : null;
|
|
925
|
+
}
|
|
926
|
+
async writeReserved(dir, name, content, options = {}) {
|
|
927
|
+
assertSafeReservedDir(dir);
|
|
928
|
+
assertSafeReservedFilename(name);
|
|
929
|
+
const key = reservedKey(dir, name);
|
|
930
|
+
const version = versionOfBytes(content);
|
|
931
|
+
const expected = options.expectedVersion;
|
|
932
|
+
return this.#mutate((state) => {
|
|
933
|
+
const current = state.reserved.get(key)?.version ?? null;
|
|
934
|
+
if (expected !== undefined && expected !== current)
|
|
935
|
+
throw new VersionConflict(key, expected, current);
|
|
936
|
+
return { changes: [{ family: "reserved", key, value: { content, version } }], result: version };
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
// ── blobs ──────────────────────────────────────────────────────────────────────────────
|
|
940
|
+
async readBlob(key) {
|
|
941
|
+
assertSafeBlobKey(key);
|
|
942
|
+
this.#assertOpen();
|
|
943
|
+
const record = this.#state.blobs.get(key);
|
|
944
|
+
return record ? { bytes: new Uint8Array(record.bytes), contentType: record.contentType, version: record.version } : null;
|
|
945
|
+
}
|
|
946
|
+
async writeBlob(key, bytes, contentType, options = {}) {
|
|
947
|
+
assertSafeBlobKey(key);
|
|
948
|
+
const version = blobVersion(bytes);
|
|
949
|
+
const resolvedType = resolveContentType(key, contentType);
|
|
950
|
+
const stored = new Uint8Array(bytes);
|
|
951
|
+
const expected = options.expectedVersion;
|
|
952
|
+
return this.#mutate((state) => {
|
|
953
|
+
const current = state.blobs.get(key);
|
|
954
|
+
if (expected !== undefined && expected !== (current?.version ?? null))
|
|
955
|
+
throw new VersionConflict(key, expected, current?.version ?? null);
|
|
956
|
+
if (current && current.version === version && current.contentType === resolvedType)
|
|
957
|
+
return { changes: [], result: version };
|
|
958
|
+
return { changes: [{ family: "blob", key, value: { bytes: stored, contentType: resolvedType, version } }], result: version };
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
async deleteBlob(key, options = {}) {
|
|
962
|
+
assertSafeBlobKey(key);
|
|
963
|
+
const expected = options.expectedVersion;
|
|
964
|
+
return this.#mutate((state) => {
|
|
965
|
+
const current = state.blobs.get(key);
|
|
966
|
+
if (!current)
|
|
967
|
+
return { changes: [], result: false };
|
|
968
|
+
if (expected !== undefined && expected !== current.version)
|
|
969
|
+
throw new VersionConflict(key, expected, current.version);
|
|
970
|
+
return { changes: [{ family: "blob", key, value: null }], result: true };
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
async existsBlob(key) {
|
|
974
|
+
assertSafeBlobKey(key);
|
|
975
|
+
this.#assertOpen();
|
|
976
|
+
return this.#state.blobs.has(key);
|
|
977
|
+
}
|
|
978
|
+
async listBlobs(prefix) {
|
|
979
|
+
this.#assertOpen();
|
|
980
|
+
return sortedKeys(this.#state.blobs.keys(), prefix);
|
|
981
|
+
}
|
|
982
|
+
// ── the journal seam ──────────────────────────────────────────────────────────────────
|
|
983
|
+
#checkGuard(state, guard) {
|
|
984
|
+
if (guard === undefined)
|
|
985
|
+
return;
|
|
986
|
+
const document = state.documents.get(guard.target);
|
|
987
|
+
assertJournalGuard(guard, {
|
|
988
|
+
target: guard.target,
|
|
989
|
+
document: document ? { version: document.version, raw: document.raw } : null,
|
|
990
|
+
intents: documentIntents(state).filter((row) => row.target === guard.target),
|
|
991
|
+
meta: guard.meta.map((row) => ({ key: row.key, expected: state.meta.has(row.key) ? { present: true, value: state.meta.get(row.key) } : { present: false } })),
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
static #holder(state, id) {
|
|
995
|
+
return documentIntents(state).sort(bySequence).find((row) => row.target === id && row.state !== "acknowledged");
|
|
996
|
+
}
|
|
997
|
+
static #metaChanges(puts, removals = []) {
|
|
998
|
+
return [
|
|
999
|
+
...puts.map((row) => ({ family: "meta", key: row.key, present: true, value: structuredClone(row.value) })),
|
|
1000
|
+
...removals.map((key) => ({ family: "meta", key, present: false })),
|
|
1001
|
+
];
|
|
1002
|
+
}
|
|
1003
|
+
async writeJournaled(id, doc, options = {}) {
|
|
1004
|
+
// Everything the caller handed over is captured before the first await, so a caller that
|
|
1005
|
+
// mutates its inputs while the write is queued cannot change what is written.
|
|
1006
|
+
const snapshotGuard = captureJournalGuardOption(options, id);
|
|
1007
|
+
if (snapshotGuard !== undefined) {
|
|
1008
|
+
options = captureJournalWriteOptions(options);
|
|
1009
|
+
doc = captureJournalValue(doc);
|
|
1010
|
+
}
|
|
1011
|
+
assertSafeConceptId(id);
|
|
1012
|
+
assertJournalResolutionOptions(id, options);
|
|
1013
|
+
if (options.intent)
|
|
1014
|
+
options = { ...options, intent: captureNewDocumentIntent(options.intent) };
|
|
1015
|
+
if (snapshotGuard && (doc.id !== id || (options.intent && options.intent.target !== id)))
|
|
1016
|
+
throw new JournalGuardConflict(id);
|
|
1017
|
+
const raw = stringifyDoc(doc.frontmatter, doc.body ?? "");
|
|
1018
|
+
const version = versionOfBytes(raw);
|
|
1019
|
+
const producedMeta = typeof options.meta === "function" ? options.meta({ version, raw }) : options.meta ?? [];
|
|
1020
|
+
assertJournalMetaChanges(snapshotGuard, producedMeta, options.removeMeta);
|
|
1021
|
+
const meta = structuredClone(producedMeta);
|
|
1022
|
+
const removeMeta = options.removeMeta ? [...options.removeMeta] : [];
|
|
1023
|
+
const intent = options.intent ? structuredClone(options.intent) : undefined;
|
|
1024
|
+
if (intent && "recordFamily" in intent)
|
|
1025
|
+
throw new JournalGuardConflict(id);
|
|
1026
|
+
const supersede = options.supersede ? { ...options.supersede } : undefined;
|
|
1027
|
+
const resolveIntents = options.resolveIntents ? structuredClone(options.resolveIntents) : undefined;
|
|
1028
|
+
const { requireSettled } = options;
|
|
1029
|
+
const expected = options.expectedVersion;
|
|
1030
|
+
const updatedBy = options.actor?.trim() || defaultActor();
|
|
1031
|
+
const now = new Date().toISOString();
|
|
1032
|
+
return this.#mutate((state) => {
|
|
1033
|
+
this.#checkGuard(state, snapshotGuard);
|
|
1034
|
+
assertJournalIntentChanges(snapshotGuard, intent ? state.intents.get(intent.requestId) : undefined, supersede ? state.intents.get(supersede.requestId) : undefined);
|
|
1035
|
+
if (intent && state.intents.has(intent.requestId))
|
|
1036
|
+
throw new JournalSnapshotConflict(id);
|
|
1037
|
+
if (resolveIntents)
|
|
1038
|
+
assertJournalSnapshot(id, resolveIntents.expected, documentIntents(state), intent?.requestId);
|
|
1039
|
+
if (requireSettled) {
|
|
1040
|
+
const holder = _a.#holder(state, id);
|
|
1041
|
+
if (holder)
|
|
1042
|
+
throw new IntentHoldConflict(id, holder.requestId, holder.state);
|
|
1043
|
+
}
|
|
1044
|
+
const current = state.documents.get(id)?.version ?? null;
|
|
1045
|
+
if (expected !== undefined && expected !== current)
|
|
1046
|
+
throw new VersionConflict(id, expected, current);
|
|
1047
|
+
if (supersede) {
|
|
1048
|
+
const existing = documentIntent(state, supersede.requestId);
|
|
1049
|
+
if (!existing || existing.state !== supersede.expectedState || existing.attempts !== supersede.expectedAttempts)
|
|
1050
|
+
throw new IntentStateConflict(supersede.requestId, supersede.expectedState, existing?.state ?? null);
|
|
1051
|
+
}
|
|
1052
|
+
const changes = [{ family: "document", key: id, value: { raw, version, updatedBy, updatedAt: now } }];
|
|
1053
|
+
if (supersede)
|
|
1054
|
+
changes.push({ family: "intent", key: supersede.requestId, value: null });
|
|
1055
|
+
for (const row of resolveIntents?.expected ?? [])
|
|
1056
|
+
changes.push({ family: "intent", key: row.requestId, value: null });
|
|
1057
|
+
let record = null;
|
|
1058
|
+
if (intent) {
|
|
1059
|
+
const sequence = state.sequence + 1;
|
|
1060
|
+
record = { ...intent, local: version, content: raw, sequence, attempts: 0, state: "pending", updatedAt: now };
|
|
1061
|
+
changes.push({ family: "sequence", value: sequence }, { family: "intent", key: record.requestId, value: structuredClone(record) });
|
|
1062
|
+
}
|
|
1063
|
+
changes.push(..._a.#metaChanges(meta, removeMeta));
|
|
1064
|
+
return { changes, result: { version, raw, intent: record } };
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
async deleteJournaled(id, options = {}) {
|
|
1068
|
+
options = captureJournalDeleteOptions(id, options);
|
|
1069
|
+
assertSafeConceptId(id);
|
|
1070
|
+
assertJournalResolutionOptions(id, options);
|
|
1071
|
+
assertDeletionIntent(id, options);
|
|
1072
|
+
const captured = structuredClone({ meta: options.meta ?? [], removeMeta: [...(options.removeMeta ?? [])], onHeld: options.onHeld, resolveIntents: options.resolveIntents, intent: options.intent, supersede: options.supersede });
|
|
1073
|
+
const { guard, requireSettled } = options;
|
|
1074
|
+
if (captured.intent && "recordFamily" in captured.intent)
|
|
1075
|
+
throw new JournalGuardConflict(id);
|
|
1076
|
+
const expected = options.expectedVersion;
|
|
1077
|
+
const now = new Date().toISOString();
|
|
1078
|
+
return this.#mutate((state) => {
|
|
1079
|
+
this.#checkGuard(state, guard);
|
|
1080
|
+
if (captured.resolveIntents)
|
|
1081
|
+
assertJournalSnapshot(id, captured.resolveIntents.expected, documentIntents(state), captured.intent?.requestId);
|
|
1082
|
+
if (captured.intent && state.intents.has(captured.intent.requestId))
|
|
1083
|
+
throw new JournalSnapshotConflict(id);
|
|
1084
|
+
if (requireSettled) {
|
|
1085
|
+
const holder = _a.#holder(state, id);
|
|
1086
|
+
if (holder) {
|
|
1087
|
+
if (!captured.onHeld)
|
|
1088
|
+
throw new IntentHoldConflict(id, holder.requestId, holder.state);
|
|
1089
|
+
return { changes: _a.#metaChanges(captured.onHeld.meta), result: { outcome: "held", requestId: holder.requestId, state: holder.state } };
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const current = state.documents.get(id)?.version ?? null;
|
|
1093
|
+
if ((current !== null || captured.resolveIntents) && expected !== undefined && expected !== current)
|
|
1094
|
+
throw new VersionConflict(id, expected, current);
|
|
1095
|
+
const { supersede } = captured;
|
|
1096
|
+
if (supersede) {
|
|
1097
|
+
const existing = documentIntent(state, supersede.requestId);
|
|
1098
|
+
if (!existing || existing.target !== id || existing.state !== supersede.expectedState || existing.attempts !== supersede.expectedAttempts)
|
|
1099
|
+
throw new IntentStateConflict(supersede.requestId, supersede.expectedState, existing?.state ?? null);
|
|
1100
|
+
}
|
|
1101
|
+
const changes = [];
|
|
1102
|
+
if (current !== null)
|
|
1103
|
+
changes.push({ family: "document", key: id, value: null });
|
|
1104
|
+
if (supersede)
|
|
1105
|
+
changes.push({ family: "intent", key: supersede.requestId, value: null });
|
|
1106
|
+
for (const row of captured.resolveIntents?.expected ?? [])
|
|
1107
|
+
changes.push({ family: "intent", key: row.requestId, value: null });
|
|
1108
|
+
let record;
|
|
1109
|
+
if (captured.intent) {
|
|
1110
|
+
const sequence = state.sequence + 1;
|
|
1111
|
+
record = deletionIntentRecord(captured.intent, sequence, now);
|
|
1112
|
+
changes.push({ family: "sequence", value: sequence }, { family: "intent", key: record.requestId, value: structuredClone(record) });
|
|
1113
|
+
}
|
|
1114
|
+
changes.push(..._a.#metaChanges(captured.meta, captured.removeMeta));
|
|
1115
|
+
return { changes, result: { outcome: current !== null ? "deleted" : "absent", ...(record ? { intent: record } : {}) } };
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
async readWithJournal(id, options = {}) {
|
|
1119
|
+
assertSafeConceptId(id);
|
|
1120
|
+
this.#assertOpen();
|
|
1121
|
+
const state = this.#state;
|
|
1122
|
+
const record = state.documents.get(id);
|
|
1123
|
+
const intents = documentIntents(state).filter((row) => row.target === id).sort(bySequence).map((row) => structuredClone(row));
|
|
1124
|
+
const meta = new Map();
|
|
1125
|
+
for (const key of options.meta ?? []) {
|
|
1126
|
+
if (state.meta.has(key))
|
|
1127
|
+
meta.set(key, structuredClone(state.meta.get(key)));
|
|
1128
|
+
}
|
|
1129
|
+
if (!record)
|
|
1130
|
+
return { document: null, raw: null, intents, meta };
|
|
1131
|
+
const { frontmatter, body } = parseMarkdown(record.raw, pathFromConceptId(id), { okfVersion: editionOf(state.reserved.get(reservedKey("", "index.md"))) });
|
|
1132
|
+
return { document: { doc: { id, frontmatter, body }, version: record.version }, raw: record.raw, intents, meta };
|
|
1133
|
+
}
|
|
1134
|
+
async readHeads(options = {}) {
|
|
1135
|
+
this.#assertOpen();
|
|
1136
|
+
const state = this.#state;
|
|
1137
|
+
const keysOf = options.meta ?? (() => []);
|
|
1138
|
+
const sharedKeys = [...new Set(options.shared ?? [])];
|
|
1139
|
+
const project = options.project ?? ((head) => head);
|
|
1140
|
+
// One synchronous pass: no mutation can apply between the first row and the last.
|
|
1141
|
+
const edition = editionOf(state.reserved.get(reservedKey("", "index.md")));
|
|
1142
|
+
const byTarget = new Map();
|
|
1143
|
+
for (const row of documentIntents(state)) {
|
|
1144
|
+
const list = byTarget.get(row.target);
|
|
1145
|
+
if (list)
|
|
1146
|
+
list.push(row);
|
|
1147
|
+
else
|
|
1148
|
+
byTarget.set(row.target, [row]);
|
|
1149
|
+
}
|
|
1150
|
+
const shared = new Map();
|
|
1151
|
+
for (const key of sharedKeys) {
|
|
1152
|
+
if (state.meta.has(key))
|
|
1153
|
+
shared.set(key, structuredClone(state.meta.get(key)));
|
|
1154
|
+
}
|
|
1155
|
+
const out = [];
|
|
1156
|
+
for (const id of sortedKeys(state.documents.keys())) {
|
|
1157
|
+
const record = state.documents.get(id);
|
|
1158
|
+
const intents = (byTarget.get(id) ?? []).sort(bySequence).map((row) => structuredClone(row));
|
|
1159
|
+
const meta = new Map(shared);
|
|
1160
|
+
for (const key of new Set(keysOf(id, intents))) {
|
|
1161
|
+
if (!sharedKeys.includes(key) && state.meta.has(key))
|
|
1162
|
+
meta.set(key, structuredClone(state.meta.get(key)));
|
|
1163
|
+
}
|
|
1164
|
+
out.push(project(headOf(id, record, intents, meta, edition)));
|
|
1165
|
+
}
|
|
1166
|
+
return out;
|
|
1167
|
+
}
|
|
1168
|
+
async listIntents(state) {
|
|
1169
|
+
this.#assertOpen();
|
|
1170
|
+
const wanted = state === undefined ? null : new Set(typeof state === "string" ? [state] : state);
|
|
1171
|
+
return documentIntents(this.#state).filter((row) => !wanted || wanted.has(row.state)).sort(bySequence).map((row) => structuredClone(row));
|
|
1172
|
+
}
|
|
1173
|
+
async readIntent(requestId) {
|
|
1174
|
+
this.#assertOpen();
|
|
1175
|
+
const row = documentIntent(this.#state, requestId);
|
|
1176
|
+
return row ? structuredClone(row) : undefined;
|
|
1177
|
+
}
|
|
1178
|
+
async updateIntent(requestId, expectedState, patch, options = {}) {
|
|
1179
|
+
({ patch, options } = captureIntentUpdate(patch, options));
|
|
1180
|
+
if ("recordFamily" in patch)
|
|
1181
|
+
throw new JournalGuardConflict(requestId);
|
|
1182
|
+
const now = new Date().toISOString();
|
|
1183
|
+
const raw = options.document ? stringifyDoc(options.document.frontmatter, options.document.body ?? "") : undefined;
|
|
1184
|
+
const replacement = raw === undefined ? undefined : { id: options.document.id, record: { raw, version: versionOfBytes(raw), updatedBy: defaultActor(), updatedAt: now } };
|
|
1185
|
+
const { guard } = options;
|
|
1186
|
+
const meta = options.meta ?? [];
|
|
1187
|
+
return this.#mutate((state) => {
|
|
1188
|
+
this.#checkGuard(state, guard);
|
|
1189
|
+
const current = documentIntent(state, requestId);
|
|
1190
|
+
if (!current || current.state !== expectedState)
|
|
1191
|
+
throw new IntentStateConflict(requestId, expectedState, current?.state ?? null);
|
|
1192
|
+
if (guard && current.target !== guard.target)
|
|
1193
|
+
throw new JournalGuardConflict(current.target);
|
|
1194
|
+
const next = { ...current, ...patch, requestId, sequence: current.sequence, updatedAt: now };
|
|
1195
|
+
const changes = [];
|
|
1196
|
+
if (replacement)
|
|
1197
|
+
changes.push({ family: "document", key: replacement.id, value: replacement.record });
|
|
1198
|
+
changes.push({ family: "intent", key: requestId, value: structuredClone(next) }, ..._a.#metaChanges(meta));
|
|
1199
|
+
return { changes, result: next };
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
async retireAcknowledged(target, options) {
|
|
1203
|
+
assertSafeConceptId(target);
|
|
1204
|
+
const { guard, requestIds, removeMeta } = captureRetireAcknowledged(target, options);
|
|
1205
|
+
await this.#mutate((state) => {
|
|
1206
|
+
this.#checkGuard(state, guard);
|
|
1207
|
+
const changes = requestIds.map((key) => ({ family: "intent", key, value: null }));
|
|
1208
|
+
return { changes: [...changes, ..._a.#metaChanges([], removeMeta ?? [])], result: undefined };
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
// ── optional ordinary-file journal (no filesystem/body mirror) ─────────────────────────
|
|
1212
|
+
/** The committed index as the pure blob transition owner reads it. */
|
|
1213
|
+
#blobView(state) {
|
|
1214
|
+
return {
|
|
1215
|
+
blobIntentsFor: key => [...state.intents.values()].filter((row) => !isDocumentIntent(row) && row.target === key),
|
|
1216
|
+
blobIntent: id => { const row = state.intents.get(id); return row && !isDocumentIntent(row) ? row : undefined; },
|
|
1217
|
+
intentState: id => state.intents.get(id)?.state ?? null,
|
|
1218
|
+
meta: key => state.meta.has(key) ? { present: true, value: state.meta.get(key) } : { present: false },
|
|
1219
|
+
sequence: state.sequence,
|
|
1220
|
+
claimedInThisSession: (id, attempts) => this.#claims.get(id) === attempts,
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
/** A decided blob verb as one mutation: its changes, the format they need, and the session claims. */
|
|
1224
|
+
#blobDecision(decision) {
|
|
1225
|
+
const formats = decision.intents.flatMap(({ value }) => (value === null ? [] : [blobIntentFormat(value)]));
|
|
1226
|
+
const format = formats.length === 0 ? undefined : Math.max(...formats);
|
|
1227
|
+
return {
|
|
1228
|
+
changes: _a.#blobChanges(decision),
|
|
1229
|
+
result: decision.result,
|
|
1230
|
+
...(format === undefined ? {} : { format }),
|
|
1231
|
+
afterCommit: () => {
|
|
1232
|
+
if (decision.session) {
|
|
1233
|
+
if (decision.session.claimed === null)
|
|
1234
|
+
this.#claims.delete(decision.session.requestId);
|
|
1235
|
+
else
|
|
1236
|
+
this.#claims.set(decision.session.requestId, decision.session.claimed);
|
|
1237
|
+
}
|
|
1238
|
+
for (const { requestId, value } of decision.intents)
|
|
1239
|
+
if (value === null)
|
|
1240
|
+
this.#claims.delete(requestId);
|
|
1241
|
+
},
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
/** A decided blob verb as one log record's changes. */
|
|
1245
|
+
static #blobChanges(decision) {
|
|
1246
|
+
return [
|
|
1247
|
+
...(decision.sequence === undefined ? [] : [{ family: "sequence", value: decision.sequence }]),
|
|
1248
|
+
...decision.intents.map(({ requestId, value }) => ({ family: "intent", key: requestId, value: value === null ? null : structuredClone(value) })),
|
|
1249
|
+
..._a.#metaChanges(decision.meta, decision.removeMeta),
|
|
1250
|
+
];
|
|
1251
|
+
}
|
|
1252
|
+
async readBlobWithJournal(key, options = {}) {
|
|
1253
|
+
assertBlobKey(key);
|
|
1254
|
+
this.#assertOpen();
|
|
1255
|
+
return readBlobJournal(this.#blobView(this.#state), key, options);
|
|
1256
|
+
}
|
|
1257
|
+
async writeBlobMeta(key, changes, options) {
|
|
1258
|
+
const captured = captureBlobMetaWrite(key, changes, options);
|
|
1259
|
+
await this.#mutate(state => this.#blobDecision(decideBlobMetaWrite(this.#blobView(state), captured)));
|
|
1260
|
+
}
|
|
1261
|
+
async admitPreparedBlob(prepared, options) {
|
|
1262
|
+
const captured = captureBlobAdmission(prepared, options);
|
|
1263
|
+
const now = new Date().toISOString();
|
|
1264
|
+
return this.#mutate(state => this.#blobDecision(decideBlobAdmission(this.#blobView(state), captured, now)));
|
|
1265
|
+
}
|
|
1266
|
+
async listBlobIntents(state) {
|
|
1267
|
+
this.#assertOpen();
|
|
1268
|
+
const wanted = state === undefined ? null : new Set(typeof state === "string" ? [state] : state);
|
|
1269
|
+
return [...this.#state.intents.values()].filter((row) => !isDocumentIntent(row) && (!wanted || wanted.has(row.state))).sort(bySequence).map(row => structuredClone(row));
|
|
1270
|
+
}
|
|
1271
|
+
async readBlobIntent(requestId) {
|
|
1272
|
+
this.#assertOpen();
|
|
1273
|
+
const row = this.#state.intents.get(requestId);
|
|
1274
|
+
return row && !isDocumentIntent(row) ? structuredClone(row) : undefined;
|
|
1275
|
+
}
|
|
1276
|
+
async updateBlobIntent(requestId, expectedState, patch, options) {
|
|
1277
|
+
const captured = captureBlobUpdate(requestId, expectedState, patch, options);
|
|
1278
|
+
const now = new Date().toISOString();
|
|
1279
|
+
return this.#mutate(state => this.#blobDecision(decideBlobUpdate(this.#blobView(state), captured, now, assertReceiptBound)));
|
|
1280
|
+
}
|
|
1281
|
+
async retireBlobIntents(key, requestIds, options) {
|
|
1282
|
+
const captured = captureBlobRetirement(key, requestIds, options);
|
|
1283
|
+
await this.#mutate(state => this.#blobDecision(decideBlobRetirement(this.#blobView(state), captured)));
|
|
1284
|
+
}
|
|
1285
|
+
async forgetBlobIntent(requestId, options) {
|
|
1286
|
+
const captured = captureBlobForget(requestId, options);
|
|
1287
|
+
await this.#mutate(state => this.#blobDecision(decideBlobForget(this.#blobView(state), captured)));
|
|
1288
|
+
}
|
|
1289
|
+
async readMeta(key) {
|
|
1290
|
+
this.#assertOpen();
|
|
1291
|
+
return this.#state.meta.has(key) ? structuredClone(this.#state.meta.get(key)) : undefined;
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* @internal Plant exact bytes as `id`'s stored serialization, bypassing the serializer, through
|
|
1295
|
+
* the log like any other write: the contract kit's malformed-record and edition rows. Reachable
|
|
1296
|
+
* only through {@link STORE_RAW}.
|
|
1297
|
+
*/
|
|
1298
|
+
async [STORE_RAW](id, raw) {
|
|
1299
|
+
assertSafeConceptId(id);
|
|
1300
|
+
const record = { raw, version: versionOfBytes(raw), updatedBy: defaultActor(), updatedAt: new Date().toISOString() };
|
|
1301
|
+
await this.#mutate(() => ({ changes: [{ family: "document", key: id, value: record }], result: undefined }));
|
|
1302
|
+
}
|
|
1303
|
+
async writeMeta(key, value, options = {}) {
|
|
1304
|
+
options = captureMetaWrite(options);
|
|
1305
|
+
const captured = options.expected !== undefined || options.requireEmptyJournal ? captureJournalValue(value) : structuredClone(value);
|
|
1306
|
+
await this.#mutate((state) => {
|
|
1307
|
+
assertMetaWrite(key, options, state.meta.has(key) ? { present: true, value: state.meta.get(key) } : { present: false }, [...state.intents.values()]);
|
|
1308
|
+
return { changes: [{ family: "meta", key, present: true, value: captured }], result: undefined };
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Every meta row whose key starts with `prefix` (all rows when it is empty), in code-unit key
|
|
1313
|
+
* order, read in one synchronous pass over the last committed transaction. Values are copies.
|
|
1314
|
+
*/
|
|
1315
|
+
async listMeta(prefix = "") {
|
|
1316
|
+
if (typeof prefix !== "string")
|
|
1317
|
+
throw new TypeError("a meta prefix is a string");
|
|
1318
|
+
this.#assertOpen();
|
|
1319
|
+
const meta = this.#state.meta;
|
|
1320
|
+
const keys = [...meta.keys()].filter((key) => key.startsWith(prefix)).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
1321
|
+
return keys.map((key) => ({ key, value: structuredClone(meta.get(key)) }));
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Remove one meta row, compare-and-swap: `expected` is the row's presence and value as the
|
|
1325
|
+
* caller last read it, compared in the same transaction as the removal. A mismatch is
|
|
1326
|
+
* `JournalGuardConflict` and writes nothing. It resolves true when it removed the row and false
|
|
1327
|
+
* when the row was absent as expected, which writes nothing either.
|
|
1328
|
+
*/
|
|
1329
|
+
async removeMeta(key, options) {
|
|
1330
|
+
if (typeof key !== "string")
|
|
1331
|
+
throw new TypeError("a meta key is a string");
|
|
1332
|
+
if (!options || typeof options !== "object" || options.expected === undefined)
|
|
1333
|
+
throw new JournalGuardConflict(key);
|
|
1334
|
+
const { expected } = captureMetaWrite({ expected: options.expected });
|
|
1335
|
+
return this.#mutate((state) => {
|
|
1336
|
+
const actual = state.meta.has(key) ? { present: true, value: state.meta.get(key) } : { present: false };
|
|
1337
|
+
assertMetaWrite(key, { expected }, actual, []);
|
|
1338
|
+
if (!actual.present)
|
|
1339
|
+
return { changes: [], result: false };
|
|
1340
|
+
return { changes: [{ family: "meta", key, present: false }], result: true };
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
_a = FileJournaledBackend;
|
|
1345
|
+
//# sourceMappingURL=file-journaled-backend.js.map
|