@aigne/afs-did-space 1.12.0-beta.5
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/LICENSE.md +26 -0
- package/README.md +129 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/_virtual/rolldown_runtime.cjs +29 -0
- package/dist/afs-metadata-context.cjs +20 -0
- package/dist/afs-metadata-context.d.cts +21 -0
- package/dist/afs-metadata-context.d.cts.map +1 -0
- package/dist/afs-metadata-context.d.mts +21 -0
- package/dist/afs-metadata-context.d.mts.map +1 -0
- package/dist/afs-metadata-context.mjs +18 -0
- package/dist/afs-metadata-context.mjs.map +1 -0
- package/dist/blocklet-hook.cjs +180 -0
- package/dist/blocklet-hook.d.cts +29 -0
- package/dist/blocklet-hook.d.cts.map +1 -0
- package/dist/blocklet-hook.d.mts +29 -0
- package/dist/blocklet-hook.d.mts.map +1 -0
- package/dist/blocklet-hook.mjs +178 -0
- package/dist/blocklet-hook.mjs.map +1 -0
- package/dist/changelog-schema.cjs +603 -0
- package/dist/changelog-schema.d.cts +395 -0
- package/dist/changelog-schema.d.cts.map +1 -0
- package/dist/changelog-schema.d.mts +395 -0
- package/dist/changelog-schema.d.mts.map +1 -0
- package/dist/changelog-schema.mjs +570 -0
- package/dist/changelog-schema.mjs.map +1 -0
- package/dist/changelog-write.cjs +80 -0
- package/dist/changelog-write.d.cts +70 -0
- package/dist/changelog-write.d.cts.map +1 -0
- package/dist/changelog-write.d.mts +70 -0
- package/dist/changelog-write.d.mts.map +1 -0
- package/dist/changelog-write.mjs +77 -0
- package/dist/changelog-write.mjs.map +1 -0
- package/dist/cid-byte-cache.cjs +126 -0
- package/dist/cid-byte-cache.d.cts +46 -0
- package/dist/cid-byte-cache.d.cts.map +1 -0
- package/dist/cid-byte-cache.d.mts +47 -0
- package/dist/cid-byte-cache.d.mts.map +1 -0
- package/dist/cid-byte-cache.mjs +125 -0
- package/dist/cid-byte-cache.mjs.map +1 -0
- package/dist/cid.cjs +68 -0
- package/dist/cid.d.cts +22 -0
- package/dist/cid.d.cts.map +1 -0
- package/dist/cid.d.mts +22 -0
- package/dist/cid.d.mts.map +1 -0
- package/dist/cid.mjs +65 -0
- package/dist/cid.mjs.map +1 -0
- package/dist/client.cjs +221 -0
- package/dist/client.d.cts +72 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.mts +72 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +220 -0
- package/dist/client.mjs.map +1 -0
- package/dist/cloudflare.cjs +510 -0
- package/dist/cloudflare.d.cts +307 -0
- package/dist/cloudflare.d.cts.map +1 -0
- package/dist/cloudflare.d.mts +308 -0
- package/dist/cloudflare.d.mts.map +1 -0
- package/dist/cloudflare.mjs +507 -0
- package/dist/cloudflare.mjs.map +1 -0
- package/dist/ctime-backfill.cjs +272 -0
- package/dist/ctime-backfill.d.cts +169 -0
- package/dist/ctime-backfill.d.cts.map +1 -0
- package/dist/ctime-backfill.d.mts +169 -0
- package/dist/ctime-backfill.d.mts.map +1 -0
- package/dist/ctime-backfill.mjs +267 -0
- package/dist/ctime-backfill.mjs.map +1 -0
- package/dist/d1-gc-queue.cjs +80 -0
- package/dist/d1-gc-queue.d.cts +59 -0
- package/dist/d1-gc-queue.d.cts.map +1 -0
- package/dist/d1-gc-queue.d.mts +59 -0
- package/dist/d1-gc-queue.d.mts.map +1 -0
- package/dist/d1-gc-queue.mjs +81 -0
- package/dist/d1-gc-queue.mjs.map +1 -0
- package/dist/d1-resolve-store.cjs +189 -0
- package/dist/d1-resolve-store.d.cts +93 -0
- package/dist/d1-resolve-store.d.cts.map +1 -0
- package/dist/d1-resolve-store.d.mts +93 -0
- package/dist/d1-resolve-store.d.mts.map +1 -0
- package/dist/d1-resolve-store.mjs +190 -0
- package/dist/d1-resolve-store.mjs.map +1 -0
- package/dist/d1-schema.cjs +361 -0
- package/dist/d1-schema.d.cts +237 -0
- package/dist/d1-schema.d.cts.map +1 -0
- package/dist/d1-schema.d.mts +237 -0
- package/dist/d1-schema.d.mts.map +1 -0
- package/dist/d1-schema.mjs +341 -0
- package/dist/d1-schema.mjs.map +1 -0
- package/dist/d1-share-store.cjs +0 -0
- package/dist/d1-share-store.d.cts +53 -0
- package/dist/d1-share-store.d.cts.map +1 -0
- package/dist/d1-share-store.d.mts +53 -0
- package/dist/d1-share-store.d.mts.map +1 -0
- package/dist/d1-share-store.mjs +0 -0
- package/dist/d1-share-store.mjs.map +1 -0
- package/dist/d1-tree-index.cjs +1098 -0
- package/dist/d1-tree-index.d.cts +203 -0
- package/dist/d1-tree-index.d.cts.map +1 -0
- package/dist/d1-tree-index.d.mts +203 -0
- package/dist/d1-tree-index.d.mts.map +1 -0
- package/dist/d1-tree-index.mjs +1094 -0
- package/dist/d1-tree-index.mjs.map +1 -0
- package/dist/data-backend.cjs +147 -0
- package/dist/data-backend.d.cts +30 -0
- package/dist/data-backend.d.cts.map +1 -0
- package/dist/data-backend.d.mts +30 -0
- package/dist/data-backend.d.mts.map +1 -0
- package/dist/data-backend.mjs +148 -0
- package/dist/data-backend.mjs.map +1 -0
- package/dist/dedup-backfill.cjs +278 -0
- package/dist/dedup-backfill.d.cts +165 -0
- package/dist/dedup-backfill.d.cts.map +1 -0
- package/dist/dedup-backfill.d.mts +165 -0
- package/dist/dedup-backfill.d.mts.map +1 -0
- package/dist/dedup-backfill.mjs +273 -0
- package/dist/dedup-backfill.mjs.map +1 -0
- package/dist/did-space-like.d.cts +2 -0
- package/dist/did-space-like.d.mts +2 -0
- package/dist/did-utils.cjs +44 -0
- package/dist/did-utils.d.cts +25 -0
- package/dist/did-utils.d.cts.map +1 -0
- package/dist/did-utils.d.mts +25 -0
- package/dist/did-utils.d.mts.map +1 -0
- package/dist/did-utils.mjs +43 -0
- package/dist/did-utils.mjs.map +1 -0
- package/dist/dirty-path-queue.cjs +31 -0
- package/dist/dirty-path-queue.d.cts +104 -0
- package/dist/dirty-path-queue.d.cts.map +1 -0
- package/dist/dirty-path-queue.d.mts +104 -0
- package/dist/dirty-path-queue.d.mts.map +1 -0
- package/dist/dirty-path-queue.mjs +31 -0
- package/dist/dirty-path-queue.mjs.map +1 -0
- package/dist/dirty-path-schema.cjs +142 -0
- package/dist/dirty-path-schema.d.cts +90 -0
- package/dist/dirty-path-schema.d.cts.map +1 -0
- package/dist/dirty-path-schema.d.mts +90 -0
- package/dist/dirty-path-schema.d.mts.map +1 -0
- package/dist/dirty-path-schema.mjs +136 -0
- package/dist/dirty-path-schema.mjs.map +1 -0
- package/dist/download-actions.cjs +43 -0
- package/dist/download-actions.mjs +40 -0
- package/dist/download-actions.mjs.map +1 -0
- package/dist/fs-object-store.cjs +153 -0
- package/dist/fs-object-store.d.cts +21 -0
- package/dist/fs-object-store.d.cts.map +1 -0
- package/dist/fs-object-store.d.mts +22 -0
- package/dist/fs-object-store.d.mts.map +1 -0
- package/dist/fs-object-store.mjs +153 -0
- package/dist/fs-object-store.mjs.map +1 -0
- package/dist/gc-queue.d.cts +43 -0
- package/dist/gc-queue.d.cts.map +1 -0
- package/dist/gc-queue.d.mts +43 -0
- package/dist/gc-queue.d.mts.map +1 -0
- package/dist/index.cjs +2712 -0
- package/dist/index.d.cts +504 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +505 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2537 -0
- package/dist/index.mjs.map +1 -0
- package/dist/inline-content-migration.cjs +212 -0
- package/dist/inline-content-migration.d.cts +110 -0
- package/dist/inline-content-migration.d.cts.map +1 -0
- package/dist/inline-content-migration.d.mts +111 -0
- package/dist/inline-content-migration.d.mts.map +1 -0
- package/dist/inline-content-migration.mjs +207 -0
- package/dist/inline-content-migration.mjs.map +1 -0
- package/dist/inline-content.cjs +145 -0
- package/dist/inline-content.d.cts +98 -0
- package/dist/inline-content.d.cts.map +1 -0
- package/dist/inline-content.d.mts +98 -0
- package/dist/inline-content.d.mts.map +1 -0
- package/dist/inline-content.mjs +137 -0
- package/dist/inline-content.mjs.map +1 -0
- package/dist/local-changelog.cjs +241 -0
- package/dist/local-changelog.d.cts +119 -0
- package/dist/local-changelog.d.cts.map +1 -0
- package/dist/local-changelog.d.mts +119 -0
- package/dist/local-changelog.d.mts.map +1 -0
- package/dist/local-changelog.mjs +240 -0
- package/dist/local-changelog.mjs.map +1 -0
- package/dist/local.cjs +435 -0
- package/dist/local.d.cts +254 -0
- package/dist/local.d.cts.map +1 -0
- package/dist/local.d.mts +254 -0
- package/dist/local.d.mts.map +1 -0
- package/dist/local.mjs +427 -0
- package/dist/local.mjs.map +1 -0
- package/dist/memory-gc-queue.cjs +36 -0
- package/dist/memory-gc-queue.d.cts +22 -0
- package/dist/memory-gc-queue.d.cts.map +1 -0
- package/dist/memory-gc-queue.d.mts +22 -0
- package/dist/memory-gc-queue.d.mts.map +1 -0
- package/dist/memory-gc-queue.mjs +36 -0
- package/dist/memory-gc-queue.mjs.map +1 -0
- package/dist/memory-tree-index.cjs +734 -0
- package/dist/memory-tree-index.d.cts +159 -0
- package/dist/memory-tree-index.d.cts.map +1 -0
- package/dist/memory-tree-index.d.mts +159 -0
- package/dist/memory-tree-index.d.mts.map +1 -0
- package/dist/memory-tree-index.mjs +735 -0
- package/dist/memory-tree-index.mjs.map +1 -0
- package/dist/metadata-db.cjs +562 -0
- package/dist/metadata-db.d.cts +102 -0
- package/dist/metadata-db.d.cts.map +1 -0
- package/dist/metadata-db.d.mts +102 -0
- package/dist/metadata-db.d.mts.map +1 -0
- package/dist/metadata-db.mjs +562 -0
- package/dist/metadata-db.mjs.map +1 -0
- package/dist/mime-utils.cjs +2 -0
- package/dist/mime-utils.d.cts +2 -0
- package/dist/mime-utils.d.mts +2 -0
- package/dist/mime-utils.mjs +3 -0
- package/dist/object-key.cjs +41 -0
- package/dist/object-key.d.cts +25 -0
- package/dist/object-key.d.cts.map +1 -0
- package/dist/object-key.d.mts +25 -0
- package/dist/object-key.d.mts.map +1 -0
- package/dist/object-key.mjs +40 -0
- package/dist/object-key.mjs.map +1 -0
- package/dist/packages/session/dist/index.cjs +788 -0
- package/dist/packages/session/dist/index.mjs +782 -0
- package/dist/packages/session/dist/index.mjs.map +1 -0
- package/dist/platform/r2/dist/types.d.cts +49 -0
- package/dist/platform/r2/dist/types.d.cts.map +1 -0
- package/dist/prefix-range.cjs +33 -0
- package/dist/prefix-range.d.cts +29 -0
- package/dist/prefix-range.d.cts.map +1 -0
- package/dist/prefix-range.d.mts +29 -0
- package/dist/prefix-range.d.mts.map +1 -0
- package/dist/prefix-range.mjs +33 -0
- package/dist/prefix-range.mjs.map +1 -0
- package/dist/prefixed-tree-index.cjs +238 -0
- package/dist/prefixed-tree-index.d.cts +85 -0
- package/dist/prefixed-tree-index.d.cts.map +1 -0
- package/dist/prefixed-tree-index.d.mts +85 -0
- package/dist/prefixed-tree-index.d.mts.map +1 -0
- package/dist/prefixed-tree-index.mjs +239 -0
- package/dist/prefixed-tree-index.mjs.map +1 -0
- package/dist/presign-signer.cjs +7 -0
- package/dist/presign-signer.d.cts +99 -0
- package/dist/presign-signer.d.cts.map +1 -0
- package/dist/presign-signer.d.mts +99 -0
- package/dist/presign-signer.d.mts.map +1 -0
- package/dist/presign-signer.mjs +7 -0
- package/dist/presign-signer.mjs.map +1 -0
- package/dist/providers/platform/r2/dist/index.d.mts +3 -0
- package/dist/providers/platform/r2/dist/types.d.mts +49 -0
- package/dist/providers/platform/r2/dist/types.d.mts.map +1 -0
- package/dist/reconcile.cjs +184 -0
- package/dist/reconcile.d.cts +26 -0
- package/dist/reconcile.d.cts.map +1 -0
- package/dist/reconcile.d.mts +26 -0
- package/dist/reconcile.d.mts.map +1 -0
- package/dist/reconcile.mjs +184 -0
- package/dist/reconcile.mjs.map +1 -0
- package/dist/reference-check.cjs +49 -0
- package/dist/reference-check.d.cts +10 -0
- package/dist/reference-check.d.cts.map +1 -0
- package/dist/reference-check.d.mts +10 -0
- package/dist/reference-check.d.mts.map +1 -0
- package/dist/reference-check.mjs +48 -0
- package/dist/reference-check.mjs.map +1 -0
- package/dist/remote-afs.cjs +681 -0
- package/dist/remote-afs.d.cts +236 -0
- package/dist/remote-afs.d.cts.map +1 -0
- package/dist/remote-afs.d.mts +236 -0
- package/dist/remote-afs.d.mts.map +1 -0
- package/dist/remote-afs.mjs +676 -0
- package/dist/remote-afs.mjs.map +1 -0
- package/dist/rename-guard.cjs +18 -0
- package/dist/rename-guard.mjs +18 -0
- package/dist/rename-guard.mjs.map +1 -0
- package/dist/resolve.cjs +260 -0
- package/dist/resolve.d.cts +113 -0
- package/dist/resolve.d.cts.map +1 -0
- package/dist/resolve.d.mts +113 -0
- package/dist/resolve.d.mts.map +1 -0
- package/dist/resolve.mjs +260 -0
- package/dist/resolve.mjs.map +1 -0
- package/dist/rsync-content.cjs +33 -0
- package/dist/rsync-content.mjs +33 -0
- package/dist/rsync-content.mjs.map +1 -0
- package/dist/rsync.cjs +400 -0
- package/dist/rsync.d.cts +157 -0
- package/dist/rsync.d.cts.map +1 -0
- package/dist/rsync.d.mts +157 -0
- package/dist/rsync.d.mts.map +1 -0
- package/dist/rsync.mjs +397 -0
- package/dist/rsync.mjs.map +1 -0
- package/dist/run-gc.cjs +57 -0
- package/dist/run-gc.d.cts +40 -0
- package/dist/run-gc.d.cts.map +1 -0
- package/dist/run-gc.d.mts +41 -0
- package/dist/run-gc.d.mts.map +1 -0
- package/dist/run-gc.mjs +57 -0
- package/dist/run-gc.mjs.map +1 -0
- package/dist/scope-id.cjs +70 -0
- package/dist/scope-id.d.cts +35 -0
- package/dist/scope-id.d.cts.map +1 -0
- package/dist/scope-id.d.mts +35 -0
- package/dist/scope-id.d.mts.map +1 -0
- package/dist/scope-id.mjs +68 -0
- package/dist/scope-id.mjs.map +1 -0
- package/dist/scope-migration-node.cjs +172 -0
- package/dist/scope-migration-node.d.cts +44 -0
- package/dist/scope-migration-node.d.cts.map +1 -0
- package/dist/scope-migration-node.d.mts +44 -0
- package/dist/scope-migration-node.d.mts.map +1 -0
- package/dist/scope-migration-node.mjs +171 -0
- package/dist/scope-migration-node.mjs.map +1 -0
- package/dist/scope-migration.cjs +155 -0
- package/dist/scope-migration.d.cts +65 -0
- package/dist/scope-migration.d.cts.map +1 -0
- package/dist/scope-migration.d.mts +65 -0
- package/dist/scope-migration.d.mts.map +1 -0
- package/dist/scope-migration.mjs +154 -0
- package/dist/scope-migration.mjs.map +1 -0
- package/dist/share-declaration.cjs +225 -0
- package/dist/share-declaration.d.cts +30 -0
- package/dist/share-declaration.d.cts.map +1 -0
- package/dist/share-declaration.d.mts +32 -0
- package/dist/share-declaration.d.mts.map +1 -0
- package/dist/share-declaration.mjs +221 -0
- package/dist/share-declaration.mjs.map +1 -0
- package/dist/share-hook.cjs +63 -0
- package/dist/share-hook.mjs +62 -0
- package/dist/share-hook.mjs.map +1 -0
- package/dist/share-resolve.cjs +176 -0
- package/dist/share-resolve.d.cts +36 -0
- package/dist/share-resolve.d.cts.map +1 -0
- package/dist/share-resolve.d.mts +36 -0
- package/dist/share-resolve.d.mts.map +1 -0
- package/dist/share-resolve.mjs +177 -0
- package/dist/share-resolve.mjs.map +1 -0
- package/dist/share-serve.cjs +145 -0
- package/dist/share-serve.d.cts +30 -0
- package/dist/share-serve.d.cts.map +1 -0
- package/dist/share-serve.d.mts +30 -0
- package/dist/share-serve.d.mts.map +1 -0
- package/dist/share-serve.mjs +144 -0
- package/dist/share-serve.mjs.map +1 -0
- package/dist/share-store.cjs +25 -0
- package/dist/share-store.d.cts +65 -0
- package/dist/share-store.d.cts.map +1 -0
- package/dist/share-store.d.mts +65 -0
- package/dist/share-store.d.mts.map +1 -0
- package/dist/share-store.mjs +25 -0
- package/dist/share-store.mjs.map +1 -0
- package/dist/share-virtual-path.cjs +102 -0
- package/dist/share-virtual-path.mjs +101 -0
- package/dist/share-virtual-path.mjs.map +1 -0
- package/dist/space-resolver.cjs +54 -0
- package/dist/space-resolver.d.cts +34 -0
- package/dist/space-resolver.d.cts.map +1 -0
- package/dist/space-resolver.d.mts +34 -0
- package/dist/space-resolver.d.mts.map +1 -0
- package/dist/space-resolver.mjs +55 -0
- package/dist/space-resolver.mjs.map +1 -0
- package/dist/sqlite-gc-queue.cjs +51 -0
- package/dist/sqlite-gc-queue.d.cts +22 -0
- package/dist/sqlite-gc-queue.d.cts.map +1 -0
- package/dist/sqlite-gc-queue.d.mts +22 -0
- package/dist/sqlite-gc-queue.d.mts.map +1 -0
- package/dist/sqlite-gc-queue.mjs +52 -0
- package/dist/sqlite-gc-queue.mjs.map +1 -0
- package/dist/sqlite-resolve-store.cjs +163 -0
- package/dist/sqlite-resolve-store.d.cts +50 -0
- package/dist/sqlite-resolve-store.d.cts.map +1 -0
- package/dist/sqlite-resolve-store.d.mts +50 -0
- package/dist/sqlite-resolve-store.d.mts.map +1 -0
- package/dist/sqlite-resolve-store.mjs +164 -0
- package/dist/sqlite-resolve-store.mjs.map +1 -0
- package/dist/sqlite-share-store.cjs +157 -0
- package/dist/sqlite-share-store.d.cts +31 -0
- package/dist/sqlite-share-store.d.cts.map +1 -0
- package/dist/sqlite-share-store.d.mts +31 -0
- package/dist/sqlite-share-store.d.mts.map +1 -0
- package/dist/sqlite-share-store.mjs +157 -0
- package/dist/sqlite-share-store.mjs.map +1 -0
- package/dist/sqlite-tree-index.cjs +901 -0
- package/dist/sqlite-tree-index.d.cts +107 -0
- package/dist/sqlite-tree-index.d.cts.map +1 -0
- package/dist/sqlite-tree-index.d.mts +107 -0
- package/dist/sqlite-tree-index.d.mts.map +1 -0
- package/dist/sqlite-tree-index.mjs +902 -0
- package/dist/sqlite-tree-index.mjs.map +1 -0
- package/dist/sync-engine.cjs +1324 -0
- package/dist/sync-engine.d.cts +473 -0
- package/dist/sync-engine.d.cts.map +1 -0
- package/dist/sync-engine.d.mts +473 -0
- package/dist/sync-engine.d.mts.map +1 -0
- package/dist/sync-engine.mjs +1315 -0
- package/dist/sync-engine.mjs.map +1 -0
- package/dist/sync-state.cjs +303 -0
- package/dist/sync-state.d.cts +224 -0
- package/dist/sync-state.d.cts.map +1 -0
- package/dist/sync-state.d.mts +224 -0
- package/dist/sync-state.d.mts.map +1 -0
- package/dist/sync-state.mjs +297 -0
- package/dist/sync-state.mjs.map +1 -0
- package/dist/timestamp.cjs +92 -0
- package/dist/timestamp.mjs +91 -0
- package/dist/timestamp.mjs.map +1 -0
- package/dist/transports/http.cjs +54 -0
- package/dist/transports/http.d.cts +26 -0
- package/dist/transports/http.d.cts.map +1 -0
- package/dist/transports/http.d.mts +26 -0
- package/dist/transports/http.d.mts.map +1 -0
- package/dist/transports/http.mjs +54 -0
- package/dist/transports/http.mjs.map +1 -0
- package/dist/transports/static.cjs +55 -0
- package/dist/transports/static.d.cts +28 -0
- package/dist/transports/static.d.cts.map +1 -0
- package/dist/transports/static.d.mts +28 -0
- package/dist/transports/static.d.mts.map +1 -0
- package/dist/transports/static.mjs +55 -0
- package/dist/transports/static.mjs.map +1 -0
- package/dist/transports/types.cjs +13 -0
- package/dist/transports/types.d.cts +72 -0
- package/dist/transports/types.d.cts.map +1 -0
- package/dist/transports/types.d.mts +72 -0
- package/dist/transports/types.d.mts.map +1 -0
- package/dist/transports/types.mjs +13 -0
- package/dist/transports/types.mjs.map +1 -0
- package/dist/transports/ws.cjs +207 -0
- package/dist/transports/ws.d.cts +47 -0
- package/dist/transports/ws.d.cts.map +1 -0
- package/dist/transports/ws.d.mts +47 -0
- package/dist/transports/ws.d.mts.map +1 -0
- package/dist/transports/ws.mjs +206 -0
- package/dist/transports/ws.mjs.map +1 -0
- package/dist/tree-index.cjs +333 -0
- package/dist/tree-index.d.cts +524 -0
- package/dist/tree-index.d.cts.map +1 -0
- package/dist/tree-index.d.mts +524 -0
- package/dist/tree-index.d.mts.map +1 -0
- package/dist/tree-index.mjs +322 -0
- package/dist/tree-index.mjs.map +1 -0
- package/dist/tree-query.cjs +655 -0
- package/dist/tree-query.d.cts +70 -0
- package/dist/tree-query.d.cts.map +1 -0
- package/dist/tree-query.d.mts +70 -0
- package/dist/tree-query.d.mts.map +1 -0
- package/dist/tree-query.mjs +648 -0
- package/dist/tree-query.mjs.map +1 -0
- package/dist/upload-actions.cjs +97 -0
- package/dist/upload-actions.d.cts +19 -0
- package/dist/upload-actions.d.cts.map +1 -0
- package/dist/upload-actions.d.mts +21 -0
- package/dist/upload-actions.d.mts.map +1 -0
- package/dist/upload-actions.mjs +90 -0
- package/dist/upload-actions.mjs.map +1 -0
- package/dist/upload-session-store.cjs +185 -0
- package/dist/upload-session-store.d.cts +99 -0
- package/dist/upload-session-store.d.cts.map +1 -0
- package/dist/upload-session-store.d.mts +99 -0
- package/dist/upload-session-store.d.mts.map +1 -0
- package/dist/upload-session-store.mjs +184 -0
- package/dist/upload-session-store.mjs.map +1 -0
- package/dist/url-normalize.cjs +61 -0
- package/dist/url-normalize.d.cts +33 -0
- package/dist/url-normalize.d.cts.map +1 -0
- package/dist/url-normalize.d.mts +33 -0
- package/dist/url-normalize.d.mts.map +1 -0
- package/dist/url-normalize.mjs +61 -0
- package/dist/url-normalize.mjs.map +1 -0
- package/dist/versioned-content-backends.cjs +155 -0
- package/dist/versioned-content-backends.d.cts +49 -0
- package/dist/versioned-content-backends.d.cts.map +1 -0
- package/dist/versioned-content-backends.d.mts +49 -0
- package/dist/versioned-content-backends.d.mts.map +1 -0
- package/dist/versioned-content-backends.mjs +154 -0
- package/dist/versioned-content-backends.mjs.map +1 -0
- package/dist/versioned-content-index.cjs +102 -0
- package/dist/versioned-content-index.d.cts +46 -0
- package/dist/versioned-content-index.d.cts.map +1 -0
- package/dist/versioned-content-index.d.mts +46 -0
- package/dist/versioned-content-index.d.mts.map +1 -0
- package/dist/versioned-content-index.mjs +103 -0
- package/dist/versioned-content-index.mjs.map +1 -0
- package/dist/versioned-content-store.cjs +121 -0
- package/dist/versioned-content-store.d.cts +90 -0
- package/dist/versioned-content-store.d.cts.map +1 -0
- package/dist/versioned-content-store.d.mts +90 -0
- package/dist/versioned-content-store.d.mts.map +1 -0
- package/dist/versioned-content-store.mjs +122 -0
- package/dist/versioned-content-store.mjs.map +1 -0
- package/dist/versioned-scope.cjs +39 -0
- package/dist/versioned-scope.d.cts +57 -0
- package/dist/versioned-scope.d.cts.map +1 -0
- package/dist/versioned-scope.d.mts +57 -0
- package/dist/versioned-scope.d.mts.map +1 -0
- package/dist/versioned-scope.mjs +37 -0
- package/dist/versioned-scope.mjs.map +1 -0
- package/migrations/0001_tree_entries.sql +17 -0
- package/migrations/0002_resolve_entries.sql +7 -0
- package/migrations/0003_entries_v2.sql +38 -0
- package/migrations/0004_gc_queue.sql +5 -0
- package/migrations/0006_resolve_enhance.sql +14 -0
- package/migrations/0007_resolve_created_at.sql +10 -0
- package/migrations/0008_changelog.sql +34 -0
- package/migrations/0009_device_sync_state.sql +29 -0
- package/migrations/0010_scope_meta.sql +33 -0
- package/migrations/0011_shares.sql +25 -0
- package/migrations/0012_scope_meta_file_count.sql +41 -0
- package/migrations/0013_scope_meta_total_bytes.sql +47 -0
- package/migrations/0014_rename_tables.sql +62 -0
- package/package.json +102 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2537 @@
|
|
|
1
|
+
import { BLOCKLET_PUBLISH_MARKER, applyBlockletDeleteHook, applyBlockletWriteHook } from "./blocklet-hook.mjs";
|
|
2
|
+
import { CHANGELOG_DDL, CHANGELOG_INDEX_SQL, CHANGELOG_ROW_KIND, CHANGELOG_SCOPE_CID_MIGRATION_SQL, CHANGELOG_TABLE_SQL, CHANGELOG_WIRE_EXTRA_FIELDS, DEVICE_SYNC_STATE_INDEX_SQL, DEVICE_SYNC_STATE_TABLE_SQL, MANIFEST_FILE_KIND, MANIFEST_WIRE_EXTRA_FIELDS, PREFIX_WATERMARK_TABLE_SQL, SCOPE_META_FILE_COUNT_BACKFILL_SQL, SCOPE_META_FILE_COUNT_DELETE_ONE_SQL, SCOPE_META_FILE_COUNT_DELETE_RANGE_SQL, SCOPE_META_FILE_COUNT_TRIGGER_SQL, SCOPE_META_INDEX_SQL, SCOPE_META_TABLE_SQL, SCOPE_META_TOTAL_BYTES_BACKFILL_SQL, SCOPE_META_TOTAL_BYTES_COLUMN_SQL, SCOPE_META_TOTAL_BYTES_TRIGGER_SQL, SYNC_STATE_DDL, SYNC_STATE_INDEX_SQL, SYNC_STATE_TABLE_SQL, normalizeDdl, parseChangelogEntry, parseManifestEntry, serializeChangelogEntry, serializeManifestEntry, watermarkWritePrefix } from "./changelog-schema.mjs";
|
|
3
|
+
import { checksumSha256OfContent, cidToChecksumSha256, computeCID } from "./cid.mjs";
|
|
4
|
+
import { normalizeId, validateId } from "./did-utils.mjs";
|
|
5
|
+
import { DOWNLOAD_ACTIONS, DOWNLOAD_GRANT_TTL_SEC, PrepareDownloadArgsSchema } from "./download-actions.mjs";
|
|
6
|
+
import { detectMimeType, isTextMime } from "./mime-utils.mjs";
|
|
7
|
+
import { DEFAULT_INLINE_THRESHOLD, INGEST_DEVICE_ID, INLINE_CONTENT_HARD_CAP, assertInlineThreshold, decodeInlineText } from "./inline-content.mjs";
|
|
8
|
+
import { isShareDeclarationPath, isShareReservedPath } from "./share-declaration.mjs";
|
|
9
|
+
import { applyShareDeleteHook, applyShareWriteHook } from "./share-hook.mjs";
|
|
10
|
+
import { ShareVirtualHandler, matchSharesVirtualPath } from "./share-virtual-path.mjs";
|
|
11
|
+
import { resolveCtime } from "./timestamp.mjs";
|
|
12
|
+
import { buildKeysetCursor, buildOrderedQueryParts, compareTreeSortValues, keysetAfter, loopBatchDeleteEntries, loopBatchPutEntries, parseTreeCursor } from "./tree-index.mjs";
|
|
13
|
+
import { assertTreeQuerySpec, parseRecordText, projectRecordFields } from "./tree-query.mjs";
|
|
14
|
+
import { D1_DID_SPACE_SCHEMA_SQL, ENTRIES_CONTENT_TYPE_COLUMN_SQL, ENTRIES_CONTENT_TYPE_INDEX_DROP_SQL, ENTRIES_CONTENT_TYPE_INDEX_SQL, ENTRIES_DEDUP_INDEX_SQL, ENTRIES_DEDUP_KEY_COLUMN_SQL, ENTRIES_DEDUP_SCOPE_COLUMN_SQL, ENTRIES_INDEX_SQL, ENTRIES_INLINE_CONTENT_COLUMN_SQL, ENTRIES_METADATA_COLUMN_SQL, ENTRIES_TABLE_SQL, GC_QUEUE_INDEX_SQL, GC_QUEUE_TABLE_SQL, RESOLVE_ENTRIES_INDEX_SQL, RESOLVE_ENTRIES_TABLE_SQL, SHARES_INDEX_SQL, SHARES_TABLE_SQL, UPLOAD_SESSIONS_INDEX_SQL, UPLOAD_SESSIONS_TABLE_SQL, UPLOAD_SESSION_ENTRIES_TABLE_SQL, VERSIONED_SCOPES_INDEX_SQL, VERSIONED_SCOPES_TABLE_SQL } from "./d1-schema.mjs";
|
|
15
|
+
import { D1UploadSessionStore, SqliteUploadSessionStore, UPLOAD_SESSION_TTL_MS } from "./upload-session-store.mjs";
|
|
16
|
+
import { AFS_IFMATCH_ABSENT, AbortUploadArgsSchema, CommitUploadArgsSchema, GRANT_EXPIRES_SEC, PrepareUploadArgsSchema, UPLOAD_ACTIONS, ifMatchPreconditionFailed, zodMessage } from "./upload-actions.mjs";
|
|
17
|
+
import { normalizeItemUrl } from "./url-normalize.mjs";
|
|
18
|
+
import { __decorate } from "./_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs";
|
|
19
|
+
import { attachMetadataContext, detachMetadataContext, getMetadataContext } from "./afs-metadata-context.mjs";
|
|
20
|
+
import { CHANGELOG_INSERT_SQL, MAX_DEVICE_ID_BYTES, buildChangelogBinds, validateDeviceId } from "./changelog-write.mjs";
|
|
21
|
+
import { CachingObjectStore, CidByteCache } from "./cid-byte-cache.mjs";
|
|
22
|
+
import { BatchOverBudgetError, BatchUnsupportedError, RpcUserSpaceAFS } from "./remote-afs.mjs";
|
|
23
|
+
import { HttpTransport } from "./transports/http.mjs";
|
|
24
|
+
import { StaticReadonlyTransport } from "./transports/static.mjs";
|
|
25
|
+
import { WsTransport, createWsTransport } from "./transports/ws.mjs";
|
|
26
|
+
import { DIDSpaceClient, connectDIDSpace } from "./client.mjs";
|
|
27
|
+
import { D1GCQueue } from "./d1-gc-queue.mjs";
|
|
28
|
+
import { prefixRange } from "./prefix-range.mjs";
|
|
29
|
+
import { D1ResolveStore } from "./d1-resolve-store.mjs";
|
|
30
|
+
import { assertValidCid, objectKeyForCid } from "./object-key.mjs";
|
|
31
|
+
import { ShareResolver } from "./share-resolve.mjs";
|
|
32
|
+
import { rowToEntry } from "./share-store.mjs";
|
|
33
|
+
import { D1ShareReconciler, D1ShareStore } from "./d1-share-store.mjs";
|
|
34
|
+
import { DIRTY_PATH_DDL, DIRTY_PATH_INDEX_SQL, DIRTY_PATH_TABLE_SQL, DIRTY_PATH_TRIGGER_SQL } from "./dirty-path-schema.mjs";
|
|
35
|
+
import { D1TreeIndex, MAX_ENTRIES_PER_SCOPE, ancestorDirs, nameOf, parentOf } from "./d1-tree-index.mjs";
|
|
36
|
+
import { DIDSpaceBlockletDataBackend } from "./data-backend.mjs";
|
|
37
|
+
import { applyDirtyUpsert } from "./dirty-path-queue.mjs";
|
|
38
|
+
import { LOCAL_CHANGELOG_DEFAULT_LIMIT, LOCAL_CHANGELOG_MAX_LIMIT, LocalChangelog } from "./local-changelog.mjs";
|
|
39
|
+
import { MemoryGcQueue } from "./memory-gc-queue.mjs";
|
|
40
|
+
import { MemoryTreeIndex } from "./memory-tree-index.mjs";
|
|
41
|
+
import { PrefixedTreeIndex } from "./prefixed-tree-index.mjs";
|
|
42
|
+
import { PRESIGN_MAX_EXPIRES_SEC } from "./presign-signer.mjs";
|
|
43
|
+
import { referencedAnywhereD1, referencedAnywhereLibsql } from "./reference-check.mjs";
|
|
44
|
+
import { DIDSpaceResolver } from "./resolve.mjs";
|
|
45
|
+
import { buildManifest, computeDiff, fetchRemoteManifest, runWithConcurrency, syncDIDSpace } from "./rsync.mjs";
|
|
46
|
+
import { DEFAULT_GC_GRACE_MS, runGc } from "./run-gc.mjs";
|
|
47
|
+
import { decodeScopeHmacSecret, deriveScope, isPlainDidScope } from "./scope-id.mjs";
|
|
48
|
+
import { buildD1MigrationSql, classifyScopes, migrateD1Scopes } from "./scope-migration.mjs";
|
|
49
|
+
import { serveShare } from "./share-serve.mjs";
|
|
50
|
+
import { SqliteGcQueue } from "./sqlite-gc-queue.mjs";
|
|
51
|
+
import { SqliteResolveStore } from "./sqlite-resolve-store.mjs";
|
|
52
|
+
import { SqliteShareStore, openDaemonShareStore } from "./sqlite-share-store.mjs";
|
|
53
|
+
import { ConflictResolutionError, CurrentObjectMissingError, PULL_BATCH_LIMIT, PULL_FETCH_CONCURRENCY, PUSH_BATCH_LIMIT, PUSH_PROGRESS_FLUSH_INTERVAL, SYNC_WALL_CLOCK_BUDGET_MS, SyncEngine, classifyMissingObject } from "./sync-engine.mjs";
|
|
54
|
+
import { DEVICE_ID_FORMAT_RE, SYNC_STATE_KEYS, SyncStateStore, defaultAppState, generateDesktopDeviceId, isValidDeviceId, parseSyncStatus, serializeSyncStatus } from "./sync-state.mjs";
|
|
55
|
+
import { D1VersionedScopeBackend, LibsqlVersionedScopeBackend, MemoryVersionedScopeBackend } from "./versioned-content-backends.mjs";
|
|
56
|
+
import { VersionedContentIndex } from "./versioned-content-index.mjs";
|
|
57
|
+
import { VersionedContentStore } from "./versioned-content-store.mjs";
|
|
58
|
+
import { resolveVersionToken, versionedScope, versionedScopeGroup } from "./versioned-scope.mjs";
|
|
59
|
+
import { AFSCompactedError, AFSConflictError, AFSForbiddenError, AFSNotFoundError, AFSUnsupportedError, AFSValidationError, SCRIPT_EVENT_TYPES, collectionQueryStandardAction, isValidOrderBy, makeNsLog, orderByRequested, orderEntries, resolveFieldPath } from "@aigne/afs";
|
|
60
|
+
import { AFSBaseProvider, Actions, Delete, Explain, List, Meta, Read, Search, Stat, Write } from "@aigne/afs/provider";
|
|
61
|
+
import { time } from "@aigne/afs-timing";
|
|
62
|
+
import { joinURL } from "ufo";
|
|
63
|
+
import { z } from "zod";
|
|
64
|
+
|
|
65
|
+
//#region src/index.ts
|
|
66
|
+
const log = makeNsLog("provider:did-space");
|
|
67
|
+
const LIST_MAX_LIMIT = 1e3;
|
|
68
|
+
/**
|
|
69
|
+
* AFS entry `id` is the basename (last path segment), matching AFSFS/AFSJSON —
|
|
70
|
+
* NOT the full path. `AFSBaseProvider.buildEntry` defaults `id` to the full
|
|
71
|
+
* normalized path, so the list handlers below MUST pass an explicit basename id;
|
|
72
|
+
* otherwise consumers that labelize `entry.id` (e.g. the auto-surface settings
|
|
73
|
+
* tabs) render the whole path instead of "Input"/"Sort"/"View". The stat handler
|
|
74
|
+
* already does this (`afsPath.split("/").pop()`); the list handlers were the gap.
|
|
75
|
+
*/
|
|
76
|
+
function basenameId(path) {
|
|
77
|
+
return path.split("/").filter(Boolean).pop() ?? "/";
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Provider-reserved meta keys (planning/agent-hub/metadata-foundation.md §
|
|
81
|
+
* 白名单). These are computed by DID Space at write time and overwrite
|
|
82
|
+
* anything the application tries to put under the same name. The application
|
|
83
|
+
* may write any OTHER key into `payload.meta` and it'll be persisted to the
|
|
84
|
+
* `entries.metadata` column.
|
|
85
|
+
*
|
|
86
|
+
* Note: `ctime / mtime` are NOT in `entry.meta` (they live at AFSEntry top
|
|
87
|
+
* level as `createdAt / updatedAt`), so they're not in the reserved set —
|
|
88
|
+
* the application can write `meta.ctime` and it'll be persisted as just
|
|
89
|
+
* another application field, NOT conflated with the entry's timestamp.
|
|
90
|
+
*/
|
|
91
|
+
const PROVIDER_RESERVED_META_KEYS = new Set([
|
|
92
|
+
"kind",
|
|
93
|
+
"size",
|
|
94
|
+
"cid",
|
|
95
|
+
"mimeType"
|
|
96
|
+
]);
|
|
97
|
+
/**
|
|
98
|
+
* Strip provider-reserved keys from a write payload's meta, leaving only the
|
|
99
|
+
* application-owned fields that should go to `entries.metadata`.
|
|
100
|
+
* Returns undefined when nothing application-owned remains (avoids storing
|
|
101
|
+
* `{}` blobs).
|
|
102
|
+
*/
|
|
103
|
+
function extractApplicationMeta(meta) {
|
|
104
|
+
if (!meta) return void 0;
|
|
105
|
+
const out = {};
|
|
106
|
+
let any = false;
|
|
107
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
108
|
+
if (PROVIDER_RESERVED_META_KEYS.has(k)) continue;
|
|
109
|
+
out[k] = v;
|
|
110
|
+
any = true;
|
|
111
|
+
}
|
|
112
|
+
return any ? out : void 0;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Loose upper bound on serialized application metadata (did-space-metadata
|
|
116
|
+
* NEEDS DECISION 3). `metadata` rides `z.record(z.unknown())` and is NOT
|
|
117
|
+
* digest-checked (it's application data, not bytes — design §5), so the wire
|
|
118
|
+
* imposes no size limit of its own. Aside's real metadata is tiny (a few ISO
|
|
119
|
+
* timestamps, ~50B), but a malicious / buggy client could otherwise stuff a
|
|
120
|
+
* huge JSON blob straight into the D1 `metadata` column (row-size / write-
|
|
121
|
+
* amplification risk, cf. the D1 burn-through lesson). 64KB is far above any
|
|
122
|
+
* legitimate use yet blocks abuse. Shared by the two new server ingress points
|
|
123
|
+
* (batchWrite + upload-plane commit) so the threshold can't drift.
|
|
124
|
+
*/
|
|
125
|
+
const METADATA_MAX_BYTES = 64 * 1024;
|
|
126
|
+
/**
|
|
127
|
+
* Reject abusively large application metadata gracefully (an `AFSValidationError`,
|
|
128
|
+
* which afs-rpc maps to 400 — never an uncaught 500). Call AFTER
|
|
129
|
+
* `extractApplicationMeta` so the reserved-key strip can't be gamed to dodge
|
|
130
|
+
* the bound. No-op for absent metadata.
|
|
131
|
+
*/
|
|
132
|
+
function assertMetadataSize(meta, path) {
|
|
133
|
+
if (!meta) return;
|
|
134
|
+
const bytes = new TextEncoder().encode(JSON.stringify(meta)).length;
|
|
135
|
+
if (bytes > METADATA_MAX_BYTES) throw new AFSValidationError(`Application metadata for ${path} is ${bytes} bytes, over the ${METADATA_MAX_BYTES}-byte limit`);
|
|
136
|
+
}
|
|
137
|
+
/** Plain JSON object (not array, not null) — the deep-merge branch. */
|
|
138
|
+
function isPlainObject(v) {
|
|
139
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
140
|
+
}
|
|
141
|
+
/** An incoming scalar that carries no information and must never overwrite. */
|
|
142
|
+
function isEmptyScalar(v) {
|
|
143
|
+
return v === void 0 || v === null || v === "";
|
|
144
|
+
}
|
|
145
|
+
/** Union two arrays, dedup by structural identity, existing entries first. */
|
|
146
|
+
function unionArrays(existing, incoming) {
|
|
147
|
+
const seen = new Set(existing.map((x) => JSON.stringify(x)));
|
|
148
|
+
const out = [...existing];
|
|
149
|
+
for (const item of incoming) {
|
|
150
|
+
const key = JSON.stringify(item);
|
|
151
|
+
if (!seen.has(key)) {
|
|
152
|
+
out.push(item);
|
|
153
|
+
seen.add(key);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Merge incoming item content into the existing item on a duplicate-key write
|
|
160
|
+
* (#492). A re-save is ADDITIVE and must never DEGRADE the stored item:
|
|
161
|
+
*
|
|
162
|
+
* - keys in `immutable` (always `createdAt`, plus caller-supplied lifecycle
|
|
163
|
+
* fields like `status`) keep the existing value — a re-clip never resets an
|
|
164
|
+
* archived item or rewrites its creation time;
|
|
165
|
+
* - arrays union-dedup (existing first) so tags / collections accrete;
|
|
166
|
+
* - plain objects deep-merge recursively, so a partial re-clip of `og` /
|
|
167
|
+
* `content` keeps fields the new capture omitted;
|
|
168
|
+
* - scalars take the incoming value EXCEPT empty ones (`undefined` / `null` /
|
|
169
|
+
* `""`), which keep the existing value — a bookmarklet that failed to grab a
|
|
170
|
+
* title must not wipe the good title already saved;
|
|
171
|
+
* - type conflicts (array↔scalar, object↔scalar) take the incoming value.
|
|
172
|
+
*/
|
|
173
|
+
function mergeItemContent(existing, incoming, immutable = new Set(["createdAt"])) {
|
|
174
|
+
const merged = { ...existing };
|
|
175
|
+
for (const [k, v] of Object.entries(incoming)) {
|
|
176
|
+
if (immutable.has(k)) continue;
|
|
177
|
+
const cur = merged[k];
|
|
178
|
+
if (Array.isArray(v) && Array.isArray(cur)) merged[k] = unionArrays(cur, v);
|
|
179
|
+
else if (isPlainObject(v) && isPlainObject(cur)) merged[k] = mergeItemContent(cur, v, immutable);
|
|
180
|
+
else if (isEmptyScalar(v)) {} else merged[k] = v;
|
|
181
|
+
}
|
|
182
|
+
return merged;
|
|
183
|
+
}
|
|
184
|
+
var DIDSpaceProvider = class extends AFSBaseProvider {
|
|
185
|
+
static manifest() {
|
|
186
|
+
return {
|
|
187
|
+
name: "did-space",
|
|
188
|
+
description: "DID-scoped persistent storage with CID-based content deduplication.\n- Per-user data isolation via DID prefix\n- Content-addressable deduplication (CID v1, raw codec + SHA-256)\n- R2 for objects, D1 for tree index",
|
|
189
|
+
uriTemplate: "did-space://{did}/{path*}",
|
|
190
|
+
category: "storage",
|
|
191
|
+
schema: z.object({
|
|
192
|
+
did: z.string(),
|
|
193
|
+
path: z.string().optional()
|
|
194
|
+
}),
|
|
195
|
+
tags: [
|
|
196
|
+
"did",
|
|
197
|
+
"storage",
|
|
198
|
+
"cid",
|
|
199
|
+
"dedup",
|
|
200
|
+
"cloud"
|
|
201
|
+
],
|
|
202
|
+
capabilityTags: [
|
|
203
|
+
"read-write",
|
|
204
|
+
"crud",
|
|
205
|
+
"search",
|
|
206
|
+
"auth:token"
|
|
207
|
+
],
|
|
208
|
+
security: {
|
|
209
|
+
riskLevel: "sandboxed",
|
|
210
|
+
resourceAccess: []
|
|
211
|
+
},
|
|
212
|
+
capabilities: { filesystem: {
|
|
213
|
+
read: true,
|
|
214
|
+
write: true
|
|
215
|
+
} },
|
|
216
|
+
noPerEntryActions: true
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
static treeSchema() {
|
|
220
|
+
return {
|
|
221
|
+
operations: [
|
|
222
|
+
"list",
|
|
223
|
+
"read",
|
|
224
|
+
"write",
|
|
225
|
+
"delete",
|
|
226
|
+
"search",
|
|
227
|
+
"stat",
|
|
228
|
+
"explain"
|
|
229
|
+
],
|
|
230
|
+
tree: {
|
|
231
|
+
"/": { kind: "did-space:root" },
|
|
232
|
+
"/{path+}": { kind: "did-space:file" }
|
|
233
|
+
},
|
|
234
|
+
auth: { type: "custom" },
|
|
235
|
+
bestFor: [
|
|
236
|
+
"user files",
|
|
237
|
+
"persistent storage",
|
|
238
|
+
"content-addressed data"
|
|
239
|
+
],
|
|
240
|
+
notFor: ["large binary blobs", "real-time streaming"]
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
name;
|
|
244
|
+
description;
|
|
245
|
+
accessMode;
|
|
246
|
+
did;
|
|
247
|
+
objectStore;
|
|
248
|
+
treeIndex;
|
|
249
|
+
gcQueue;
|
|
250
|
+
/** See `DIDSpaceProviderOptions.deviceId`. Read by writeHandler/deleteHandler. */
|
|
251
|
+
deviceId;
|
|
252
|
+
/** Optional ShareResolver for .share reconcile hook (Phase 0.4) */
|
|
253
|
+
shareResolver;
|
|
254
|
+
/** See `DIDSpaceProviderOptions.shareOwnerDid` / `shareTreePrefix`. */
|
|
255
|
+
shareOwnerDid;
|
|
256
|
+
shareTreePrefix;
|
|
257
|
+
/** See `DIDSpaceProviderOptions.blockletResolver` / `blockletOwnerDid` (#674). */
|
|
258
|
+
blockletResolver;
|
|
259
|
+
blockletOwnerDid;
|
|
260
|
+
/** Upload plane deps (design §7). See `DIDSpaceProviderOptions`. */
|
|
261
|
+
signer;
|
|
262
|
+
uploadSessionStore;
|
|
263
|
+
uploadScope;
|
|
264
|
+
constructor(options) {
|
|
265
|
+
super();
|
|
266
|
+
validateId(options.did);
|
|
267
|
+
this.name = options.name || "did-space";
|
|
268
|
+
this.description = options.description;
|
|
269
|
+
this.accessMode = options.accessMode ?? "readwrite";
|
|
270
|
+
this.did = normalizeId(options.did);
|
|
271
|
+
this.objectStore = options.objectStore;
|
|
272
|
+
this.treeIndex = options.treeIndex;
|
|
273
|
+
this.gcQueue = options.gcQueue;
|
|
274
|
+
this.deviceId = options.deviceId ?? "server:default";
|
|
275
|
+
this.shareResolver = options.shareResolver;
|
|
276
|
+
this.shareOwnerDid = options.shareOwnerDid;
|
|
277
|
+
this.shareTreePrefix = options.shareTreePrefix;
|
|
278
|
+
this.blockletResolver = options.blockletResolver;
|
|
279
|
+
this.blockletOwnerDid = options.blockletOwnerDid;
|
|
280
|
+
this.signer = options.signer;
|
|
281
|
+
this.uploadSessionStore = options.uploadSessionStore;
|
|
282
|
+
this.uploadScope = options.uploadScope;
|
|
283
|
+
}
|
|
284
|
+
/** Convert AFS path to tree-index path (strip leading /) */
|
|
285
|
+
treePath(afsPath) {
|
|
286
|
+
if (afsPath.includes("\0")) throw new Error("Null byte in path");
|
|
287
|
+
if (/(?:^|\/)\.\.(?:\/|$)/.test(afsPath)) throw new Error("Path traversal in path");
|
|
288
|
+
return afsPath.replace(/^\/+/, "");
|
|
289
|
+
}
|
|
290
|
+
/** Convert AFS path to tree-index prefix for directory listing */
|
|
291
|
+
treeListPrefix(afsPath) {
|
|
292
|
+
if (afsPath === "/") return "";
|
|
293
|
+
return `${this.treePath(afsPath)}/`;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Remap a write/delete into the arguments the .share reconcile hook expects.
|
|
297
|
+
* On a whole-space provider this is identity. On a prefixed instance-fragment
|
|
298
|
+
* provider (`shareTreePrefix` set) it restores the WHOLE-TREE decl path and
|
|
299
|
+
* keys the share to the space owner (`shareOwnerDid`), so reconcile stores the
|
|
300
|
+
* same `owner_did` + full `decl_path` the public serving path resolves by.
|
|
301
|
+
*/
|
|
302
|
+
shareHookArgs(afsPath, treePath) {
|
|
303
|
+
const fullTreePath = this.shareTreePrefix ? joinURL(this.shareTreePrefix, treePath) : treePath;
|
|
304
|
+
return {
|
|
305
|
+
afsPath: this.shareTreePrefix ? `/${fullTreePath}` : afsPath,
|
|
306
|
+
treePath: fullTreePath,
|
|
307
|
+
ownerDid: this.shareOwnerDid ?? this.did
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Remap a write/delete into the arguments the blocklet hook expects. Same
|
|
312
|
+
* whole-tree path restoration as {@link shareHookArgs}, but the recorded
|
|
313
|
+
* `ownerDid` is the PLAINTEXT space owner (`blockletOwnerDid`), not the de-id
|
|
314
|
+
* scope — the serve path re-derives the storage scope from the plaintext DID.
|
|
315
|
+
*/
|
|
316
|
+
blockletHookArgs(afsPath, treePath) {
|
|
317
|
+
const fullTreePath = this.shareTreePrefix ? joinURL(this.shareTreePrefix, treePath) : treePath;
|
|
318
|
+
return {
|
|
319
|
+
afsPath: this.shareTreePrefix ? `/${fullTreePath}` : afsPath,
|
|
320
|
+
treePath: fullTreePath,
|
|
321
|
+
ownerDid: this.blockletOwnerDid ?? this.did
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
objectKey(cid) {
|
|
325
|
+
if (!/^[bz][a-zA-Z0-9]{20,100}$/.test(cid)) throw new Error(`Invalid CID format: ${cid.slice(0, 30)}`);
|
|
326
|
+
return `objects/${cid}`;
|
|
327
|
+
}
|
|
328
|
+
async isDirectory(path) {
|
|
329
|
+
if (path === "/") return true;
|
|
330
|
+
return this.treeIndex.hasChildren(this.treeListPrefix(path));
|
|
331
|
+
}
|
|
332
|
+
/** Collect all CIDs under a prefix for GC queuing before bulk delete. */
|
|
333
|
+
async collectCIDsByPrefix(prefix) {
|
|
334
|
+
const cids = [];
|
|
335
|
+
let cursor;
|
|
336
|
+
do {
|
|
337
|
+
const result = await this.treeIndex.listEntries(prefix, {
|
|
338
|
+
limit: 1e3,
|
|
339
|
+
cursor
|
|
340
|
+
});
|
|
341
|
+
for (const e of result.entries) if (e.cid) cids.push(e.cid);
|
|
342
|
+
cursor = result.hasMore ? result.cursor : void 0;
|
|
343
|
+
} while (cursor);
|
|
344
|
+
return cids;
|
|
345
|
+
}
|
|
346
|
+
async readMeta(ctx) {
|
|
347
|
+
const afsPath = joinURL("/", ctx.params.path ?? "");
|
|
348
|
+
if (afsPath === "/") {
|
|
349
|
+
const childrenCount = await this.treeIndex.countChildren(this.treeListPrefix("/"));
|
|
350
|
+
return this.buildEntry(joinURL(afsPath, ".meta"), {
|
|
351
|
+
content: {
|
|
352
|
+
type: "directory",
|
|
353
|
+
path: "/"
|
|
354
|
+
},
|
|
355
|
+
meta: {
|
|
356
|
+
kind: "did-space:root",
|
|
357
|
+
childrenCount
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const entry = await this.treeIndex.get(this.treePath(afsPath), { includeContent: false });
|
|
362
|
+
if (entry) return this.buildEntry(joinURL(afsPath, ".meta"), {
|
|
363
|
+
content: {
|
|
364
|
+
type: "file",
|
|
365
|
+
path: afsPath,
|
|
366
|
+
size: entry.size,
|
|
367
|
+
cid: entry.cid
|
|
368
|
+
},
|
|
369
|
+
meta: {
|
|
370
|
+
kind: "did-space:file",
|
|
371
|
+
size: entry.size,
|
|
372
|
+
cid: entry.cid,
|
|
373
|
+
mimeType: entry.mimeType
|
|
374
|
+
},
|
|
375
|
+
createdAt: new Date(entry.ctime),
|
|
376
|
+
updatedAt: new Date(entry.mtime)
|
|
377
|
+
});
|
|
378
|
+
if (await this.isDirectory(afsPath)) {
|
|
379
|
+
const childrenCount = await this.treeIndex.countChildren(this.treeListPrefix(afsPath));
|
|
380
|
+
return this.buildEntry(joinURL(afsPath, ".meta"), {
|
|
381
|
+
content: {
|
|
382
|
+
type: "directory",
|
|
383
|
+
path: afsPath
|
|
384
|
+
},
|
|
385
|
+
meta: {
|
|
386
|
+
kind: "did-space:directory",
|
|
387
|
+
childrenCount
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
throw new AFSNotFoundError(afsPath);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Override to implement the `maxDepth: -1` snapshot contract.
|
|
395
|
+
*
|
|
396
|
+
* When called with `maxDepth: -1`, collects ALL file entries under `path`
|
|
397
|
+
* using cursor-based pagination through the treeIndex and returns them in
|
|
398
|
+
* a single `AFSListResult` with `meta.cursor = String(manifestSeq)` so
|
|
399
|
+
* `SyncEngine.fullResync()` can advance `cloudSeq` correctly.
|
|
400
|
+
*
|
|
401
|
+
* > 10_000 entries → sets `meta.truncated: true` (HTTP 200, SyncEngine
|
|
402
|
+
* throws `ManifestTooLargeError`).
|
|
403
|
+
*
|
|
404
|
+
* All other options fall through to `super.list()` → `listHandler`.
|
|
405
|
+
*/
|
|
406
|
+
async list(path, options) {
|
|
407
|
+
if (options?.maxDepth !== -1) return super.list(path, options);
|
|
408
|
+
const normPath = path.startsWith("/") ? path.slice(1) : path;
|
|
409
|
+
if (normPath === ".changelog" || normPath.startsWith(".changelog/")) throw new AFSValidationError(".changelog path does not support maxDepth=-1 (changelog uses cursor-based incremental access)");
|
|
410
|
+
if (options.cursor !== void 0 || options.limit !== void 0 && Number.isFinite(options.limit) || options.offset !== void 0 && Number.isFinite(options.offset)) throw new AFSValidationError("list with maxDepth=-1 cannot be combined with cursor, limit, or offset");
|
|
411
|
+
const SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
412
|
+
const prefix = this.treeListPrefix(path);
|
|
413
|
+
const allEntries = [];
|
|
414
|
+
let cursor;
|
|
415
|
+
do {
|
|
416
|
+
const result = await this.treeIndex.listEntries(prefix, {
|
|
417
|
+
limit: LIST_MAX_LIMIT,
|
|
418
|
+
cursor
|
|
419
|
+
});
|
|
420
|
+
for (const treeEntry of result.entries) {
|
|
421
|
+
if (allEntries.length >= SNAPSHOT_MAX_ENTRIES) return {
|
|
422
|
+
data: allEntries,
|
|
423
|
+
meta: {
|
|
424
|
+
truncated: true,
|
|
425
|
+
...orderByRequested(options.orderBy) ? { orderByIgnored: true } : {}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
allEntries.push(this.buildEntry(`/${treeEntry.path}`, {
|
|
429
|
+
id: basenameId(treeEntry.path),
|
|
430
|
+
meta: serializeManifestEntry({
|
|
431
|
+
cid: treeEntry.cid,
|
|
432
|
+
size: treeEntry.size,
|
|
433
|
+
mtime: treeEntry.mtime,
|
|
434
|
+
mimeType: treeEntry.mimeType
|
|
435
|
+
}),
|
|
436
|
+
createdAt: new Date(treeEntry.ctime),
|
|
437
|
+
updatedAt: new Date(treeEntry.mtime)
|
|
438
|
+
}));
|
|
439
|
+
}
|
|
440
|
+
cursor = result.hasMore ? result.cursor : void 0;
|
|
441
|
+
} while (cursor);
|
|
442
|
+
const manifestSeq = await this.treeIndex.getMaxSeq();
|
|
443
|
+
const snapshotMeta = { cursor: String(manifestSeq) };
|
|
444
|
+
if (orderByRequested(options.orderBy)) snapshotMeta.orderByIgnored = true;
|
|
445
|
+
return {
|
|
446
|
+
data: allEntries,
|
|
447
|
+
meta: snapshotMeta
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
async listHandler(ctx) {
|
|
451
|
+
const afsPath = ctx.params.path ? `/${ctx.params.path}` : "/";
|
|
452
|
+
const options = ctx.options;
|
|
453
|
+
const includeContent = options?.includeContent === true;
|
|
454
|
+
const includeChildrenCounts = options?.includeChildrenCounts === true;
|
|
455
|
+
const orderRequested = orderByRequested(options?.orderBy);
|
|
456
|
+
const orderBy = orderRequested && isValidOrderBy(options?.orderBy) ? options.orderBy : void 0;
|
|
457
|
+
/** Merge the no-silent-drop flag into a result when ordering was requested but not applied. */
|
|
458
|
+
const flagIgnored = (result) => orderRequested ? {
|
|
459
|
+
...result,
|
|
460
|
+
meta: {
|
|
461
|
+
...result.meta,
|
|
462
|
+
orderByIgnored: true
|
|
463
|
+
}
|
|
464
|
+
} : result;
|
|
465
|
+
const sharesMatch = matchSharesVirtualPath(afsPath);
|
|
466
|
+
if (sharesMatch && this.shareResolver && !this.shareTreePrefix) {
|
|
467
|
+
const { list } = await new ShareVirtualHandler(this.shareResolver, this.did).list(sharesMatch);
|
|
468
|
+
return flagIgnored({ data: list });
|
|
469
|
+
}
|
|
470
|
+
const normPath = afsPath.startsWith("/") ? afsPath.slice(1) : afsPath;
|
|
471
|
+
const isChangelog = normPath === ".changelog" || normPath.startsWith(".changelog/");
|
|
472
|
+
if (isChangelog && options?.cursor !== void 0) return flagIgnored(await this.listChangelog(options.cursor, options.limit));
|
|
473
|
+
const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
474
|
+
const offset = Math.max(0, Number(options?.offset) || 0);
|
|
475
|
+
const maxDepth = options?.maxDepth ?? 1;
|
|
476
|
+
const collectLimit = offset + limit;
|
|
477
|
+
if (maxDepth === 0) return { data: [] };
|
|
478
|
+
const prefix = this.treeListPrefix(afsPath);
|
|
479
|
+
if (afsPath !== "/") {
|
|
480
|
+
if (!await this.treeIndex.get(this.treePath(afsPath), { includeContent: false })) {
|
|
481
|
+
if (!await this.treeIndex.hasChildren(prefix)) throw new AFSNotFoundError(afsPath);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const entries = [];
|
|
485
|
+
if (maxDepth === 1) {
|
|
486
|
+
if (orderBy) {
|
|
487
|
+
const scan$1 = await this.listShallowOrdered(prefix, collectLimit, entries, orderBy, includeContent);
|
|
488
|
+
if (includeChildrenCounts) await this.fillChildrenCounts(entries);
|
|
489
|
+
return this.withLiveSeq({
|
|
490
|
+
data: entries,
|
|
491
|
+
total: await this.shallowTotal(prefix, scan$1)
|
|
492
|
+
}, prefix, isChangelog);
|
|
493
|
+
}
|
|
494
|
+
const scan = await this.listShallow(prefix, collectLimit, entries, void 0, includeContent);
|
|
495
|
+
if (includeChildrenCounts) await this.fillChildrenCounts(entries);
|
|
496
|
+
return this.withLiveSeq(flagIgnored({
|
|
497
|
+
data: entries,
|
|
498
|
+
total: await this.shallowTotal(prefix, scan)
|
|
499
|
+
}), prefix, isChangelog);
|
|
500
|
+
}
|
|
501
|
+
await this.listDeep(afsPath, prefix, collectLimit, maxDepth, entries, includeContent);
|
|
502
|
+
return flagIgnored({ data: entries });
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* True direct-children total for a shallow listing.
|
|
506
|
+
*
|
|
507
|
+
* AFSListResult contract (`packages/core/src/type.ts`): absent `total`
|
|
508
|
+
* means "data IS the complete result" — so a handler that truncates its
|
|
509
|
+
* collection MUST report `total`, or every consumer downstream (base.ts
|
|
510
|
+
* pagination, afs-list pager) mistakes the page for the whole set.
|
|
511
|
+
*
|
|
512
|
+
* Cost model (D1: per-query round-trips dominate): `listShallow` scans
|
|
513
|
+
* the index in LIST_MAX_LIMIT batches and keeps counting rows past the
|
|
514
|
+
* build cap, so when the cursor drained (directories ≤ LIST_MAX_LIMIT
|
|
515
|
+
* children — the common case) the scan itself yields the exact total:
|
|
516
|
+
* ZERO extra queries. Only a directory truncated mid-cursor (> 1000
|
|
517
|
+
* children) pays ONE COUNT round-trip (same `countChildren(prefix)`
|
|
518
|
+
* call and parent-key format `readMeta` already relies on).
|
|
519
|
+
*/
|
|
520
|
+
async shallowTotal(prefix, scan) {
|
|
521
|
+
if (scan.exhausted) return scan.seen;
|
|
522
|
+
return this.treeIndex.countChildren(prefix);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Attach the per-prefix watermark `version` as the result's top-level
|
|
526
|
+
* `liveSeq` (Live Update Resilience §4 / 0a'). Read ONE row via the
|
|
527
|
+
* TreeIndex — O(1), independent of changelog history/global write volume.
|
|
528
|
+
*
|
|
529
|
+
* - Skipped for `.changelog` listings (sync-internal, no live-update scope).
|
|
530
|
+
* - Skipped when the index has no `getPrefixWatermark` (rollout) or reports
|
|
531
|
+
* no watermark key for the path (`null` — no collection root): the
|
|
532
|
+
* consumer then falls back to full re-list on the next edge/poll, still
|
|
533
|
+
* correct.
|
|
534
|
+
* - A valid-but-never-written collection root reports `"0"` so consumers
|
|
535
|
+
* baseline at 0 instead of conservatively refetching every poll tick on a
|
|
536
|
+
* read-only collection (the first write bumps to 1 > 0 and is detected).
|
|
537
|
+
* - The value is prefix-LOCAL (only this collection root's write count), so
|
|
538
|
+
* it never leaks another blocklet's timing (invariant I3).
|
|
539
|
+
*/
|
|
540
|
+
async withLiveSeq(result, prefix, isChangelog) {
|
|
541
|
+
if (isChangelog || !this.treeIndex.getPrefixWatermark) return result;
|
|
542
|
+
const version = await this.treeIndex.getPrefixWatermark(prefix);
|
|
543
|
+
if (version != null) result.liveSeq = String(version);
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Shallow listing with a VALID orderBy (did-space-sorting Phase 3).
|
|
548
|
+
*
|
|
549
|
+
* - Single tuple → forwarded to the TreeIndex (`listChildren` orders
|
|
550
|
+
* natively over the (scope,parent,col,name) indexes; keyset cursor
|
|
551
|
+
* pages stay globally ordered, so collecting offset+limit entries is
|
|
552
|
+
* the correct order-before-pagination slice).
|
|
553
|
+
* - 2-3 tuples → the index can order by one column only, so collect the
|
|
554
|
+
* FULL child set and apply the canonical core sort before truncation
|
|
555
|
+
* (order → offset → limit still holds).
|
|
556
|
+
*/
|
|
557
|
+
async listShallowOrdered(prefix, collectLimit, entries, orderBy, includeContent = false) {
|
|
558
|
+
if (orderBy.length === 1) {
|
|
559
|
+
const [field, order] = orderBy[0];
|
|
560
|
+
return this.listShallow(prefix, collectLimit, entries, {
|
|
561
|
+
orderBy: field,
|
|
562
|
+
order
|
|
563
|
+
}, includeContent);
|
|
564
|
+
}
|
|
565
|
+
const all = [];
|
|
566
|
+
await this.listShallow(prefix, Number.MAX_SAFE_INTEGER, all, void 0, includeContent);
|
|
567
|
+
const { entries: sorted } = orderEntries(all, orderBy);
|
|
568
|
+
entries.push(...sorted.slice(0, collectLimit));
|
|
569
|
+
return {
|
|
570
|
+
exhausted: true,
|
|
571
|
+
seen: all.length
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
async listShallow(prefix, limit, entries, order, includeContent = false) {
|
|
575
|
+
let cursor;
|
|
576
|
+
let seen = 0;
|
|
577
|
+
do {
|
|
578
|
+
const result = await this.treeIndex.listChildren(prefix, {
|
|
579
|
+
limit: LIST_MAX_LIMIT,
|
|
580
|
+
cursor,
|
|
581
|
+
includeContent,
|
|
582
|
+
...order ?? {}
|
|
583
|
+
});
|
|
584
|
+
seen += result.entries.length;
|
|
585
|
+
for (const child of result.entries) {
|
|
586
|
+
if (entries.length >= limit) continue;
|
|
587
|
+
if (child.path.endsWith("/")) {
|
|
588
|
+
const displayPath = `/${child.path.slice(0, -1)}`;
|
|
589
|
+
entries.push(this.buildEntry(displayPath, {
|
|
590
|
+
id: basenameId(displayPath),
|
|
591
|
+
meta: {
|
|
592
|
+
kind: "did-space:directory",
|
|
593
|
+
childrenCount: -1,
|
|
594
|
+
size: child.size
|
|
595
|
+
},
|
|
596
|
+
createdAt: new Date(child.ctime),
|
|
597
|
+
updatedAt: new Date(child.mtime)
|
|
598
|
+
}));
|
|
599
|
+
} else entries.push(this.buildEntry(`/${child.path}`, {
|
|
600
|
+
id: basenameId(child.path),
|
|
601
|
+
...includeContent && await this.verifiedInline(child) !== null ? { content: child.content } : {},
|
|
602
|
+
meta: {
|
|
603
|
+
kind: "did-space:file",
|
|
604
|
+
cid: child.cid,
|
|
605
|
+
size: child.size,
|
|
606
|
+
mimeType: child.mimeType
|
|
607
|
+
},
|
|
608
|
+
createdAt: new Date(child.ctime),
|
|
609
|
+
updatedAt: new Date(child.mtime)
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
cursor = result.hasMore ? result.cursor : void 0;
|
|
613
|
+
} while (cursor && entries.length < limit);
|
|
614
|
+
return {
|
|
615
|
+
exhausted: cursor === void 0,
|
|
616
|
+
seen
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Patch EXACT `meta.childrenCount` onto the directory entries of a
|
|
621
|
+
* collected shallow page — the opt-in half of the two-tier childrenCount
|
|
622
|
+
* contract (`AFSListOptions.includeChildrenCounts`; default tier is the
|
|
623
|
+
* `-1` sentinel stamped by listShallow). Page-bounded by construction:
|
|
624
|
+
* only the dirs actually collected are counted, ONE `countChildrenMany`
|
|
625
|
+
* round-trip (per-dir `countChildren` fallback for indexes without it),
|
|
626
|
+
* so the cost is Σ direct children of the page's dirs — never the
|
|
627
|
+
* whole-subtree scan the retired `countChildrenGrouped` performed.
|
|
628
|
+
*/
|
|
629
|
+
async fillChildrenCounts(entries) {
|
|
630
|
+
const dirs = entries.filter((e) => e.meta?.kind === "did-space:directory");
|
|
631
|
+
if (dirs.length === 0) return;
|
|
632
|
+
const keyOf = (e) => `${this.treePath(e.path)}/`;
|
|
633
|
+
if (this.treeIndex.countChildrenMany) {
|
|
634
|
+
const counts = await this.treeIndex.countChildrenMany(dirs.map(keyOf));
|
|
635
|
+
for (const e of dirs) e.meta.childrenCount = counts.get(keyOf(e)) ?? 0;
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
for (const e of dirs) e.meta.childrenCount = await this.treeIndex.countChildren(keyOf(e));
|
|
639
|
+
}
|
|
640
|
+
async listDeep(afsBasePath, prefix, limit, maxDepth, entries, includeContent = false) {
|
|
641
|
+
const prefixDepth = afsBasePath === "/" ? 0 : afsBasePath.split("/").filter(Boolean).length;
|
|
642
|
+
const seenDirs = /* @__PURE__ */ new Set();
|
|
643
|
+
let cursor;
|
|
644
|
+
do {
|
|
645
|
+
const result = await this.treeIndex.listEntries(prefix, {
|
|
646
|
+
limit: LIST_MAX_LIMIT,
|
|
647
|
+
cursor,
|
|
648
|
+
includeContent
|
|
649
|
+
});
|
|
650
|
+
for (const treeEntry of result.entries) {
|
|
651
|
+
if (entries.length >= limit) return;
|
|
652
|
+
const parts = treeEntry.path.slice(prefix.length).split("/").filter(Boolean);
|
|
653
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
654
|
+
if (i + 1 > maxDepth) break;
|
|
655
|
+
const dirKey = prefix + parts.slice(0, i + 1).join("/");
|
|
656
|
+
if (!seenDirs.has(dirKey)) {
|
|
657
|
+
seenDirs.add(dirKey);
|
|
658
|
+
entries.push(this.buildEntry(`/${dirKey}`, {
|
|
659
|
+
id: basenameId(dirKey),
|
|
660
|
+
meta: {
|
|
661
|
+
kind: "did-space:directory",
|
|
662
|
+
childrenCount: -1
|
|
663
|
+
}
|
|
664
|
+
}));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const path = `/${treeEntry.path}`;
|
|
668
|
+
if (path.split("/").filter(Boolean).length - prefixDepth <= maxDepth) entries.push(this.buildEntry(path, {
|
|
669
|
+
id: basenameId(path),
|
|
670
|
+
...includeContent && await this.verifiedInline(treeEntry) !== null ? { content: treeEntry.content } : {},
|
|
671
|
+
meta: {
|
|
672
|
+
kind: "did-space:file",
|
|
673
|
+
cid: treeEntry.cid,
|
|
674
|
+
size: treeEntry.size,
|
|
675
|
+
mimeType: treeEntry.mimeType
|
|
676
|
+
},
|
|
677
|
+
createdAt: new Date(treeEntry.ctime),
|
|
678
|
+
updatedAt: new Date(treeEntry.mtime)
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
cursor = result.hasMore ? result.cursor : void 0;
|
|
682
|
+
} while (cursor && entries.length < limit);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Task 1.9: list changelog rows starting after `cursor` (seq number).
|
|
686
|
+
* Returns rows as AFSEntry objects + meta.cursor pointing to the last row's seq.
|
|
687
|
+
*/
|
|
688
|
+
async listChangelog(cursor, limit) {
|
|
689
|
+
const sinceSeq = Number(cursor) || 0;
|
|
690
|
+
const effectiveLimit = Math.min(limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
691
|
+
const watermark = await this.treeIndex.getCompactedBelowSeq?.() ?? 0;
|
|
692
|
+
if (watermark > 0 && sinceSeq < watermark) throw new AFSCompactedError("/.changelog", String(watermark));
|
|
693
|
+
const changelog = await this.treeIndex.getChangelogSince(sinceSeq, effectiveLimit);
|
|
694
|
+
return {
|
|
695
|
+
data: changelog.entries.map((row) => this.buildEntry(`/.changelog/${row.seq}`, {
|
|
696
|
+
content: null,
|
|
697
|
+
meta: {
|
|
698
|
+
kind: CHANGELOG_ROW_KIND,
|
|
699
|
+
extra: serializeChangelogEntry(row)
|
|
700
|
+
}
|
|
701
|
+
})),
|
|
702
|
+
meta: {
|
|
703
|
+
cursor: String(changelog.cursor),
|
|
704
|
+
hasMore: changelog.hasMore
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Promote write + delete to the object form so callers can discover the
|
|
710
|
+
* `ifMatch` sub-capability (CLAUDE.md "Sub-capability schema"). The base impl
|
|
711
|
+
* (base.ts) returns plain booleans; without this override the if-match
|
|
712
|
+
* conformance suite's `getCapabilityFeature("write","ifMatch")` reads nothing
|
|
713
|
+
* and the suite skips. Mirrors AFSFS's override (fs/src/index.ts).
|
|
714
|
+
*/
|
|
715
|
+
getOperationsDeclaration() {
|
|
716
|
+
const ops = super.getOperationsDeclaration();
|
|
717
|
+
if (typeof ops.write === "boolean" ? ops.write : ops.write?.supported === true) ops.write = {
|
|
718
|
+
supported: true,
|
|
719
|
+
features: {
|
|
720
|
+
ifMatch: true,
|
|
721
|
+
treeMerge: true,
|
|
722
|
+
treeToggle: true,
|
|
723
|
+
treeIncrement: true,
|
|
724
|
+
treeListAdd: true,
|
|
725
|
+
treeListRemove: true,
|
|
726
|
+
batchPushdown: true
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
if (typeof ops.delete === "boolean" ? ops.delete : ops.delete?.supported === true) ops.delete = {
|
|
730
|
+
supported: true,
|
|
731
|
+
features: { ifMatch: true }
|
|
732
|
+
};
|
|
733
|
+
if (typeof ops.list === "boolean" ? ops.list : ops.list?.supported === true) ops.list = {
|
|
734
|
+
supported: true,
|
|
735
|
+
features: {
|
|
736
|
+
orderBy: true,
|
|
737
|
+
includeContent: true
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
return ops;
|
|
741
|
+
}
|
|
742
|
+
async readCapabilities(_ctx) {
|
|
743
|
+
const operations = this.getOperationsDeclaration();
|
|
744
|
+
const queryable = typeof this.treeIndex.query === "function";
|
|
745
|
+
if (queryable) operations.query = true;
|
|
746
|
+
return {
|
|
747
|
+
id: "/.meta/.capabilities",
|
|
748
|
+
path: "/.meta/.capabilities",
|
|
749
|
+
content: {
|
|
750
|
+
schemaVersion: 1,
|
|
751
|
+
provider: this.name,
|
|
752
|
+
description: this.description || "DID-scoped persistent storage with CID-based deduplication",
|
|
753
|
+
tools: [],
|
|
754
|
+
actions: [],
|
|
755
|
+
operations,
|
|
756
|
+
...queryable ? { standardActions: [collectionQueryStandardAction({
|
|
757
|
+
doc: true,
|
|
758
|
+
groupBy: true,
|
|
759
|
+
annotate: true,
|
|
760
|
+
aggregate: true
|
|
761
|
+
})] } : {}
|
|
762
|
+
},
|
|
763
|
+
meta: {
|
|
764
|
+
kind: "afs:capabilities",
|
|
765
|
+
operations
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Typed collection query over a directory of records (design §3.5).
|
|
771
|
+
*
|
|
772
|
+
* - dir-per-record (`spec.doc`): direct child dirs joined with their
|
|
773
|
+
* `<dir>/<doc>` document; file-per-record (no doc): direct child files'
|
|
774
|
+
* inline content is the record.
|
|
775
|
+
* - where/orderBy/pagination/total/groupBy execute inside the TreeIndex
|
|
776
|
+
* engine (SQL or in-memory — lockstep-tested). select + annotate.memberOf
|
|
777
|
+
* are page-level post-processing here, identical across backends.
|
|
778
|
+
* - Strict contract: spec arrives core-validated; provider constraints
|
|
779
|
+
* (single orderBy key, no text) throw AFSValidationError. An index
|
|
780
|
+
* without the engine (e.g. not-yet-migrated backends) throws
|
|
781
|
+
* AFSUnsupportedError — and is never declared in capabilities.
|
|
782
|
+
*/
|
|
783
|
+
async query(path, spec, _options) {
|
|
784
|
+
if (!this.treeIndex.query) throw new AFSUnsupportedError("query", "Collection query is not supported by this index backend");
|
|
785
|
+
const afsPath = joinURL("/", path);
|
|
786
|
+
if (this.isVirtualPath(afsPath) || matchSharesVirtualPath(afsPath)) throw new AFSValidationError(`Invalid query spec: path: not a queryable collection: ${afsPath}`);
|
|
787
|
+
assertTreeQuerySpec(spec);
|
|
788
|
+
const prefix = this.treeListPrefix(afsPath);
|
|
789
|
+
if (afsPath !== "/") {
|
|
790
|
+
if (!await this.treeIndex.get(this.treePath(afsPath), { includeContent: false }) && !await this.treeIndex.hasChildren(prefix)) throw new AFSNotFoundError(afsPath);
|
|
791
|
+
}
|
|
792
|
+
const result = await this.treeIndex.query(prefix, spec);
|
|
793
|
+
let memberValues;
|
|
794
|
+
if (spec.annotate) {
|
|
795
|
+
memberValues = /* @__PURE__ */ new Set();
|
|
796
|
+
try {
|
|
797
|
+
const ref = await this.read(joinURL("/", spec.annotate.memberOf.doc));
|
|
798
|
+
const raw = resolveFieldPath(parseRecordText(typeof ref.data?.content === "string" ? ref.data.content : void 0), spec.annotate.memberOf.field);
|
|
799
|
+
if (Array.isArray(raw)) for (const v of raw) memberValues.add(typeof v === "string" ? v.toLowerCase() : String(v));
|
|
800
|
+
} catch {}
|
|
801
|
+
}
|
|
802
|
+
const entries = [];
|
|
803
|
+
for (const row of result.rows) entries.push(await this.queryRowToEntry(row, spec, memberValues));
|
|
804
|
+
const out = {
|
|
805
|
+
entries,
|
|
806
|
+
total: result.total
|
|
807
|
+
};
|
|
808
|
+
if (result.cursor) out.cursor = result.cursor;
|
|
809
|
+
if (result.groups) out.groups = result.groups;
|
|
810
|
+
if (result.aggregateGroups) out.aggregateGroups = result.aggregateGroups;
|
|
811
|
+
if (result.seekOffset !== void 0) out.seekOffset = result.seekOffset;
|
|
812
|
+
if (this.treeIndex.getPrefixWatermark) {
|
|
813
|
+
const version = await this.treeIndex.getPrefixWatermark(prefix);
|
|
814
|
+
if (version != null) out.liveSeq = String(version);
|
|
815
|
+
}
|
|
816
|
+
return out;
|
|
817
|
+
}
|
|
818
|
+
/** Map an engine row to an AFSEntry (mirrors the list handlers' shapes). */
|
|
819
|
+
async queryRowToEntry(row, spec, memberValues) {
|
|
820
|
+
const isDir = row.kind === "dir";
|
|
821
|
+
const displayPath = isDir ? `/${row.path.slice(0, -1)}` : `/${row.path}`;
|
|
822
|
+
const verifyCid = isDir ? row.contentCid : row.cid;
|
|
823
|
+
const verified = row.content !== void 0 && verifyCid ? await this.verifiedInline({
|
|
824
|
+
cid: verifyCid,
|
|
825
|
+
content: row.content
|
|
826
|
+
}) : null;
|
|
827
|
+
let content;
|
|
828
|
+
if (spec.select) content = projectRecordFields(parseRecordText(verified ?? void 0), spec.select);
|
|
829
|
+
else if (verified !== null) content = verified;
|
|
830
|
+
const entry = this.buildEntry(displayPath, {
|
|
831
|
+
id: basenameId(displayPath),
|
|
832
|
+
...content !== void 0 ? { content } : {},
|
|
833
|
+
meta: isDir ? {
|
|
834
|
+
...row.metadata ?? {},
|
|
835
|
+
kind: "did-space:directory",
|
|
836
|
+
childrenCount: -1,
|
|
837
|
+
size: row.size
|
|
838
|
+
} : {
|
|
839
|
+
...row.metadata ?? {},
|
|
840
|
+
kind: "did-space:file",
|
|
841
|
+
cid: row.cid,
|
|
842
|
+
size: row.size,
|
|
843
|
+
mimeType: row.mimeType
|
|
844
|
+
},
|
|
845
|
+
createdAt: new Date(row.ctime),
|
|
846
|
+
updatedAt: new Date(row.mtime)
|
|
847
|
+
});
|
|
848
|
+
if (spec.annotate && memberValues) {
|
|
849
|
+
const { by, as } = spec.annotate.memberOf;
|
|
850
|
+
const byValue = by === "$id" ? row.name : resolveFieldPath(parseRecordText(verified ?? row.content), by);
|
|
851
|
+
const member = byValue != null && memberValues.has(typeof byValue === "string" ? byValue.toLowerCase() : String(byValue));
|
|
852
|
+
entry._derived = { [as]: member ? "1" : "" };
|
|
853
|
+
}
|
|
854
|
+
return entry;
|
|
855
|
+
}
|
|
856
|
+
async readHandler(ctx) {
|
|
857
|
+
const afsPath = joinURL("/", ctx.params.path ?? "");
|
|
858
|
+
const sharesMatch = matchSharesVirtualPath(afsPath);
|
|
859
|
+
if (sharesMatch && this.shareResolver && !this.shareTreePrefix) {
|
|
860
|
+
const result = await new ShareVirtualHandler(this.shareResolver, this.did).read(sharesMatch);
|
|
861
|
+
if (!result) throw new AFSNotFoundError(afsPath);
|
|
862
|
+
return result.data;
|
|
863
|
+
}
|
|
864
|
+
const normPath = afsPath.startsWith("/") ? afsPath.slice(1) : afsPath;
|
|
865
|
+
if (normPath.startsWith(".objects/")) {
|
|
866
|
+
const cid = normPath.slice(9);
|
|
867
|
+
if (!await time("ds.ref", () => this.treeIndex.isReferenced(cid))) throw new AFSNotFoundError(afsPath);
|
|
868
|
+
const obj = await time("ds.obj", () => this.objectStore.get(this.objectKey(cid)));
|
|
869
|
+
if (!obj) throw new AFSNotFoundError(afsPath);
|
|
870
|
+
const bytes = new Uint8Array(await time("ds.body", () => obj.arrayBuffer()));
|
|
871
|
+
return this.buildEntry(afsPath, {
|
|
872
|
+
content: bytes,
|
|
873
|
+
meta: {
|
|
874
|
+
kind: "did-space:object",
|
|
875
|
+
cid,
|
|
876
|
+
size: bytes.length
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
if (afsPath === "/") {
|
|
881
|
+
const childrenCount = await this.treeIndex.countChildren(this.treeListPrefix("/"));
|
|
882
|
+
return this.buildEntry(afsPath, {
|
|
883
|
+
content: `DID Space root for ${this.did} (${childrenCount} entries)`,
|
|
884
|
+
meta: {
|
|
885
|
+
kind: "did-space:root",
|
|
886
|
+
childrenCount
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
const entry = await time("ds.idx", () => this.treeIndex.get(this.treePath(afsPath)));
|
|
891
|
+
if (entry) {
|
|
892
|
+
if (entry.content !== void 0 && isTextMime(entry.mimeType)) {
|
|
893
|
+
if (await time("ds.inl", async () => {
|
|
894
|
+
return await computeCID(entry.content) === entry.cid;
|
|
895
|
+
})) return this.buildEntry(afsPath, {
|
|
896
|
+
content: entry.content,
|
|
897
|
+
meta: {
|
|
898
|
+
...entry.metadata ?? {},
|
|
899
|
+
kind: "did-space:file",
|
|
900
|
+
size: entry.size,
|
|
901
|
+
cid: entry.cid,
|
|
902
|
+
mimeType: entry.mimeType,
|
|
903
|
+
version: entry.cid
|
|
904
|
+
},
|
|
905
|
+
createdAt: new Date(entry.ctime),
|
|
906
|
+
updatedAt: new Date(entry.mtime)
|
|
907
|
+
});
|
|
908
|
+
log.warn(`[did-space] inline content failed cid verification at ${afsPath} (cid=${entry.cid}) — serving from objects; row will self-repair on next write`);
|
|
909
|
+
}
|
|
910
|
+
const obj = await time("ds.obj", () => this.objectStore.get(this.objectKey(entry.cid)));
|
|
911
|
+
const content = await time("ds.body", async () => obj ? isTextMime(entry.mimeType) ? await obj.text() : await obj.arrayBuffer() : isTextMime(entry.mimeType) ? "" : /* @__PURE__ */ new ArrayBuffer(0));
|
|
912
|
+
return this.buildEntry(afsPath, {
|
|
913
|
+
content,
|
|
914
|
+
meta: {
|
|
915
|
+
...entry.metadata ?? {},
|
|
916
|
+
kind: "did-space:file",
|
|
917
|
+
size: entry.size,
|
|
918
|
+
cid: entry.cid,
|
|
919
|
+
mimeType: entry.mimeType,
|
|
920
|
+
version: entry.cid
|
|
921
|
+
},
|
|
922
|
+
createdAt: new Date(entry.ctime),
|
|
923
|
+
updatedAt: new Date(entry.mtime)
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
if (await this.isDirectory(afsPath)) {
|
|
927
|
+
const childrenCount = await this.treeIndex.countChildren(this.treeListPrefix(afsPath));
|
|
928
|
+
return this.buildEntry(afsPath, {
|
|
929
|
+
content: `Directory: ${afsPath} (${childrenCount} entries)`,
|
|
930
|
+
meta: {
|
|
931
|
+
kind: "did-space:directory",
|
|
932
|
+
childrenCount
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
throw new AFSNotFoundError(afsPath);
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* presignRead — path-keyed read-grant capability (preview-r2-direct §3).
|
|
940
|
+
*
|
|
941
|
+
* Resolves `path` in THIS provider's (prefix-isolated) view exactly like
|
|
942
|
+
* `read`/`stat` (`treeIndex.get(treePath(path))`, incl. the same null-byte /
|
|
943
|
+
* `..`-segment guards). iff the resolved entry has a cid AND a signer with
|
|
944
|
+
* `presignGet` is present → mint a short-TTL presigned GET with the
|
|
945
|
+
* Content-Type baked in, and return `{ url, expiresAt }`.
|
|
946
|
+
*
|
|
947
|
+
* The path resolution IS the authorization: a caller can only presign what
|
|
948
|
+
* `read` would already return. Prefix isolation is inherited from the view's
|
|
949
|
+
* `PrefixedTreeIndex` — no separate object-level `isReferenced(cid)` is needed
|
|
950
|
+
* (that primitive is for the cid-keyed `.objects/{cid}` path, which has no
|
|
951
|
+
* tree path). NEVER throws to the caller: no signer (Node) / non-CAS / no cid /
|
|
952
|
+
* unresolved / any internal error → `null`, and the raw handler then falls back
|
|
953
|
+
* to the worker serve path. TTL = `DOWNLOAD_GRANT_TTL_SEC` (2h) — never the 7d
|
|
954
|
+
* signer cap (§8.3).
|
|
955
|
+
*/
|
|
956
|
+
async presignRead(path, opts) {
|
|
957
|
+
try {
|
|
958
|
+
const entry = await this.treeIndex.get(this.treePath(path), { includeContent: false });
|
|
959
|
+
if (!entry?.cid) return null;
|
|
960
|
+
if (this.signer == null || typeof this.signer.presignGet !== "function") return null;
|
|
961
|
+
const g = await this.signer.presignGet({
|
|
962
|
+
objectKey: this.objectKey(entry.cid),
|
|
963
|
+
expiresInSec: DOWNLOAD_GRANT_TTL_SEC,
|
|
964
|
+
responseContentType: opts?.responseContentType
|
|
965
|
+
});
|
|
966
|
+
return {
|
|
967
|
+
url: g.url,
|
|
968
|
+
expiresAt: g.expiresAt
|
|
969
|
+
};
|
|
970
|
+
} catch {
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Re-verify an entry's inline copy against its cid (§2.I list contract).
|
|
976
|
+
* Returns the content when verified, null otherwise (best-effort miss).
|
|
977
|
+
*/
|
|
978
|
+
async verifiedInline(entry) {
|
|
979
|
+
if (entry.content === void 0) return null;
|
|
980
|
+
return await computeCID(entry.content) === entry.cid ? entry.content : null;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Returns true if the given AFS path is a virtual D4-protected path
|
|
984
|
+
* (.objects/* or .changelog/*).
|
|
985
|
+
*/
|
|
986
|
+
isVirtualPath(afsPath) {
|
|
987
|
+
const norm = afsPath.startsWith("/") ? afsPath.slice(1) : afsPath;
|
|
988
|
+
return norm === ".objects" || norm.startsWith(".objects/") || norm === ".changelog" || norm.startsWith(".changelog/");
|
|
989
|
+
}
|
|
990
|
+
async writeHandler(ctx, payload) {
|
|
991
|
+
const afsPath = joinURL("/", ctx.params.path ?? "");
|
|
992
|
+
if (this.isVirtualPath(afsPath)) throw new AFSForbiddenError(afsPath, "virtual_path");
|
|
993
|
+
if (isShareReservedPath(afsPath)) throw new AFSForbiddenError(afsPath, "share-path-reserved");
|
|
994
|
+
const writeIfMatch = ctx.options?.ifMatch;
|
|
995
|
+
if (writeIfMatch !== void 0) {
|
|
996
|
+
const current = await this.treeIndex.get(this.treePath(afsPath), { includeContent: false });
|
|
997
|
+
if (ifMatchPreconditionFailed(writeIfMatch, current)) throw new AFSConflictError(afsPath, {
|
|
998
|
+
cid: current?.cid ?? null,
|
|
999
|
+
mtime: current?.mtime ?? null
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
if (payload.content === void 0 && payload.meta !== void 0) return { data: this.buildEntry(afsPath, { meta: {
|
|
1003
|
+
kind: "did-space:file",
|
|
1004
|
+
...payload.meta
|
|
1005
|
+
} }) };
|
|
1006
|
+
let bytes;
|
|
1007
|
+
let storeValue;
|
|
1008
|
+
if (payload.content instanceof Uint8Array) {
|
|
1009
|
+
bytes = payload.content;
|
|
1010
|
+
storeValue = bytes.buffer;
|
|
1011
|
+
} else if (payload.content instanceof ArrayBuffer) {
|
|
1012
|
+
bytes = new Uint8Array(payload.content);
|
|
1013
|
+
storeValue = payload.content;
|
|
1014
|
+
} else {
|
|
1015
|
+
const text = typeof payload.content === "string" ? payload.content : JSON.stringify(payload.content);
|
|
1016
|
+
bytes = new TextEncoder().encode(text);
|
|
1017
|
+
storeValue = text;
|
|
1018
|
+
}
|
|
1019
|
+
const cid = await computeCID(bytes);
|
|
1020
|
+
const treePath = this.treePath(afsPath);
|
|
1021
|
+
await applyShareWriteHook({
|
|
1022
|
+
...this.shareHookArgs(afsPath, treePath),
|
|
1023
|
+
bytes,
|
|
1024
|
+
cid,
|
|
1025
|
+
resolver: this.shareResolver,
|
|
1026
|
+
phase: "pre-io"
|
|
1027
|
+
});
|
|
1028
|
+
if (!await this.objectStore.head(this.objectKey(cid))) await this.objectStore.put(this.objectKey(cid), storeValue);
|
|
1029
|
+
const writeMeta = ctx.options?.meta;
|
|
1030
|
+
const writeDeviceId = typeof writeMeta?.deviceId === "string" && writeMeta.deviceId ? writeMeta.deviceId : this.deviceId;
|
|
1031
|
+
const now = typeof writeMeta?.mtime === "number" ? writeMeta.mtime : Date.now();
|
|
1032
|
+
let mimeType = payload.meta?.mimeType ?? detectMimeType(afsPath) ?? void 0;
|
|
1033
|
+
if (!mimeType && (payload.content instanceof Uint8Array || payload.content instanceof ArrayBuffer)) mimeType = "application/octet-stream";
|
|
1034
|
+
const ctime = resolveCtime(payload.meta, now);
|
|
1035
|
+
const treeEntry = {
|
|
1036
|
+
cid,
|
|
1037
|
+
size: bytes.length,
|
|
1038
|
+
mtime: now,
|
|
1039
|
+
ctime,
|
|
1040
|
+
mimeType
|
|
1041
|
+
};
|
|
1042
|
+
const appMeta = extractApplicationMeta(payload.meta);
|
|
1043
|
+
if (appMeta) {
|
|
1044
|
+
if (typeof appMeta.dedupScope === "string") {
|
|
1045
|
+
treeEntry.dedupScope = appMeta.dedupScope;
|
|
1046
|
+
delete appMeta.dedupScope;
|
|
1047
|
+
}
|
|
1048
|
+
if (typeof appMeta.dedupKey === "string") treeEntry.dedupKey = appMeta.dedupKey;
|
|
1049
|
+
if (Object.keys(appMeta).length > 0) treeEntry.metadata = appMeta;
|
|
1050
|
+
}
|
|
1051
|
+
if (typeof storeValue === "string") treeEntry.content = storeValue;
|
|
1052
|
+
else {
|
|
1053
|
+
const inlineText = decodeInlineText(bytes, mimeType);
|
|
1054
|
+
if (inlineText !== null) treeEntry.content = inlineText;
|
|
1055
|
+
}
|
|
1056
|
+
const { oldCid } = await this.treeIndex.put(treePath, treeEntry, { deviceId: writeDeviceId });
|
|
1057
|
+
if (this.gcQueue) {
|
|
1058
|
+
await this.gcQueue.remove(cid);
|
|
1059
|
+
if (oldCid && oldCid !== cid) await this.gcQueue.enqueue([oldCid]);
|
|
1060
|
+
}
|
|
1061
|
+
await applyShareWriteHook({
|
|
1062
|
+
...this.shareHookArgs(afsPath, treePath),
|
|
1063
|
+
bytes,
|
|
1064
|
+
cid,
|
|
1065
|
+
resolver: this.shareResolver,
|
|
1066
|
+
phase: "post-io"
|
|
1067
|
+
});
|
|
1068
|
+
await applyBlockletWriteHook({
|
|
1069
|
+
...this.blockletHookArgs(afsPath, treePath),
|
|
1070
|
+
bytes,
|
|
1071
|
+
resolver: this.blockletResolver
|
|
1072
|
+
});
|
|
1073
|
+
if (afsPath.endsWith(".ash")) this.emit({
|
|
1074
|
+
type: SCRIPT_EVENT_TYPES.REGISTERED,
|
|
1075
|
+
path: afsPath,
|
|
1076
|
+
data: { runtime: "ash" }
|
|
1077
|
+
});
|
|
1078
|
+
const meta = {
|
|
1079
|
+
...appMeta ?? {},
|
|
1080
|
+
kind: "did-space:file",
|
|
1081
|
+
size: treeEntry.size,
|
|
1082
|
+
cid,
|
|
1083
|
+
mimeType: treeEntry.mimeType
|
|
1084
|
+
};
|
|
1085
|
+
if (this.treeIndex.getPrefixWatermark) {
|
|
1086
|
+
const version = await this.treeIndex.getPrefixWatermark(this.treePath(afsPath));
|
|
1087
|
+
if (version != null) meta.liveSeq = String(version);
|
|
1088
|
+
}
|
|
1089
|
+
return { data: this.buildEntry(afsPath, {
|
|
1090
|
+
content: storeValue,
|
|
1091
|
+
meta,
|
|
1092
|
+
createdAt: new Date(treeEntry.ctime),
|
|
1093
|
+
updatedAt: new Date(treeEntry.mtime)
|
|
1094
|
+
}) };
|
|
1095
|
+
}
|
|
1096
|
+
async deleteHandler(ctx) {
|
|
1097
|
+
const afsPath = joinURL("/", ctx.params.path ?? "");
|
|
1098
|
+
if (this.isVirtualPath(afsPath)) throw new AFSForbiddenError(afsPath, "virtual_path");
|
|
1099
|
+
if (isShareReservedPath(afsPath)) throw new AFSForbiddenError(afsPath, "share-path-reserved");
|
|
1100
|
+
const treePath = this.treePath(afsPath);
|
|
1101
|
+
const deleteMeta = ctx.options?.meta;
|
|
1102
|
+
const deleteIfMatch = deleteMeta?.ifMatch;
|
|
1103
|
+
if (deleteIfMatch !== void 0) {
|
|
1104
|
+
const current = await this.treeIndex.get(treePath, { includeContent: false });
|
|
1105
|
+
if (!current || current.cid !== deleteIfMatch) throw new AFSConflictError(afsPath, {
|
|
1106
|
+
cid: current?.cid ?? null,
|
|
1107
|
+
mtime: current?.mtime ?? null
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
const deleteOpts = {
|
|
1111
|
+
deviceId: typeof deleteMeta?.deviceId === "string" && deleteMeta.deviceId ? deleteMeta.deviceId : this.deviceId,
|
|
1112
|
+
...typeof deleteMeta?.mtime === "number" ? { mtime: deleteMeta.mtime } : {}
|
|
1113
|
+
};
|
|
1114
|
+
const entry = await this.treeIndex.get(treePath, { includeContent: false });
|
|
1115
|
+
if (entry) {
|
|
1116
|
+
await this.gcQueue?.enqueue([entry.cid]);
|
|
1117
|
+
await this.treeIndex.delete(treePath, deleteOpts);
|
|
1118
|
+
await applyShareDeleteHook({
|
|
1119
|
+
...this.shareHookArgs(afsPath, treePath),
|
|
1120
|
+
resolver: this.shareResolver
|
|
1121
|
+
});
|
|
1122
|
+
await applyBlockletDeleteHook({
|
|
1123
|
+
...this.blockletHookArgs(afsPath, treePath),
|
|
1124
|
+
resolver: this.blockletResolver
|
|
1125
|
+
});
|
|
1126
|
+
if (afsPath.endsWith(".ash")) this.emit({
|
|
1127
|
+
type: SCRIPT_EVENT_TYPES.UNREGISTERED,
|
|
1128
|
+
path: afsPath,
|
|
1129
|
+
data: { runtime: "ash" }
|
|
1130
|
+
});
|
|
1131
|
+
return {
|
|
1132
|
+
path: afsPath,
|
|
1133
|
+
deleted: true
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
const prefix = this.treeListPrefix(afsPath);
|
|
1137
|
+
if (this.gcQueue) {
|
|
1138
|
+
const cids = await this.collectCIDsByPrefix(prefix);
|
|
1139
|
+
if (cids.length > 0) await this.gcQueue.enqueue(cids);
|
|
1140
|
+
}
|
|
1141
|
+
if (await this.treeIndex.deleteByPrefix(prefix, deleteOpts) === 0) throw new AFSNotFoundError(afsPath);
|
|
1142
|
+
return {
|
|
1143
|
+
path: afsPath,
|
|
1144
|
+
deleted: true
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
/** Conflict failure shaped exactly like the single-op AFSConflictError surface. */
|
|
1148
|
+
batchConflictFailure(afsPath, conflict) {
|
|
1149
|
+
return {
|
|
1150
|
+
path: afsPath,
|
|
1151
|
+
success: false,
|
|
1152
|
+
error: `Write conflict at ${afsPath}`,
|
|
1153
|
+
code: "AFS_CONFLICT",
|
|
1154
|
+
conflict
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/** ONE preflight read for a batch — getMany when the index has it, per-path get otherwise. */
|
|
1158
|
+
async batchPreread(treePaths) {
|
|
1159
|
+
if (this.treeIndex.getMany) return this.treeIndex.getMany(treePaths);
|
|
1160
|
+
return new Map(await Promise.all(treePaths.map(async (tp) => [tp, await this.treeIndex.get(tp)])));
|
|
1161
|
+
}
|
|
1162
|
+
/** Run `fn` over `items` with bounded concurrency (Workers cap: 6 sockets). */
|
|
1163
|
+
async boundedAll(items, limit, fn) {
|
|
1164
|
+
let next = 0;
|
|
1165
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1166
|
+
while (next < items.length) {
|
|
1167
|
+
const item = items[next++];
|
|
1168
|
+
await fn(item);
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
await Promise.all(workers);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Native batch read (issue #808 follow-up) — the render fast path. Resolves the
|
|
1175
|
+
* whole path set with ONE `treeIndex.getMany` (chunked `IN(…)`, which carries
|
|
1176
|
+
* inline content when the index has it), so small cid-verified inline files are
|
|
1177
|
+
* served with ZERO R2 gets; only the (few, large) non-inline entries fall back
|
|
1178
|
+
* to bounded-concurrent `objectStore.get`. Mirrors the single-read handler's
|
|
1179
|
+
* inline-verify → objects cascade per entry. File-oriented: getMany matches
|
|
1180
|
+
* `kind='file'`, so directory / absent paths come back AFS_NOT_FOUND (callers
|
|
1181
|
+
* reading directories use `list`/`read`).
|
|
1182
|
+
*/
|
|
1183
|
+
async batchRead(entries, _options) {
|
|
1184
|
+
const afsPaths = entries.map((e) => joinURL("/", e.path));
|
|
1185
|
+
const treePaths = afsPaths.map((p) => this.treePath(p));
|
|
1186
|
+
const preread = await this.batchPreread(treePaths);
|
|
1187
|
+
const results = new Array(entries.length);
|
|
1188
|
+
const fileEntry = (afsPath, entry, content) => this.buildEntry(afsPath, {
|
|
1189
|
+
content,
|
|
1190
|
+
meta: {
|
|
1191
|
+
...entry.metadata ?? {},
|
|
1192
|
+
kind: "did-space:file",
|
|
1193
|
+
size: entry.size,
|
|
1194
|
+
cid: entry.cid,
|
|
1195
|
+
mimeType: entry.mimeType,
|
|
1196
|
+
version: entry.cid
|
|
1197
|
+
},
|
|
1198
|
+
createdAt: new Date(entry.ctime),
|
|
1199
|
+
updatedAt: new Date(entry.mtime)
|
|
1200
|
+
});
|
|
1201
|
+
const needObject = [];
|
|
1202
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1203
|
+
const entry = preread.get(treePaths[i]);
|
|
1204
|
+
if (!entry) {
|
|
1205
|
+
results[i] = {
|
|
1206
|
+
path: afsPaths[i],
|
|
1207
|
+
success: false,
|
|
1208
|
+
error: `Path not found: ${afsPaths[i]}`,
|
|
1209
|
+
code: "AFS_NOT_FOUND"
|
|
1210
|
+
};
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if (entry.content !== void 0 && isTextMime(entry.mimeType)) {
|
|
1214
|
+
if (await computeCID(entry.content) === entry.cid) {
|
|
1215
|
+
results[i] = {
|
|
1216
|
+
path: afsPaths[i],
|
|
1217
|
+
success: true,
|
|
1218
|
+
data: fileEntry(afsPaths[i], entry, entry.content)
|
|
1219
|
+
};
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
log.warn(`[did-space] inline content failed cid verification at ${afsPaths[i]} (cid=${entry.cid}) — serving from objects; row will self-repair on next write`);
|
|
1223
|
+
}
|
|
1224
|
+
needObject.push(i);
|
|
1225
|
+
}
|
|
1226
|
+
await this.boundedAll(needObject, 6, async (i) => {
|
|
1227
|
+
const entry = preread.get(treePaths[i]);
|
|
1228
|
+
const obj = await this.objectStore.get(this.objectKey(entry.cid));
|
|
1229
|
+
const isText = isTextMime(entry.mimeType);
|
|
1230
|
+
const content = obj ? isText ? await obj.text() : await obj.arrayBuffer() : isText ? "" : /* @__PURE__ */ new ArrayBuffer(0);
|
|
1231
|
+
results[i] = {
|
|
1232
|
+
path: afsPaths[i],
|
|
1233
|
+
success: true,
|
|
1234
|
+
data: fileEntry(afsPaths[i], entry, content)
|
|
1235
|
+
};
|
|
1236
|
+
});
|
|
1237
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
1238
|
+
return {
|
|
1239
|
+
results,
|
|
1240
|
+
succeeded,
|
|
1241
|
+
failed: entries.length - succeeded
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Native batch write — the CF-subrequest-budget merge (§4.3): eight-step
|
|
1246
|
+
* order aligned point-for-point with the single write path (preread BEFORE
|
|
1247
|
+
* share pre-io, so a stale ifMatch on a `.share` declaration yields 409,
|
|
1248
|
+
* never a share error):
|
|
1249
|
+
* 1 shape/decode/computeCID → 2 ONE preread (conflict/skip exit here —
|
|
1250
|
+
* no share hook, no R2 upload) → 3 share pre-io (survivors) → 4 in-batch
|
|
1251
|
+
* CID dedup → 5 unconditional R2 put (unique cids, bounded ≤6 —
|
|
1252
|
+
* P0-measured wall-time) → 6 treeIndex.batchPutEntries (single db.batch)
|
|
1253
|
+
* → 7 gcQueue removeMany/enqueue → 8 share post-io + per-entry results.
|
|
1254
|
+
*/
|
|
1255
|
+
async batchWrite(entries, _options) {
|
|
1256
|
+
if (entries.some((e) => e.options?.dedupBy || e.options?.dedupKey)) {
|
|
1257
|
+
const seq = [];
|
|
1258
|
+
let succeeded$1 = 0;
|
|
1259
|
+
for (const entry of entries) {
|
|
1260
|
+
const afsPath = joinURL("/", entry.path);
|
|
1261
|
+
try {
|
|
1262
|
+
const payload = entry.content;
|
|
1263
|
+
if (!payload || payload.content === void 0) throw new AFSValidationError("Batch write requires entry content");
|
|
1264
|
+
const opts = {
|
|
1265
|
+
...entry.options ?? {},
|
|
1266
|
+
...entry.mode ? { mode: entry.mode } : {}
|
|
1267
|
+
};
|
|
1268
|
+
const res = await this.write(entry.path, payload, opts);
|
|
1269
|
+
seq.push({
|
|
1270
|
+
path: res.data.path,
|
|
1271
|
+
success: true,
|
|
1272
|
+
data: res.data
|
|
1273
|
+
});
|
|
1274
|
+
succeeded$1++;
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
seq.push({
|
|
1277
|
+
path: afsPath,
|
|
1278
|
+
success: false,
|
|
1279
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1280
|
+
code: err instanceof AFSConflictError ? "AFS_CONFLICT" : err?.code ?? "AFS_WRITE_ERROR"
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
results: seq,
|
|
1286
|
+
succeeded: succeeded$1,
|
|
1287
|
+
failed: entries.length - succeeded$1
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
const results = new Array(entries.length);
|
|
1291
|
+
const prepared = [];
|
|
1292
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1293
|
+
const entry = entries[i];
|
|
1294
|
+
const afsPath = joinURL("/", entry.path);
|
|
1295
|
+
if (entry.patches || entry.mode && entry.mode !== "replace") {
|
|
1296
|
+
results[i] = {
|
|
1297
|
+
path: afsPath,
|
|
1298
|
+
success: false,
|
|
1299
|
+
error: `Batch write supports replace-mode content writes only (got mode '${entry.mode ?? "patches"}')`,
|
|
1300
|
+
code: "AFS_UNSUPPORTED"
|
|
1301
|
+
};
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
if (this.isVirtualPath(afsPath) || isShareReservedPath(afsPath)) {
|
|
1305
|
+
results[i] = {
|
|
1306
|
+
path: afsPath,
|
|
1307
|
+
success: false,
|
|
1308
|
+
error: `Forbidden at ${afsPath}: virtual_path`,
|
|
1309
|
+
code: "AFS_FORBIDDEN"
|
|
1310
|
+
};
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
const payload = entry.content ?? {};
|
|
1314
|
+
if (payload.content === void 0) {
|
|
1315
|
+
results[i] = {
|
|
1316
|
+
path: afsPath,
|
|
1317
|
+
success: false,
|
|
1318
|
+
error: "Batch write requires entry content (meta-only writes are single-op)",
|
|
1319
|
+
code: "AFS_VALIDATION_ERROR"
|
|
1320
|
+
};
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
let bytes;
|
|
1324
|
+
let storeValue;
|
|
1325
|
+
if (payload.content instanceof Uint8Array) {
|
|
1326
|
+
bytes = payload.content;
|
|
1327
|
+
storeValue = bytes.buffer;
|
|
1328
|
+
} else if (payload.content instanceof ArrayBuffer) {
|
|
1329
|
+
bytes = new Uint8Array(payload.content);
|
|
1330
|
+
storeValue = payload.content;
|
|
1331
|
+
} else {
|
|
1332
|
+
const text = typeof payload.content === "string" ? payload.content : JSON.stringify(payload.content);
|
|
1333
|
+
bytes = new TextEncoder().encode(text);
|
|
1334
|
+
storeValue = text;
|
|
1335
|
+
}
|
|
1336
|
+
const cid = await computeCID(bytes);
|
|
1337
|
+
const writeOptions = entry.options;
|
|
1338
|
+
const writeMeta = writeOptions?.meta;
|
|
1339
|
+
const deviceId = typeof writeMeta?.deviceId === "string" && writeMeta.deviceId ? writeMeta.deviceId : this.deviceId;
|
|
1340
|
+
const now = typeof writeMeta?.mtime === "number" ? writeMeta.mtime : Date.now();
|
|
1341
|
+
let mimeType = payload.meta?.mimeType ?? detectMimeType(afsPath) ?? void 0;
|
|
1342
|
+
if (!mimeType && (payload.content instanceof Uint8Array || payload.content instanceof ArrayBuffer)) mimeType = "application/octet-stream";
|
|
1343
|
+
const ctime = resolveCtime(payload.meta, now);
|
|
1344
|
+
const treeEntry = {
|
|
1345
|
+
cid,
|
|
1346
|
+
size: bytes.length,
|
|
1347
|
+
mtime: now,
|
|
1348
|
+
ctime,
|
|
1349
|
+
mimeType
|
|
1350
|
+
};
|
|
1351
|
+
const appMeta = extractApplicationMeta(payload.meta);
|
|
1352
|
+
if (appMeta) {
|
|
1353
|
+
try {
|
|
1354
|
+
assertMetadataSize(appMeta, afsPath);
|
|
1355
|
+
} catch (err) {
|
|
1356
|
+
results[i] = {
|
|
1357
|
+
path: afsPath,
|
|
1358
|
+
success: false,
|
|
1359
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1360
|
+
code: err.code ?? "AFS_VALIDATION_ERROR"
|
|
1361
|
+
};
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
treeEntry.metadata = appMeta;
|
|
1365
|
+
}
|
|
1366
|
+
if (typeof storeValue === "string") treeEntry.content = storeValue;
|
|
1367
|
+
else {
|
|
1368
|
+
const inlineText = decodeInlineText(bytes, mimeType);
|
|
1369
|
+
if (inlineText !== null) treeEntry.content = inlineText;
|
|
1370
|
+
}
|
|
1371
|
+
prepared.push({
|
|
1372
|
+
i,
|
|
1373
|
+
afsPath,
|
|
1374
|
+
treePath: this.treePath(afsPath),
|
|
1375
|
+
bytes,
|
|
1376
|
+
storeValue,
|
|
1377
|
+
cid,
|
|
1378
|
+
treeEntry,
|
|
1379
|
+
deviceId,
|
|
1380
|
+
ifMatch: writeOptions?.ifMatch
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
const preread = await this.batchPreread(prepared.map((p) => p.treePath));
|
|
1384
|
+
const skipped = [];
|
|
1385
|
+
let live = [];
|
|
1386
|
+
for (const p of prepared) {
|
|
1387
|
+
const existing = preread.get(p.treePath) ?? null;
|
|
1388
|
+
if (ifMatchPreconditionFailed(p.ifMatch, existing)) {
|
|
1389
|
+
results[p.i] = this.batchConflictFailure(p.afsPath, {
|
|
1390
|
+
currentCid: existing?.cid ?? null,
|
|
1391
|
+
currentMtime: existing?.mtime ?? null,
|
|
1392
|
+
currentDeviceId: null
|
|
1393
|
+
});
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
live.push(p);
|
|
1397
|
+
}
|
|
1398
|
+
const preIoOk = [];
|
|
1399
|
+
for (const p of live) try {
|
|
1400
|
+
await applyShareWriteHook({
|
|
1401
|
+
...this.shareHookArgs(p.afsPath, p.treePath),
|
|
1402
|
+
bytes: p.bytes,
|
|
1403
|
+
cid: p.cid,
|
|
1404
|
+
resolver: this.shareResolver,
|
|
1405
|
+
phase: "pre-io"
|
|
1406
|
+
});
|
|
1407
|
+
preIoOk.push(p);
|
|
1408
|
+
} catch (err) {
|
|
1409
|
+
results[p.i] = {
|
|
1410
|
+
path: p.afsPath,
|
|
1411
|
+
success: false,
|
|
1412
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1413
|
+
code: err.code
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
live = preIoOk;
|
|
1417
|
+
const uniqueCids = /* @__PURE__ */ new Map();
|
|
1418
|
+
for (const p of live) if (!uniqueCids.has(p.cid)) uniqueCids.set(p.cid, p);
|
|
1419
|
+
const uploadFailed = /* @__PURE__ */ new Map();
|
|
1420
|
+
const uploaded = [];
|
|
1421
|
+
await this.boundedAll([...uniqueCids.values()], 6, async (p) => {
|
|
1422
|
+
try {
|
|
1423
|
+
await this.objectStore.put(this.objectKey(p.cid), p.storeValue);
|
|
1424
|
+
uploaded.push(p.cid);
|
|
1425
|
+
} catch (err) {
|
|
1426
|
+
uploadFailed.set(p.cid, err instanceof Error ? err.message : String(err));
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
if (uploadFailed.size > 0) {
|
|
1430
|
+
const stillLive = [];
|
|
1431
|
+
for (const p of live) {
|
|
1432
|
+
const failure = uploadFailed.get(p.cid);
|
|
1433
|
+
if (failure !== void 0) results[p.i] = {
|
|
1434
|
+
path: p.afsPath,
|
|
1435
|
+
success: false,
|
|
1436
|
+
error: `R2 put failed: ${failure}`
|
|
1437
|
+
};
|
|
1438
|
+
else stillLive.push(p);
|
|
1439
|
+
}
|
|
1440
|
+
live = stillLive;
|
|
1441
|
+
}
|
|
1442
|
+
let indexResults = [];
|
|
1443
|
+
if (live.length > 0) {
|
|
1444
|
+
const batchEntries = live.map((p) => ({
|
|
1445
|
+
path: p.treePath,
|
|
1446
|
+
entry: p.treeEntry,
|
|
1447
|
+
opts: { deviceId: p.deviceId }
|
|
1448
|
+
}));
|
|
1449
|
+
try {
|
|
1450
|
+
indexResults = this.treeIndex.batchPutEntries ? await this.treeIndex.batchPutEntries(batchEntries, { preread }) : await loopBatchPutEntries(this.treeIndex, batchEntries, { preread });
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
await this.gcQueue?.enqueue(uploaded);
|
|
1453
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1454
|
+
for (const p of live) results[p.i] = {
|
|
1455
|
+
path: p.afsPath,
|
|
1456
|
+
success: false,
|
|
1457
|
+
error: message
|
|
1458
|
+
};
|
|
1459
|
+
live = [];
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const written = [];
|
|
1463
|
+
live.forEach((p, j) => {
|
|
1464
|
+
const r = indexResults[j];
|
|
1465
|
+
if (!r) {
|
|
1466
|
+
results[p.i] = {
|
|
1467
|
+
path: p.afsPath,
|
|
1468
|
+
success: false,
|
|
1469
|
+
error: "index batch returned no result"
|
|
1470
|
+
};
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
if (r.conflict) {
|
|
1474
|
+
results[p.i] = this.batchConflictFailure(p.afsPath, r.conflict);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
if (r.error) {
|
|
1478
|
+
results[p.i] = {
|
|
1479
|
+
path: p.afsPath,
|
|
1480
|
+
success: false,
|
|
1481
|
+
error: r.error
|
|
1482
|
+
};
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (r.skipped) {
|
|
1486
|
+
skipped.push(p);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
written.push({
|
|
1490
|
+
p,
|
|
1491
|
+
oldCid: r.oldCid,
|
|
1492
|
+
liveSeq: r.liveSeq
|
|
1493
|
+
});
|
|
1494
|
+
});
|
|
1495
|
+
if (this.gcQueue) {
|
|
1496
|
+
const reReferenced = [...new Set([...written.map((w) => w.p.cid), ...skipped.map((s) => s.cid)])];
|
|
1497
|
+
if (reReferenced.length > 0) if (this.gcQueue.removeMany) await this.gcQueue.removeMany(reReferenced);
|
|
1498
|
+
else for (const cid of reReferenced) await this.gcQueue.remove(cid);
|
|
1499
|
+
const replaced = [...new Set(written.filter((w) => w.oldCid && w.oldCid !== w.p.cid).map((w) => w.oldCid))];
|
|
1500
|
+
if (replaced.length > 0) await this.gcQueue.enqueue(replaced);
|
|
1501
|
+
}
|
|
1502
|
+
for (const w of written) {
|
|
1503
|
+
try {
|
|
1504
|
+
await applyShareWriteHook({
|
|
1505
|
+
...this.shareHookArgs(w.p.afsPath, w.p.treePath),
|
|
1506
|
+
bytes: w.p.bytes,
|
|
1507
|
+
cid: w.p.cid,
|
|
1508
|
+
resolver: this.shareResolver,
|
|
1509
|
+
phase: "post-io"
|
|
1510
|
+
});
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
w.error = err instanceof Error ? err.message : String(err);
|
|
1513
|
+
}
|
|
1514
|
+
if (w.p.afsPath.endsWith(".ash")) this.emit({
|
|
1515
|
+
type: SCRIPT_EVENT_TYPES.REGISTERED,
|
|
1516
|
+
path: w.p.afsPath,
|
|
1517
|
+
data: { runtime: "ash" }
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
const maxSeqByPrefix = /* @__PURE__ */ new Map();
|
|
1521
|
+
for (const w of written) {
|
|
1522
|
+
if (typeof w.liveSeq !== "number") continue;
|
|
1523
|
+
const wm = watermarkWritePrefix(w.p.treePath);
|
|
1524
|
+
if (wm === null) continue;
|
|
1525
|
+
maxSeqByPrefix.set(wm, Math.max(maxSeqByPrefix.get(wm) ?? 0, w.liveSeq));
|
|
1526
|
+
}
|
|
1527
|
+
for (const s of skipped) {
|
|
1528
|
+
const wm = watermarkWritePrefix(s.treePath);
|
|
1529
|
+
let liveSeq = wm !== null ? maxSeqByPrefix.get(wm) : void 0;
|
|
1530
|
+
if (liveSeq === void 0 && this.treeIndex.getPrefixWatermark) liveSeq = await this.treeIndex.getPrefixWatermark(s.treePath) ?? void 0;
|
|
1531
|
+
results[s.i] = {
|
|
1532
|
+
path: s.afsPath,
|
|
1533
|
+
success: true,
|
|
1534
|
+
data: this.buildBatchWriteData(s, liveSeq)
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
for (const w of written) {
|
|
1538
|
+
if (w.error) {
|
|
1539
|
+
results[w.p.i] = {
|
|
1540
|
+
path: w.p.afsPath,
|
|
1541
|
+
success: false,
|
|
1542
|
+
error: w.error
|
|
1543
|
+
};
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
results[w.p.i] = {
|
|
1547
|
+
path: w.p.afsPath,
|
|
1548
|
+
success: true,
|
|
1549
|
+
data: this.buildBatchWriteData(w.p, w.liveSeq)
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
const succeeded = results.filter((r) => r?.success).length;
|
|
1553
|
+
return {
|
|
1554
|
+
results,
|
|
1555
|
+
succeeded,
|
|
1556
|
+
failed: results.length - succeeded
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
/** Write-echo entry — field-for-field the single-op writeHandler result. */
|
|
1560
|
+
buildBatchWriteData(p, liveSeq) {
|
|
1561
|
+
const meta = {
|
|
1562
|
+
kind: "did-space:file",
|
|
1563
|
+
size: p.treeEntry.size,
|
|
1564
|
+
cid: p.cid,
|
|
1565
|
+
mimeType: p.treeEntry.mimeType
|
|
1566
|
+
};
|
|
1567
|
+
if (liveSeq != null) meta.liveSeq = String(liveSeq);
|
|
1568
|
+
return this.buildEntry(p.afsPath, {
|
|
1569
|
+
content: p.storeValue,
|
|
1570
|
+
meta,
|
|
1571
|
+
createdAt: new Date(p.treeEntry.ctime),
|
|
1572
|
+
updatedAt: new Date(p.treeEntry.mtime)
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
/** Build a failed AFSExecResult (shared across the three action handlers). */
|
|
1576
|
+
uploadErr(code, message) {
|
|
1577
|
+
return {
|
|
1578
|
+
success: false,
|
|
1579
|
+
error: {
|
|
1580
|
+
code,
|
|
1581
|
+
message
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Session ownership isolation (§9 / §10): a session belongs to THIS view only
|
|
1587
|
+
* when its scope equals this provider's storage scope. A different-scope (or
|
|
1588
|
+
* unknown) session is treated as not-found — never confirm another user's
|
|
1589
|
+
* session exists, and never let one view commit into another.
|
|
1590
|
+
*/
|
|
1591
|
+
ownsSession(session) {
|
|
1592
|
+
return !!session && session.scope === (this.uploadScope ?? this.did);
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Validate an action-supplied `path`. Unlike `@Write` handlers, action args
|
|
1596
|
+
* carry NO router-layer traversal protection — `args.entries[].path` is raw
|
|
1597
|
+
* client input, so we MUST normalize + reject traversal / reserved virtual
|
|
1598
|
+
* prefixes explicitly (design §10). Returns the tree path on success.
|
|
1599
|
+
*/
|
|
1600
|
+
validateActionPath(afsPath) {
|
|
1601
|
+
const norm = afsPath.startsWith("/") ? afsPath.slice(1) : afsPath;
|
|
1602
|
+
if (this.isVirtualPath(afsPath) || isShareReservedPath(afsPath) || norm === ".actions" || norm.startsWith(".actions/")) return {
|
|
1603
|
+
ok: false,
|
|
1604
|
+
reason: `reserved path: ${afsPath}`
|
|
1605
|
+
};
|
|
1606
|
+
let treePath;
|
|
1607
|
+
try {
|
|
1608
|
+
treePath = this.treePath(afsPath);
|
|
1609
|
+
} catch (err) {
|
|
1610
|
+
return {
|
|
1611
|
+
ok: false,
|
|
1612
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
return {
|
|
1616
|
+
ok: true,
|
|
1617
|
+
treePath
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Action catalogue — the upload-plane + download-plane actions, always listed
|
|
1622
|
+
* (protocol parity, T2.5). Each plane advertises its OWN capability flag:
|
|
1623
|
+
* `grantCapability` for prepare-upload, the INDEPENDENT `downloadGrantCapability`
|
|
1624
|
+
* for prepare-download (design §7.3 / Finding 4 — never reuse the upload flag,
|
|
1625
|
+
* so the two planes can be advertised separately).
|
|
1626
|
+
*/
|
|
1627
|
+
listObjectPlaneActions(ctx) {
|
|
1628
|
+
const grantCapable = this.signer != null;
|
|
1629
|
+
const downloadGrantCapable = this.signer != null && typeof this.signer.presignGet === "function";
|
|
1630
|
+
return { data: [...UPLOAD_ACTIONS, ...DOWNLOAD_ACTIONS].map((name) => this.buildEntry(joinURL(ctx.path, name), {
|
|
1631
|
+
id: name,
|
|
1632
|
+
meta: {
|
|
1633
|
+
kind: "afs:executable",
|
|
1634
|
+
...name === "prepare-upload" ? { grantCapability: grantCapable } : {},
|
|
1635
|
+
...name === "prepare-download" ? { downloadGrantCapability: downloadGrantCapable } : {}
|
|
1636
|
+
}
|
|
1637
|
+
})) };
|
|
1638
|
+
}
|
|
1639
|
+
async execPrepareUpload(_ctx, args) {
|
|
1640
|
+
if (!this.uploadSessionStore) return this.uploadErr("AFS_UNSUPPORTED", "upload plane not available");
|
|
1641
|
+
const parsed = PrepareUploadArgsSchema.safeParse(args);
|
|
1642
|
+
if (!parsed.success) return this.uploadErr("AFS_VALIDATION_ERROR", zodMessage(parsed.error));
|
|
1643
|
+
const outEntries = [];
|
|
1644
|
+
const storeEntries = [];
|
|
1645
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
1646
|
+
for (const e of parsed.data.entries) {
|
|
1647
|
+
const afsPath = joinURL("/", e.path);
|
|
1648
|
+
const pv = this.validateActionPath(afsPath);
|
|
1649
|
+
if (!pv.ok) return this.uploadErr("AFS_VALIDATION_ERROR", pv.reason);
|
|
1650
|
+
if (seenPaths.has(afsPath)) return this.uploadErr("AFS_VALIDATION_ERROR", `duplicate path: ${afsPath}`);
|
|
1651
|
+
seenPaths.add(afsPath);
|
|
1652
|
+
let checksum;
|
|
1653
|
+
try {
|
|
1654
|
+
checksum = cidToChecksumSha256(e.cid);
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
return this.uploadErr("AFS_VALIDATION_ERROR", `bad cid for ${afsPath}: ${err instanceof Error ? err.message : err}`);
|
|
1657
|
+
}
|
|
1658
|
+
const objectKey = this.objectKey(e.cid);
|
|
1659
|
+
let grant = null;
|
|
1660
|
+
if (this.signer) {
|
|
1661
|
+
const g = await this.signer.presignPut({
|
|
1662
|
+
objectKey,
|
|
1663
|
+
checksumSha256: checksum,
|
|
1664
|
+
expiresInSec: GRANT_EXPIRES_SEC,
|
|
1665
|
+
contentLength: e.size
|
|
1666
|
+
});
|
|
1667
|
+
grant = {
|
|
1668
|
+
kind: "presigned-put",
|
|
1669
|
+
url: g.url,
|
|
1670
|
+
method: g.method,
|
|
1671
|
+
signedHeaders: g.signedHeaders,
|
|
1672
|
+
expiresAt: g.expiresAt
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
outEntries.push({
|
|
1676
|
+
path: afsPath,
|
|
1677
|
+
cid: e.cid,
|
|
1678
|
+
status: "needs-upload",
|
|
1679
|
+
objectKey,
|
|
1680
|
+
checksumSha256: checksum,
|
|
1681
|
+
grant
|
|
1682
|
+
});
|
|
1683
|
+
storeEntries.push({
|
|
1684
|
+
path: afsPath,
|
|
1685
|
+
cid: e.cid,
|
|
1686
|
+
objectKey,
|
|
1687
|
+
checksumSha256: checksum,
|
|
1688
|
+
size: e.size,
|
|
1689
|
+
mimeType: e.mimeType ?? null,
|
|
1690
|
+
ifMatch: e.ifMatch ?? null,
|
|
1691
|
+
deviceId: e.deviceId ?? null,
|
|
1692
|
+
mtime: e.mtime ?? null,
|
|
1693
|
+
status: "needs_upload"
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
const sessionId = crypto.randomUUID();
|
|
1697
|
+
const expiresAt = Date.now() + UPLOAD_SESSION_TTL_MS;
|
|
1698
|
+
await this.uploadSessionStore.createSession({
|
|
1699
|
+
id: sessionId,
|
|
1700
|
+
scope: this.uploadScope ?? this.did,
|
|
1701
|
+
caller: null,
|
|
1702
|
+
expiresAt,
|
|
1703
|
+
entries: storeEntries
|
|
1704
|
+
});
|
|
1705
|
+
return {
|
|
1706
|
+
success: true,
|
|
1707
|
+
data: {
|
|
1708
|
+
sessionId,
|
|
1709
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
1710
|
+
entries: outEntries
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
async execPrepareDownload(_ctx, args) {
|
|
1715
|
+
const parsed = PrepareDownloadArgsSchema.safeParse(args);
|
|
1716
|
+
if (!parsed.success) return this.uploadErr("AFS_VALIDATION_ERROR", zodMessage(parsed.error));
|
|
1717
|
+
const inputEntries = parsed.data.entries;
|
|
1718
|
+
const cidValid = /* @__PURE__ */ new Map();
|
|
1719
|
+
const validCids = [];
|
|
1720
|
+
for (const e of inputEntries) {
|
|
1721
|
+
if (cidValid.has(e.cid)) continue;
|
|
1722
|
+
let ok = true;
|
|
1723
|
+
try {
|
|
1724
|
+
cidToChecksumSha256(e.cid);
|
|
1725
|
+
} catch {
|
|
1726
|
+
ok = false;
|
|
1727
|
+
}
|
|
1728
|
+
cidValid.set(e.cid, ok);
|
|
1729
|
+
if (ok) validCids.push(e.cid);
|
|
1730
|
+
}
|
|
1731
|
+
let referenced = /* @__PURE__ */ new Set();
|
|
1732
|
+
if (validCids.length > 0) try {
|
|
1733
|
+
referenced = await this.treeIndex.isReferencedMany(validCids);
|
|
1734
|
+
} catch {
|
|
1735
|
+
return this.uploadErr("AFS_INTERNAL_ERROR", "prepare-download authorization unavailable");
|
|
1736
|
+
}
|
|
1737
|
+
const canSign = this.signer != null && typeof this.signer.presignGet === "function";
|
|
1738
|
+
const grantByCid = /* @__PURE__ */ new Map();
|
|
1739
|
+
for (const cid of referenced) if (canSign) {
|
|
1740
|
+
const g = await this.signer.presignGet({
|
|
1741
|
+
objectKey: this.objectKey(cid),
|
|
1742
|
+
expiresInSec: DOWNLOAD_GRANT_TTL_SEC
|
|
1743
|
+
});
|
|
1744
|
+
grantByCid.set(cid, {
|
|
1745
|
+
kind: "presigned-get",
|
|
1746
|
+
url: g.url,
|
|
1747
|
+
method: g.method,
|
|
1748
|
+
signedHeaders: g.signedHeaders,
|
|
1749
|
+
expiresAt: g.expiresAt
|
|
1750
|
+
});
|
|
1751
|
+
} else grantByCid.set(cid, null);
|
|
1752
|
+
return {
|
|
1753
|
+
success: true,
|
|
1754
|
+
data: { entries: inputEntries.map((e) => {
|
|
1755
|
+
if (!cidValid.get(e.cid)) return {
|
|
1756
|
+
cid: e.cid,
|
|
1757
|
+
status: "invalid"
|
|
1758
|
+
};
|
|
1759
|
+
if (!referenced.has(e.cid)) return {
|
|
1760
|
+
cid: e.cid,
|
|
1761
|
+
status: "forbidden"
|
|
1762
|
+
};
|
|
1763
|
+
return {
|
|
1764
|
+
cid: e.cid,
|
|
1765
|
+
status: "ok",
|
|
1766
|
+
objectKey: this.objectKey(e.cid),
|
|
1767
|
+
grant: grantByCid.get(e.cid) ?? null
|
|
1768
|
+
};
|
|
1769
|
+
}) }
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
async execCommitUpload(_ctx, args) {
|
|
1773
|
+
if (!this.uploadSessionStore) return this.uploadErr("AFS_UNSUPPORTED", "upload plane not available");
|
|
1774
|
+
const parsed = CommitUploadArgsSchema.safeParse(args);
|
|
1775
|
+
if (!parsed.success) return this.uploadErr("AFS_VALIDATION_ERROR", zodMessage(parsed.error));
|
|
1776
|
+
const { sessionId, entries } = parsed.data;
|
|
1777
|
+
const session = await this.uploadSessionStore.getSession(sessionId);
|
|
1778
|
+
if (!this.ownsSession(session)) return this.uploadErr("AFS_NOT_FOUND", "unknown upload session");
|
|
1779
|
+
if (session.status === "aborted") return this.uploadErr("AFS_UNSUPPORTED", "session aborted");
|
|
1780
|
+
if (session.status === "prepared" && session.expiresAt <= Date.now()) return this.uploadErr("AFS_UNSUPPORTED", "session expired");
|
|
1781
|
+
const result = await this.commitSessionEntries(session.id, session.entries, entries);
|
|
1782
|
+
await this.uploadSessionStore.markCommitted(session.id);
|
|
1783
|
+
return {
|
|
1784
|
+
success: true,
|
|
1785
|
+
data: result
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
async execAbortUpload(_ctx, args) {
|
|
1789
|
+
if (!this.uploadSessionStore) return this.uploadErr("AFS_UNSUPPORTED", "upload plane not available");
|
|
1790
|
+
const parsed = AbortUploadArgsSchema.safeParse(args);
|
|
1791
|
+
if (!parsed.success) return this.uploadErr("AFS_VALIDATION_ERROR", zodMessage(parsed.error));
|
|
1792
|
+
const session = await this.uploadSessionStore.getSession(parsed.data.sessionId);
|
|
1793
|
+
if (!this.ownsSession(session)) return this.uploadErr("AFS_NOT_FOUND", "unknown upload session");
|
|
1794
|
+
await this.uploadSessionStore.markAborted(session.id);
|
|
1795
|
+
if (this.gcQueue) {
|
|
1796
|
+
const orphanCids = [...new Set(session.entries.filter((e) => e.status !== "committed").map((e) => e.cid))];
|
|
1797
|
+
if (orphanCids.length > 0) await this.gcQueue.enqueue(orphanCids);
|
|
1798
|
+
}
|
|
1799
|
+
return {
|
|
1800
|
+
success: true,
|
|
1801
|
+
data: {
|
|
1802
|
+
sessionId: session.id,
|
|
1803
|
+
status: "aborted"
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Resolve the immutable-merge-key set: `createdAt` is always preserved, plus
|
|
1809
|
+
* any caller-supplied lifecycle fields (e.g. `status`).
|
|
1810
|
+
*/
|
|
1811
|
+
resolveImmutableMergeKeys(extra) {
|
|
1812
|
+
const keys = new Set(["createdAt"]);
|
|
1813
|
+
for (const k of extra ?? []) keys.add(k);
|
|
1814
|
+
return keys;
|
|
1815
|
+
}
|
|
1816
|
+
/** Extract the JSON record object from a write payload's `content`. */
|
|
1817
|
+
parseRecordBody(content) {
|
|
1818
|
+
if (content !== null && typeof content === "object" && !Array.isArray(content) && !(content instanceof Uint8Array) && !(content instanceof ArrayBuffer)) return content;
|
|
1819
|
+
if (typeof content === "string") try {
|
|
1820
|
+
const v = JSON.parse(content);
|
|
1821
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
1822
|
+
} catch {
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Stamp the dedup identity into a write's meta so the writeHandler can project
|
|
1829
|
+
* it onto the entry's `dedup_scope` / `dedup_key` columns (#1159). `dedupKey`
|
|
1830
|
+
* stays in the metadata blob (observability / collection-query); on a merge
|
|
1831
|
+
* we drop `createdAt` so the index keeps the original file ctime (R1,
|
|
1832
|
+
* feed-sort §5.1).
|
|
1833
|
+
*/
|
|
1834
|
+
stampDedupMeta(base, dedupScope, dedupKey, isDuplicate) {
|
|
1835
|
+
const meta = { ...base ?? {} };
|
|
1836
|
+
meta.dedupKey = dedupKey;
|
|
1837
|
+
meta.dedupScope = dedupScope;
|
|
1838
|
+
if (isDuplicate) delete meta.createdAt;
|
|
1839
|
+
return meta;
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Dedup-on-write core (#492 / #1159) — the `options.dedupBy` upsert behind the
|
|
1843
|
+
* standard `write` / `batchWrite`. The record's `dedupBy` field (URL-valued)
|
|
1844
|
+
* is normalized into a `dedupKey`; the record lands at a STABLE readable path
|
|
1845
|
+
* (`{collection}/{uuid}.json`) and dedup resolves through the tree-index's
|
|
1846
|
+
* `(scope, dedup_scope, dedup_key)` secondary UNIQUE index — NOT a
|
|
1847
|
+
* cid-in-path filename (which silently orphaned records written under any
|
|
1848
|
+
* other layout, #1159). On a hit the incoming record is MERGED
|
|
1849
|
+
* (non-degrading) into the existing entry AT ITS REAL PATH. Concurrency-safe:
|
|
1850
|
+
* a racing create collides on the UNIQUE index, and the loser re-queries the
|
|
1851
|
+
* index and merges (bounded retry). Records missing the field are written
|
|
1852
|
+
* normally under the collection (no dedup).
|
|
1853
|
+
*/
|
|
1854
|
+
async upsertByDedupKey(path, content, options, dedupBy) {
|
|
1855
|
+
if (!this.treeIndex.findByDedupKey) throw new AFSValidationError("dedup-on-write (dedupBy) is not supported by this DID Space index");
|
|
1856
|
+
const { dedupBy: _by, dedupKey: _dk, mergeImmutableFields, ...baseOpts } = options;
|
|
1857
|
+
const body = this.parseRecordBody(content.content);
|
|
1858
|
+
const rawVal = body && typeof body[dedupBy] === "string" ? body[dedupBy] : null;
|
|
1859
|
+
if (!body || !rawVal) {
|
|
1860
|
+
const fresh = joinURL("/", path, `${crypto.randomUUID()}.json`);
|
|
1861
|
+
return {
|
|
1862
|
+
...await super.write(fresh, content, baseOpts),
|
|
1863
|
+
deduped: false
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
const dedupKey = normalizeItemUrl(rawVal);
|
|
1867
|
+
const dedupScope = this.treePath(joinURL("/", path));
|
|
1868
|
+
const immutable = this.resolveImmutableMergeKeys(mergeImmutableFields);
|
|
1869
|
+
const MAX_ATTEMPTS = 4;
|
|
1870
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1871
|
+
const existing = await this.treeIndex.findByDedupKey(dedupScope, dedupKey);
|
|
1872
|
+
if (existing) {
|
|
1873
|
+
const target = `/${existing.path}`;
|
|
1874
|
+
let existingBody = null;
|
|
1875
|
+
let version;
|
|
1876
|
+
try {
|
|
1877
|
+
const readResult = await this.read(target);
|
|
1878
|
+
existingBody = this.parseRecordBody(readResult.data?.content);
|
|
1879
|
+
version = (readResult.data?.meta)?.version;
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
if (err instanceof AFSNotFoundError) continue;
|
|
1882
|
+
throw err;
|
|
1883
|
+
}
|
|
1884
|
+
const mergedBody = existingBody !== null ? mergeItemContent(existingBody, body, immutable) : { ...body };
|
|
1885
|
+
const meta$1 = this.stampDedupMeta(content.meta, dedupScope, dedupKey, true);
|
|
1886
|
+
const callerIfMatch = baseOpts.ifMatch;
|
|
1887
|
+
const ifMatch = callerIfMatch ?? version;
|
|
1888
|
+
try {
|
|
1889
|
+
return {
|
|
1890
|
+
...await super.write(target, {
|
|
1891
|
+
...content,
|
|
1892
|
+
content: mergedBody,
|
|
1893
|
+
meta: meta$1
|
|
1894
|
+
}, ifMatch ? {
|
|
1895
|
+
...baseOpts,
|
|
1896
|
+
ifMatch
|
|
1897
|
+
} : baseOpts),
|
|
1898
|
+
deduped: true
|
|
1899
|
+
};
|
|
1900
|
+
} catch (err) {
|
|
1901
|
+
if (err instanceof AFSConflictError && callerIfMatch === void 0) continue;
|
|
1902
|
+
throw err;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
const fresh = joinURL("/", path, `${crypto.randomUUID()}.json`);
|
|
1906
|
+
const meta = this.stampDedupMeta(content.meta, dedupScope, dedupKey, false);
|
|
1907
|
+
try {
|
|
1908
|
+
return {
|
|
1909
|
+
...await super.write(fresh, {
|
|
1910
|
+
...content,
|
|
1911
|
+
content: body,
|
|
1912
|
+
meta
|
|
1913
|
+
}, baseOpts),
|
|
1914
|
+
deduped: false
|
|
1915
|
+
};
|
|
1916
|
+
} catch (err) {
|
|
1917
|
+
if (await this.treeIndex.findByDedupKey(dedupScope, dedupKey)) continue;
|
|
1918
|
+
throw err;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
throw new AFSConflictError(joinURL("/", path), {});
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* One-time reindex/backfill seam (#1159 — run from a deferred one-shot
|
|
1925
|
+
* script, NOT on the write hot path). Populates the dedup columns for
|
|
1926
|
+
* EXISTING records under `collection` so pre-feature entries (old
|
|
1927
|
+
* `{slug}.json` / `{uuid}/item.json` / `{cid}.json` layouts) become
|
|
1928
|
+
* dedup-able — fixing the "an already-saved URL's first re-save duplicates"
|
|
1929
|
+
* bug. Reads each record, extracts + normalizes `record[dedupBy]`, and writes
|
|
1930
|
+
* the dedup columns INDEX-ONLY (no changelog / no cid change) via
|
|
1931
|
+
* `TreeIndex.setDedupKey`. Records missing the field, and genuine
|
|
1932
|
+
* pre-existing duplicates (which would violate the UNIQUE index), are
|
|
1933
|
+
* skipped. Idempotent.
|
|
1934
|
+
*/
|
|
1935
|
+
async reindexDedupKeys(collection, dedupBy) {
|
|
1936
|
+
if (!this.treeIndex.setDedupKey) throw new AFSValidationError("reindexDedupKeys is not supported by this DID Space index");
|
|
1937
|
+
const dedupScope = this.treePath(joinURL("/", collection));
|
|
1938
|
+
const listPrefix = dedupScope.endsWith("/") ? dedupScope : `${dedupScope}/`;
|
|
1939
|
+
let scanned = 0;
|
|
1940
|
+
let indexed = 0;
|
|
1941
|
+
let skipped = 0;
|
|
1942
|
+
let cursor;
|
|
1943
|
+
do {
|
|
1944
|
+
const page = await this.treeIndex.listEntries(listPrefix, {
|
|
1945
|
+
limit: 500,
|
|
1946
|
+
cursor
|
|
1947
|
+
});
|
|
1948
|
+
for (const e of page.entries) {
|
|
1949
|
+
scanned++;
|
|
1950
|
+
let body = null;
|
|
1951
|
+
try {
|
|
1952
|
+
const r = await this.read(`/${e.path}`);
|
|
1953
|
+
body = this.parseRecordBody(r.data?.content);
|
|
1954
|
+
} catch {
|
|
1955
|
+
skipped++;
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
const raw = body && typeof body[dedupBy] === "string" ? body[dedupBy] : null;
|
|
1959
|
+
if (!raw) {
|
|
1960
|
+
skipped++;
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
try {
|
|
1964
|
+
await this.treeIndex.setDedupKey(e.path, dedupScope, normalizeItemUrl(raw), { deviceId: this.deviceId });
|
|
1965
|
+
indexed++;
|
|
1966
|
+
} catch {
|
|
1967
|
+
skipped++;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
cursor = page.hasMore ? page.cursor : void 0;
|
|
1971
|
+
} while (cursor);
|
|
1972
|
+
return {
|
|
1973
|
+
scanned,
|
|
1974
|
+
indexed,
|
|
1975
|
+
skipped
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Standard write, extended with the dedup-on-write upsert policy (#492 /
|
|
1980
|
+
* #1159). When `options.dedupBy` (canonical) or the deprecated `dedupKey`
|
|
1981
|
+
* alias is set the write becomes a secondary-index upsert (see
|
|
1982
|
+
* {@link upsertByDedupKey}); otherwise it is the base provider's write.
|
|
1983
|
+
*/
|
|
1984
|
+
async write(path, content, options) {
|
|
1985
|
+
const dedupBy = options?.dedupBy ?? options?.dedupKey;
|
|
1986
|
+
if (dedupBy) return this.upsertByDedupKey(path, content, options, dedupBy);
|
|
1987
|
+
return super.write(path, content, options);
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* The commit write — mirrors `batchWrite` steps 2/6/7/8 WITHOUT the R2 byte
|
|
1991
|
+
* leg (existence is trusted, §8): preread → ifMatch/skip → batchPutEntries →
|
|
1992
|
+
* GC bookkeeping → share hooks → per-entry results. `cid`/`size`/`mimeType`
|
|
1993
|
+
* come from the SESSION record (the prepared contract, re-validated against
|
|
1994
|
+
* the client's entries); `deviceId`/`mtime` are client provenance threaded to
|
|
1995
|
+
* the changelog (echo防回环, §6.1).
|
|
1996
|
+
*/
|
|
1997
|
+
async commitSessionEntries(sessionId, sessionEntries, clientEntries) {
|
|
1998
|
+
const byPath = new Map(sessionEntries.map((e) => [joinURL("/", e.path), e]));
|
|
1999
|
+
const results = new Array(clientEntries.length);
|
|
2000
|
+
const prepared = [];
|
|
2001
|
+
for (let i = 0; i < clientEntries.length; i++) {
|
|
2002
|
+
const c = clientEntries[i];
|
|
2003
|
+
const afsPath = joinURL("/", c.path);
|
|
2004
|
+
const pv = this.validateActionPath(afsPath);
|
|
2005
|
+
if (!pv.ok) {
|
|
2006
|
+
results[i] = {
|
|
2007
|
+
path: afsPath,
|
|
2008
|
+
success: false,
|
|
2009
|
+
error: pv.reason,
|
|
2010
|
+
code: "AFS_VALIDATION_ERROR"
|
|
2011
|
+
};
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
const se = byPath.get(afsPath);
|
|
2015
|
+
if (!se || se.cid !== c.cid) {
|
|
2016
|
+
results[i] = {
|
|
2017
|
+
path: afsPath,
|
|
2018
|
+
success: false,
|
|
2019
|
+
error: "entry not in session, or cid mismatch with prepared contract",
|
|
2020
|
+
code: "AFS_VALIDATION_ERROR"
|
|
2021
|
+
};
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
const deviceId = typeof c.deviceId === "string" && c.deviceId ? c.deviceId : se.deviceId ?? this.deviceId;
|
|
2025
|
+
const mtime = typeof c.mtime === "number" ? c.mtime : se.mtime ?? Date.now();
|
|
2026
|
+
const ctime = resolveCtime(c.metadata, mtime);
|
|
2027
|
+
const treeEntry = {
|
|
2028
|
+
cid: se.cid,
|
|
2029
|
+
size: se.size,
|
|
2030
|
+
mtime,
|
|
2031
|
+
ctime,
|
|
2032
|
+
mimeType: se.mimeType ?? void 0
|
|
2033
|
+
};
|
|
2034
|
+
if (c.content !== void 0 && isTextMime(se.mimeType ?? void 0)) {
|
|
2035
|
+
if (await checksumSha256OfContent(c.content) === se.checksumSha256) treeEntry.content = c.content;
|
|
2036
|
+
}
|
|
2037
|
+
if (c.metadata) {
|
|
2038
|
+
const appMeta = extractApplicationMeta(c.metadata);
|
|
2039
|
+
if (appMeta) {
|
|
2040
|
+
try {
|
|
2041
|
+
assertMetadataSize(appMeta, afsPath);
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
results[i] = {
|
|
2044
|
+
path: afsPath,
|
|
2045
|
+
success: false,
|
|
2046
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2047
|
+
code: err.code ?? "AFS_VALIDATION_ERROR"
|
|
2048
|
+
};
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
treeEntry.metadata = appMeta;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
prepared.push({
|
|
2055
|
+
i,
|
|
2056
|
+
afsPath,
|
|
2057
|
+
treePath: pv.treePath,
|
|
2058
|
+
cid: se.cid,
|
|
2059
|
+
treeEntry,
|
|
2060
|
+
deviceId,
|
|
2061
|
+
...se.ifMatch ? { ifMatch: se.ifMatch } : {}
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
const preread = await this.batchPreread(prepared.map((p) => p.treePath));
|
|
2065
|
+
const skipped = [];
|
|
2066
|
+
let live = [];
|
|
2067
|
+
for (const p of prepared) {
|
|
2068
|
+
const existing = preread.get(p.treePath) ?? null;
|
|
2069
|
+
if (ifMatchPreconditionFailed(p.ifMatch, existing)) {
|
|
2070
|
+
results[p.i] = this.batchConflictFailure(p.afsPath, {
|
|
2071
|
+
currentCid: existing?.cid ?? null,
|
|
2072
|
+
currentMtime: existing?.mtime ?? null,
|
|
2073
|
+
currentDeviceId: null
|
|
2074
|
+
});
|
|
2075
|
+
continue;
|
|
2076
|
+
}
|
|
2077
|
+
live.push(p);
|
|
2078
|
+
}
|
|
2079
|
+
const preIoOk = [];
|
|
2080
|
+
for (const p of live) try {
|
|
2081
|
+
await applyShareWriteHook({
|
|
2082
|
+
...this.shareHookArgs(p.afsPath, p.treePath),
|
|
2083
|
+
bytes: new Uint8Array(0),
|
|
2084
|
+
cid: p.cid,
|
|
2085
|
+
resolver: this.shareResolver,
|
|
2086
|
+
phase: "pre-io"
|
|
2087
|
+
});
|
|
2088
|
+
preIoOk.push(p);
|
|
2089
|
+
} catch (err) {
|
|
2090
|
+
results[p.i] = {
|
|
2091
|
+
path: p.afsPath,
|
|
2092
|
+
success: false,
|
|
2093
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
live = preIoOk;
|
|
2097
|
+
let indexResults = [];
|
|
2098
|
+
if (live.length > 0) {
|
|
2099
|
+
const batchEntries = live.map((p) => ({
|
|
2100
|
+
path: p.treePath,
|
|
2101
|
+
entry: p.treeEntry,
|
|
2102
|
+
opts: { deviceId: p.deviceId }
|
|
2103
|
+
}));
|
|
2104
|
+
indexResults = this.treeIndex.batchPutEntries ? await this.treeIndex.batchPutEntries(batchEntries, { preread }) : await loopBatchPutEntries(this.treeIndex, batchEntries, { preread });
|
|
2105
|
+
}
|
|
2106
|
+
const written = [];
|
|
2107
|
+
live.forEach((p, j) => {
|
|
2108
|
+
const r = indexResults[j];
|
|
2109
|
+
if (!r) {
|
|
2110
|
+
results[p.i] = {
|
|
2111
|
+
path: p.afsPath,
|
|
2112
|
+
success: false,
|
|
2113
|
+
error: "index batch returned no result"
|
|
2114
|
+
};
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
if (r.conflict) {
|
|
2118
|
+
results[p.i] = this.batchConflictFailure(p.afsPath, r.conflict);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
if (r.error) {
|
|
2122
|
+
results[p.i] = {
|
|
2123
|
+
path: p.afsPath,
|
|
2124
|
+
success: false,
|
|
2125
|
+
error: r.error
|
|
2126
|
+
};
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
if (r.skipped) {
|
|
2130
|
+
skipped.push(p);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
written.push({
|
|
2134
|
+
p,
|
|
2135
|
+
oldCid: r.oldCid,
|
|
2136
|
+
liveSeq: r.liveSeq
|
|
2137
|
+
});
|
|
2138
|
+
});
|
|
2139
|
+
if (this.gcQueue) {
|
|
2140
|
+
const reReferenced = [...new Set([...written.map((w) => w.p.cid), ...skipped.map((s) => s.cid)])];
|
|
2141
|
+
if (reReferenced.length > 0) if (this.gcQueue.removeMany) await this.gcQueue.removeMany(reReferenced);
|
|
2142
|
+
else for (const cid of reReferenced) await this.gcQueue.remove(cid);
|
|
2143
|
+
const replaced = [...new Set(written.filter((w) => w.oldCid && w.oldCid !== w.p.cid).map((w) => w.oldCid))];
|
|
2144
|
+
if (replaced.length > 0) await this.gcQueue.enqueue(replaced);
|
|
2145
|
+
}
|
|
2146
|
+
for (const w of written) try {
|
|
2147
|
+
await applyShareWriteHook({
|
|
2148
|
+
...this.shareHookArgs(w.p.afsPath, w.p.treePath),
|
|
2149
|
+
bytes: new Uint8Array(0),
|
|
2150
|
+
cid: w.p.cid,
|
|
2151
|
+
resolver: this.shareResolver,
|
|
2152
|
+
phase: "post-io"
|
|
2153
|
+
});
|
|
2154
|
+
} catch {}
|
|
2155
|
+
const commitOk = (p, liveSeq) => ({
|
|
2156
|
+
status: 200,
|
|
2157
|
+
ok: true,
|
|
2158
|
+
data: {
|
|
2159
|
+
path: p.afsPath,
|
|
2160
|
+
meta: {
|
|
2161
|
+
cid: p.cid,
|
|
2162
|
+
liveSeq: liveSeq != null ? String(liveSeq) : void 0
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
for (const w of written) results[w.p.i] = commitOk(w.p, w.liveSeq);
|
|
2167
|
+
for (const s of skipped) {
|
|
2168
|
+
let liveSeq;
|
|
2169
|
+
if (this.treeIndex.getPrefixWatermark) liveSeq = await this.treeIndex.getPrefixWatermark(s.treePath) ?? void 0;
|
|
2170
|
+
results[s.i] = commitOk(s, liveSeq);
|
|
2171
|
+
}
|
|
2172
|
+
const committedPaths = [...written, ...skipped.map((s) => ({ p: s }))].map((w) => ({
|
|
2173
|
+
path: w.p.afsPath,
|
|
2174
|
+
status: "committed"
|
|
2175
|
+
}));
|
|
2176
|
+
if (committedPaths.length > 0) await this.uploadSessionStore?.markEntries(sessionId, committedPaths);
|
|
2177
|
+
const succeeded = results.filter((r) => r?.ok === true).length;
|
|
2178
|
+
return {
|
|
2179
|
+
ok: true,
|
|
2180
|
+
results,
|
|
2181
|
+
succeeded,
|
|
2182
|
+
failed: results.length - succeeded
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Native batch delete — same merge without the R2 leg: ONE preread → GC
|
|
2187
|
+
* enqueue (before delete, single-op ordering) → ONE atomic index batch →
|
|
2188
|
+
* share delete hooks. FILE entries only (no directory fallback — sync
|
|
2189
|
+
* never dir-deletes; a dir path fails per entry with 404).
|
|
2190
|
+
*/
|
|
2191
|
+
async batchDelete(entries, _options) {
|
|
2192
|
+
const results = new Array(entries.length);
|
|
2193
|
+
const prepared = [];
|
|
2194
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2195
|
+
const entry = entries[i];
|
|
2196
|
+
const afsPath = joinURL("/", entry.path);
|
|
2197
|
+
if (this.isVirtualPath(afsPath) || isShareReservedPath(afsPath)) {
|
|
2198
|
+
results[i] = {
|
|
2199
|
+
path: afsPath,
|
|
2200
|
+
success: false,
|
|
2201
|
+
error: `Forbidden at ${afsPath}: virtual_path`,
|
|
2202
|
+
code: "AFS_FORBIDDEN"
|
|
2203
|
+
};
|
|
2204
|
+
continue;
|
|
2205
|
+
}
|
|
2206
|
+
const deleteMeta = entry.options?.meta;
|
|
2207
|
+
prepared.push({
|
|
2208
|
+
i,
|
|
2209
|
+
afsPath,
|
|
2210
|
+
treePath: this.treePath(afsPath),
|
|
2211
|
+
deviceId: typeof deleteMeta?.deviceId === "string" && deleteMeta.deviceId ? deleteMeta.deviceId : this.deviceId,
|
|
2212
|
+
...typeof deleteMeta?.mtime === "number" ? { mtime: deleteMeta.mtime } : {},
|
|
2213
|
+
...typeof deleteMeta?.ifMatch === "string" ? { ifMatch: deleteMeta.ifMatch } : {}
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
const preread = await this.batchPreread(prepared.map((p) => p.treePath));
|
|
2217
|
+
const live = [];
|
|
2218
|
+
for (const p of prepared) {
|
|
2219
|
+
const existing = preread.get(p.treePath) ?? null;
|
|
2220
|
+
if (p.ifMatch !== void 0 && (!existing || existing.cid !== p.ifMatch)) {
|
|
2221
|
+
results[p.i] = this.batchConflictFailure(p.afsPath, {
|
|
2222
|
+
currentCid: existing?.cid ?? null,
|
|
2223
|
+
currentMtime: existing?.mtime ?? null,
|
|
2224
|
+
currentDeviceId: null
|
|
2225
|
+
});
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
if (!existing) {
|
|
2229
|
+
results[p.i] = {
|
|
2230
|
+
path: p.afsPath,
|
|
2231
|
+
success: false,
|
|
2232
|
+
error: `Path not found: ${p.afsPath}`,
|
|
2233
|
+
code: "AFS_NOT_FOUND"
|
|
2234
|
+
};
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
live.push(p);
|
|
2238
|
+
}
|
|
2239
|
+
if (this.gcQueue && live.length > 0) {
|
|
2240
|
+
const cids = [...new Set(live.map((p) => (preread.get(p.treePath) ?? null)?.cid).filter(Boolean))];
|
|
2241
|
+
if (cids.length > 0) await this.gcQueue.enqueue(cids);
|
|
2242
|
+
}
|
|
2243
|
+
if (live.length > 0) {
|
|
2244
|
+
const batchEntries = live.map((p) => ({
|
|
2245
|
+
path: p.treePath,
|
|
2246
|
+
opts: {
|
|
2247
|
+
deviceId: p.deviceId,
|
|
2248
|
+
...p.mtime !== void 0 ? { mtime: p.mtime } : {}
|
|
2249
|
+
}
|
|
2250
|
+
}));
|
|
2251
|
+
let indexResults;
|
|
2252
|
+
try {
|
|
2253
|
+
indexResults = this.treeIndex.batchDeleteEntries ? await this.treeIndex.batchDeleteEntries(batchEntries, { preread }) : await loopBatchDeleteEntries(this.treeIndex, batchEntries, { preread });
|
|
2254
|
+
} catch (err) {
|
|
2255
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2256
|
+
for (const p of live) results[p.i] = {
|
|
2257
|
+
path: p.afsPath,
|
|
2258
|
+
success: false,
|
|
2259
|
+
error: message
|
|
2260
|
+
};
|
|
2261
|
+
const succeededEarly = results.filter((r) => r?.success).length;
|
|
2262
|
+
return {
|
|
2263
|
+
results,
|
|
2264
|
+
succeeded: succeededEarly,
|
|
2265
|
+
failed: results.length - succeededEarly
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
for (let j = 0; j < live.length; j++) {
|
|
2269
|
+
const p = live[j];
|
|
2270
|
+
const r = indexResults[j];
|
|
2271
|
+
if (!r || r.error || r.conflict || r.notFound) {
|
|
2272
|
+
results[p.i] = r?.conflict ? this.batchConflictFailure(p.afsPath, r.conflict) : {
|
|
2273
|
+
path: p.afsPath,
|
|
2274
|
+
success: false,
|
|
2275
|
+
error: r?.error ?? `Path not found: ${p.afsPath}`,
|
|
2276
|
+
...r?.notFound ? { code: "AFS_NOT_FOUND" } : {}
|
|
2277
|
+
};
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
await applyShareDeleteHook({
|
|
2281
|
+
...this.shareHookArgs(p.afsPath, p.treePath),
|
|
2282
|
+
resolver: this.shareResolver
|
|
2283
|
+
});
|
|
2284
|
+
if (p.afsPath.endsWith(".ash")) this.emit({
|
|
2285
|
+
type: SCRIPT_EVENT_TYPES.UNREGISTERED,
|
|
2286
|
+
path: p.afsPath,
|
|
2287
|
+
data: { runtime: "ash" }
|
|
2288
|
+
});
|
|
2289
|
+
results[p.i] = {
|
|
2290
|
+
path: p.afsPath,
|
|
2291
|
+
success: true
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const succeeded = results.filter((r) => r?.success).length;
|
|
2296
|
+
return {
|
|
2297
|
+
results,
|
|
2298
|
+
succeeded,
|
|
2299
|
+
failed: results.length - succeeded
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
async statHandler(ctx) {
|
|
2303
|
+
const afsPath = joinURL("/", ctx.params.path ?? "");
|
|
2304
|
+
if (afsPath === "/") return { data: {
|
|
2305
|
+
id: "/",
|
|
2306
|
+
path: "/",
|
|
2307
|
+
meta: {
|
|
2308
|
+
kind: "did-space:root",
|
|
2309
|
+
childrenCount: await this.treeIndex.countChildren(this.treeListPrefix("/"))
|
|
2310
|
+
}
|
|
2311
|
+
} };
|
|
2312
|
+
const entry = await this.treeIndex.get(this.treePath(afsPath), { includeContent: false });
|
|
2313
|
+
if (entry) return { data: {
|
|
2314
|
+
id: afsPath.split("/").pop() || afsPath,
|
|
2315
|
+
path: afsPath,
|
|
2316
|
+
meta: {
|
|
2317
|
+
kind: "did-space:file",
|
|
2318
|
+
size: entry.size,
|
|
2319
|
+
cid: entry.cid,
|
|
2320
|
+
mimeType: entry.mimeType,
|
|
2321
|
+
version: entry.cid
|
|
2322
|
+
},
|
|
2323
|
+
createdAt: new Date(entry.ctime),
|
|
2324
|
+
updatedAt: new Date(entry.mtime)
|
|
2325
|
+
} };
|
|
2326
|
+
if (await this.isDirectory(afsPath)) {
|
|
2327
|
+
const prefix = this.treeListPrefix(afsPath);
|
|
2328
|
+
const meta = {
|
|
2329
|
+
kind: "did-space:directory",
|
|
2330
|
+
childrenCount: await this.treeIndex.countChildren(prefix)
|
|
2331
|
+
};
|
|
2332
|
+
if (this.treeIndex.getPrefixWatermark) {
|
|
2333
|
+
const version = await this.treeIndex.getPrefixWatermark(prefix);
|
|
2334
|
+
if (version != null) meta.liveSeq = String(version);
|
|
2335
|
+
}
|
|
2336
|
+
const stat = {
|
|
2337
|
+
id: afsPath.split("/").pop() || afsPath,
|
|
2338
|
+
path: afsPath,
|
|
2339
|
+
meta
|
|
2340
|
+
};
|
|
2341
|
+
const dirTimes = await this.treeIndex.getDirTimes?.(prefix);
|
|
2342
|
+
if (dirTimes) {
|
|
2343
|
+
stat.createdAt = new Date(dirTimes.ctime);
|
|
2344
|
+
stat.updatedAt = new Date(dirTimes.mtime);
|
|
2345
|
+
}
|
|
2346
|
+
return { data: stat };
|
|
2347
|
+
}
|
|
2348
|
+
throw new AFSNotFoundError(afsPath);
|
|
2349
|
+
}
|
|
2350
|
+
async searchHandler(ctx, query, options) {
|
|
2351
|
+
const afsPath = ctx.params.path ? `/${ctx.params.path}` : "/";
|
|
2352
|
+
const pattern = query || "";
|
|
2353
|
+
const limit = options?.limit || 50;
|
|
2354
|
+
if (!pattern) return {
|
|
2355
|
+
data: [],
|
|
2356
|
+
total: 0
|
|
2357
|
+
};
|
|
2358
|
+
const prefix = this.treeListPrefix(afsPath);
|
|
2359
|
+
const results = [];
|
|
2360
|
+
let cursor;
|
|
2361
|
+
const searchQuery = pattern.toLowerCase();
|
|
2362
|
+
do {
|
|
2363
|
+
const listResult = await this.treeIndex.list(prefix, {
|
|
2364
|
+
limit: LIST_MAX_LIMIT,
|
|
2365
|
+
cursor
|
|
2366
|
+
});
|
|
2367
|
+
for (const treeListEntry of listResult.entries) {
|
|
2368
|
+
if (results.length >= limit) break;
|
|
2369
|
+
const path = `/${treeListEntry.path}`;
|
|
2370
|
+
if ((path.split("/").pop() || "").toLowerCase().includes(searchQuery)) {
|
|
2371
|
+
results.push(this.buildEntry(path, {}));
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2374
|
+
const treeEntry = await this.treeIndex.get(treeListEntry.path, { includeContent: false });
|
|
2375
|
+
if (treeEntry) {
|
|
2376
|
+
if (!isTextMime(treeEntry.mimeType)) continue;
|
|
2377
|
+
const obj = await this.objectStore.get(this.objectKey(treeEntry.cid));
|
|
2378
|
+
if (obj) {
|
|
2379
|
+
const content = await obj.text();
|
|
2380
|
+
if (content.toLowerCase().includes(searchQuery)) results.push(this.buildEntry(path, { content }));
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
cursor = listResult.hasMore ? listResult.cursor : void 0;
|
|
2385
|
+
} while (cursor && results.length < limit);
|
|
2386
|
+
return {
|
|
2387
|
+
data: results,
|
|
2388
|
+
total: results.length
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
async explainHandler(ctx) {
|
|
2392
|
+
const afsPath = joinURL("/", ctx.params.path ?? "");
|
|
2393
|
+
const lines = [];
|
|
2394
|
+
lines.push(`# DID Space: ${afsPath}`);
|
|
2395
|
+
lines.push("");
|
|
2396
|
+
if (afsPath === "/") {
|
|
2397
|
+
lines.push(`DID: ${this.did}`);
|
|
2398
|
+
lines.push("");
|
|
2399
|
+
lines.push(this.description || "DID-scoped persistent storage with CID-based deduplication");
|
|
2400
|
+
lines.push("");
|
|
2401
|
+
lines.push("## Operations");
|
|
2402
|
+
lines.push("- `list /` — List root files and directories");
|
|
2403
|
+
lines.push("- `read /{path}` — Read file content");
|
|
2404
|
+
lines.push("- `write /{path}` — Write file (content-addressed dedup)");
|
|
2405
|
+
lines.push("- `delete /{path}` — Delete file entry (lazy GC)");
|
|
2406
|
+
} else {
|
|
2407
|
+
const entry = await this.treeIndex.get(this.treePath(afsPath), { includeContent: false });
|
|
2408
|
+
if (entry) {
|
|
2409
|
+
lines.push(`DID: ${this.did}`);
|
|
2410
|
+
lines.push(`Path: ${afsPath}`);
|
|
2411
|
+
lines.push(`Size: ${entry.size} bytes`);
|
|
2412
|
+
lines.push(`CID: ${entry.cid}`);
|
|
2413
|
+
lines.push(`MIME: ${entry.mimeType ?? "unknown"}`);
|
|
2414
|
+
lines.push(`Created: ${new Date(entry.ctime).toISOString()}`);
|
|
2415
|
+
lines.push(`Modified: ${new Date(entry.mtime).toISOString()}`);
|
|
2416
|
+
} else if (await this.isDirectory(afsPath)) {
|
|
2417
|
+
lines.push(`DID: ${this.did}`);
|
|
2418
|
+
lines.push(`Virtual directory: ${afsPath}`);
|
|
2419
|
+
lines.push("Use `list` to see contents.");
|
|
2420
|
+
} else lines.push("Path not found.");
|
|
2421
|
+
}
|
|
2422
|
+
return {
|
|
2423
|
+
format: "markdown",
|
|
2424
|
+
content: lines.join("\n")
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Write content to the provider, bypassing D4 virtual path protection.
|
|
2429
|
+
* Stores content to objectStore and treeIndex with the given deviceId.
|
|
2430
|
+
*/
|
|
2431
|
+
async internalWrite(afsPath, opts) {
|
|
2432
|
+
if (isShareReservedPath(afsPath)) throw new AFSForbiddenError(afsPath, "share-path-reserved");
|
|
2433
|
+
const { content, deviceId, mtime, mimeType: explicitMimeType, metadata } = opts;
|
|
2434
|
+
let bytes;
|
|
2435
|
+
let storeValue;
|
|
2436
|
+
if (content instanceof Uint8Array) {
|
|
2437
|
+
bytes = content;
|
|
2438
|
+
storeValue = bytes.buffer;
|
|
2439
|
+
} else {
|
|
2440
|
+
bytes = new Uint8Array(content);
|
|
2441
|
+
storeValue = bytes.buffer;
|
|
2442
|
+
}
|
|
2443
|
+
const cid = await computeCID(bytes);
|
|
2444
|
+
const objKey = this.objectKey(cid);
|
|
2445
|
+
const treePath = this.treePath(afsPath);
|
|
2446
|
+
await applyShareWriteHook({
|
|
2447
|
+
...this.shareHookArgs(afsPath, treePath),
|
|
2448
|
+
bytes,
|
|
2449
|
+
cid,
|
|
2450
|
+
resolver: this.shareResolver,
|
|
2451
|
+
phase: "pre-io"
|
|
2452
|
+
});
|
|
2453
|
+
if (!await this.objectStore.head(objKey)) await this.objectStore.put(objKey, storeValue);
|
|
2454
|
+
const now = mtime ?? Date.now();
|
|
2455
|
+
const detectedMime = explicitMimeType ?? detectMimeType(afsPath) ?? void 0;
|
|
2456
|
+
const ctime = resolveCtime(metadata, now);
|
|
2457
|
+
const treeEntry = {
|
|
2458
|
+
cid,
|
|
2459
|
+
size: bytes.length,
|
|
2460
|
+
mtime: now,
|
|
2461
|
+
ctime,
|
|
2462
|
+
mimeType: detectedMime
|
|
2463
|
+
};
|
|
2464
|
+
const appMeta = extractApplicationMeta(metadata);
|
|
2465
|
+
if (appMeta) treeEntry.metadata = appMeta;
|
|
2466
|
+
const inlineText = decodeInlineText(bytes, detectedMime);
|
|
2467
|
+
if (inlineText !== null) treeEntry.content = inlineText;
|
|
2468
|
+
const { oldCid } = await this.treeIndex.put(treePath, treeEntry, { deviceId });
|
|
2469
|
+
if (this.gcQueue) {
|
|
2470
|
+
await this.gcQueue.remove(cid);
|
|
2471
|
+
if (oldCid && oldCid !== cid) await this.gcQueue.enqueue([oldCid]);
|
|
2472
|
+
}
|
|
2473
|
+
await applyShareWriteHook({
|
|
2474
|
+
...this.shareHookArgs(afsPath, treePath),
|
|
2475
|
+
bytes,
|
|
2476
|
+
cid,
|
|
2477
|
+
resolver: this.shareResolver,
|
|
2478
|
+
phase: "post-io"
|
|
2479
|
+
});
|
|
2480
|
+
await applyBlockletWriteHook({
|
|
2481
|
+
...this.blockletHookArgs(afsPath, treePath),
|
|
2482
|
+
bytes,
|
|
2483
|
+
resolver: this.blockletResolver
|
|
2484
|
+
});
|
|
2485
|
+
return oldCid === void 0 ? { cid } : {
|
|
2486
|
+
cid,
|
|
2487
|
+
oldCid
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
/**
|
|
2491
|
+
* Delete a file from the provider, bypassing D4 virtual path protection.
|
|
2492
|
+
*/
|
|
2493
|
+
async internalDelete(afsPath, opts) {
|
|
2494
|
+
if (isShareReservedPath(afsPath)) throw new AFSForbiddenError(afsPath, "share-path-reserved");
|
|
2495
|
+
const treePath = this.treePath(afsPath);
|
|
2496
|
+
const entry = await this.treeIndex.get(treePath, { includeContent: false });
|
|
2497
|
+
if (!entry) return { deleted: false };
|
|
2498
|
+
await this.gcQueue?.enqueue([entry.cid]);
|
|
2499
|
+
await this.treeIndex.delete(treePath, { deviceId: opts.deviceId });
|
|
2500
|
+
await applyShareDeleteHook({
|
|
2501
|
+
...this.shareHookArgs(afsPath, treePath),
|
|
2502
|
+
resolver: this.shareResolver
|
|
2503
|
+
});
|
|
2504
|
+
await applyBlockletDeleteHook({
|
|
2505
|
+
...this.blockletHookArgs(afsPath, treePath),
|
|
2506
|
+
resolver: this.blockletResolver
|
|
2507
|
+
});
|
|
2508
|
+
return { deleted: true };
|
|
2509
|
+
}
|
|
2510
|
+
capabilities() {
|
|
2511
|
+
return {
|
|
2512
|
+
schemaVersion: 1,
|
|
2513
|
+
provider: this.name,
|
|
2514
|
+
tools: [],
|
|
2515
|
+
actions: [],
|
|
2516
|
+
operations: this.getOperationsDeclaration()
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
__decorate([Meta("/:path*")], DIDSpaceProvider.prototype, "readMeta", null);
|
|
2521
|
+
__decorate([List("/:path*", { handleDepth: true })], DIDSpaceProvider.prototype, "listHandler", null);
|
|
2522
|
+
__decorate([Read("/.meta/.capabilities")], DIDSpaceProvider.prototype, "readCapabilities", null);
|
|
2523
|
+
__decorate([Read("/:path*")], DIDSpaceProvider.prototype, "readHandler", null);
|
|
2524
|
+
__decorate([Write("/:path*")], DIDSpaceProvider.prototype, "writeHandler", null);
|
|
2525
|
+
__decorate([Delete("/:path*")], DIDSpaceProvider.prototype, "deleteHandler", null);
|
|
2526
|
+
__decorate([Actions("/")], DIDSpaceProvider.prototype, "listObjectPlaneActions", null);
|
|
2527
|
+
__decorate([Actions.Exec("/", "prepare-upload", "Reserve an upload session + (CF) mint exact-key checksum-pinned PUT grants", { effect: "write" })], DIDSpaceProvider.prototype, "execPrepareUpload", null);
|
|
2528
|
+
__decorate([Actions.Exec("/", "prepare-download", "Mint exact-key short-TTL presigned GET grants for view-referenced objects", { effect: "write" })], DIDSpaceProvider.prototype, "execPrepareDownload", null);
|
|
2529
|
+
__decorate([Actions.Exec("/", "commit-upload", "Commit uploaded objects' metadata (trust existence, 0 R2 subrequests)", { effect: "write" })], DIDSpaceProvider.prototype, "execCommitUpload", null);
|
|
2530
|
+
__decorate([Actions.Exec("/", "abort-upload", "Abort an upload session; staged objects become GC candidates", { effect: "write" })], DIDSpaceProvider.prototype, "execAbortUpload", null);
|
|
2531
|
+
__decorate([Stat("/:path*")], DIDSpaceProvider.prototype, "statHandler", null);
|
|
2532
|
+
__decorate([Search("/:path*")], DIDSpaceProvider.prototype, "searchHandler", null);
|
|
2533
|
+
__decorate([Explain("/:path*")], DIDSpaceProvider.prototype, "explainHandler", null);
|
|
2534
|
+
|
|
2535
|
+
//#endregion
|
|
2536
|
+
export { AFS_IFMATCH_ABSENT, BLOCKLET_PUBLISH_MARKER, BatchOverBudgetError, BatchUnsupportedError, CHANGELOG_DDL, CHANGELOG_INDEX_SQL, CHANGELOG_INSERT_SQL, CHANGELOG_ROW_KIND, CHANGELOG_SCOPE_CID_MIGRATION_SQL, CHANGELOG_TABLE_SQL, CHANGELOG_WIRE_EXTRA_FIELDS, CachingObjectStore, CidByteCache, ConflictResolutionError, CurrentObjectMissingError, D1GCQueue, D1ResolveStore, D1ShareReconciler, D1ShareStore, D1TreeIndex, D1UploadSessionStore, D1VersionedScopeBackend, D1_DID_SPACE_SCHEMA_SQL, DEFAULT_GC_GRACE_MS, DEFAULT_INLINE_THRESHOLD, DEVICE_ID_FORMAT_RE, DEVICE_SYNC_STATE_INDEX_SQL, DEVICE_SYNC_STATE_TABLE_SQL, DIDSpaceBlockletDataBackend, DIDSpaceClient, DIDSpaceProvider, DIDSpaceResolver, DIRTY_PATH_DDL, DIRTY_PATH_INDEX_SQL, DIRTY_PATH_TABLE_SQL, DIRTY_PATH_TRIGGER_SQL, ENTRIES_CONTENT_TYPE_COLUMN_SQL, ENTRIES_CONTENT_TYPE_INDEX_DROP_SQL, ENTRIES_CONTENT_TYPE_INDEX_SQL, ENTRIES_DEDUP_INDEX_SQL, ENTRIES_DEDUP_KEY_COLUMN_SQL, ENTRIES_DEDUP_SCOPE_COLUMN_SQL, ENTRIES_INDEX_SQL, ENTRIES_INLINE_CONTENT_COLUMN_SQL, ENTRIES_METADATA_COLUMN_SQL, ENTRIES_TABLE_SQL, GC_QUEUE_INDEX_SQL, GC_QUEUE_TABLE_SQL, HttpTransport, INGEST_DEVICE_ID, INLINE_CONTENT_HARD_CAP, LOCAL_CHANGELOG_DEFAULT_LIMIT, LOCAL_CHANGELOG_MAX_LIMIT, LibsqlVersionedScopeBackend, LocalChangelog, MANIFEST_FILE_KIND, MANIFEST_WIRE_EXTRA_FIELDS, MAX_DEVICE_ID_BYTES, MAX_ENTRIES_PER_SCOPE, MemoryGcQueue, MemoryTreeIndex, MemoryVersionedScopeBackend, PREFIX_WATERMARK_TABLE_SQL, PRESIGN_MAX_EXPIRES_SEC, PULL_BATCH_LIMIT, PULL_FETCH_CONCURRENCY, PUSH_BATCH_LIMIT, PUSH_PROGRESS_FLUSH_INTERVAL, PrefixedTreeIndex, RESOLVE_ENTRIES_INDEX_SQL, RESOLVE_ENTRIES_TABLE_SQL, RpcUserSpaceAFS, SCOPE_META_FILE_COUNT_BACKFILL_SQL, SCOPE_META_FILE_COUNT_DELETE_ONE_SQL, SCOPE_META_FILE_COUNT_DELETE_RANGE_SQL, SCOPE_META_FILE_COUNT_TRIGGER_SQL, SCOPE_META_INDEX_SQL, SCOPE_META_TABLE_SQL, SCOPE_META_TOTAL_BYTES_BACKFILL_SQL, SCOPE_META_TOTAL_BYTES_COLUMN_SQL, SCOPE_META_TOTAL_BYTES_TRIGGER_SQL, SHARES_INDEX_SQL, SHARES_TABLE_SQL, SYNC_STATE_DDL, SYNC_STATE_INDEX_SQL, SYNC_STATE_KEYS, SYNC_STATE_TABLE_SQL, SYNC_WALL_CLOCK_BUDGET_MS, ShareResolver, SqliteGcQueue, SqliteResolveStore, SqliteShareStore, SqliteUploadSessionStore, StaticReadonlyTransport, SyncEngine, SyncStateStore, UPLOAD_SESSIONS_INDEX_SQL, UPLOAD_SESSIONS_TABLE_SQL, UPLOAD_SESSION_ENTRIES_TABLE_SQL, UPLOAD_SESSION_TTL_MS, VERSIONED_SCOPES_INDEX_SQL, VERSIONED_SCOPES_TABLE_SQL, VersionedContentIndex, VersionedContentStore, WsTransport, ancestorDirs, applyDirtyUpsert, assertInlineThreshold, assertValidCid, attachMetadataContext, buildChangelogBinds, buildD1MigrationSql, buildKeysetCursor, buildManifest, buildOrderedQueryParts, cidToChecksumSha256, classifyMissingObject, classifyScopes, compareTreeSortValues, computeCID, computeDiff, connectDIDSpace, createWsTransport, decodeInlineText, decodeScopeHmacSecret, defaultAppState, deriveScope, detachMetadataContext, detectMimeType, fetchRemoteManifest, generateDesktopDeviceId, getMetadataContext, isPlainDidScope, isShareDeclarationPath, isTextMime, isValidDeviceId, keysetAfter, migrateD1Scopes, nameOf, normalizeDdl, normalizeId, normalizeItemUrl, objectKeyForCid, openDaemonShareStore, parentOf, parseChangelogEntry, parseManifestEntry, parseSyncStatus, parseTreeCursor, prefixRange, referencedAnywhereD1, referencedAnywhereLibsql, resolveVersionToken, rowToEntry, runGc, runWithConcurrency, serializeChangelogEntry, serializeManifestEntry, serializeSyncStatus, serveShare, syncDIDSpace, validateDeviceId, validateId, versionedScope, versionedScopeGroup };
|
|
2537
|
+
//# sourceMappingURL=index.mjs.map
|