@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-backend.mjs","names":[],"sources":["../src/data-backend.ts"],"sourcesContent":["/**\n * DIDSpaceBlockletDataBackend — BlockletDataBackend backed by DID Space.\n *\n * Storage layout per blocklet app space ({blockletId}):\n * system/ — blocklet content tree (role root, mounted at `/`)\n * config/mounts.toml — per-instance mount overrides (sibling fragment via getConfigSpace)\n */\n\nimport type { AFS, BlockletDataBackend, MountOverride } from \"@aigne/afs\";\nimport type { DIDSpaceLike } from \"./did-space-like.js\";\nimport { normalizeId } from \"./did-utils.js\";\n\nexport interface DIDSpaceBlockletDataBackendOptions {\n didSpace: DIDSpaceLike;\n}\n\nexport class DIDSpaceBlockletDataBackend implements BlockletDataBackend {\n private readonly didSpace: DIDSpaceLike;\n\n constructor(options: DIDSpaceBlockletDataBackendOptions) {\n this.didSpace = options.didSpace;\n }\n\n /** Resolve the system-role instance space (content tree, mounted at `/`). */\n private instanceSpace(blockletDid: string): Promise<AFS> {\n return this.didSpace.getInstanceSpace({\n instanceDid: normalizeId(blockletDid),\n role: \"system\",\n });\n }\n\n /**\n * Resolve the runtime-internal config fragment root (`/`), or null if the\n * backend has none. /files-prefix-removal: config moved out of the role-AFS\n * into a sibling fragment, reached via `getConfigSpace` instead of the old\n * `system/config` sub-mount.\n */\n private async configSpace(blockletDid: string): Promise<AFS | null> {\n if (typeof this.didSpace.getConfigSpace !== \"function\") return null;\n return this.didSpace.getConfigSpace(normalizeId(blockletDid));\n }\n\n // ── Data management ──\n\n async getInstanceAFS(blockletDid: string): Promise<AFS> {\n return this.instanceSpace(blockletDid);\n }\n\n async removeData(blockletDid: string): Promise<void> {\n const appAFS = await this.instanceSpace(blockletDid);\n\n try {\n const result = await appAFS.list(\"/\", { maxDepth: 99 });\n const files = result.data.filter(\n (e) => e.meta?.childrenCount === undefined || e.meta.childrenCount === null,\n );\n for (const file of files) {\n try {\n await appAFS.delete!(file.path);\n } catch {\n // Best-effort\n }\n }\n } catch {\n // No files — ok\n }\n }\n\n // ── Mount overrides ──\n\n async readMountOverrides(blockletDid: string): Promise<MountOverride[]> {\n const cfg = await this.configSpace(blockletDid);\n if (!cfg) return [];\n const configPath = \"/mounts.toml\";\n\n try {\n const result = await cfg.read!(configPath);\n const content = String(result.data?.content ?? \"\");\n if (!content.trim()) return [];\n\n return this.parseMountsToml(content);\n } catch {\n return [];\n }\n }\n\n async writeMountOverrides(blockletDid: string, overrides: MountOverride[]): Promise<void> {\n if (overrides.length === 0) return;\n\n const cfg = await this.configSpace(blockletDid);\n if (!cfg) return;\n const configPath = \"/mounts.toml\";\n\n // Merge with existing\n const existing = await this.readMountOverrides(blockletDid);\n const byTarget = new Map(existing.map((o) => [o.target, o]));\n\n for (const override of overrides) {\n const entry: MountOverride = { target: override.target, uri: override.uri };\n if (override.options && Object.keys(override.options).length > 0) {\n entry.options = { ...override.options };\n }\n byTarget.set(override.target, entry);\n }\n\n const content = this.stringifyMountsToml([...byTarget.values()]);\n await cfg.write!(configPath, { content });\n }\n\n async removeMountOverrides(blockletDid: string, targets: string[]): Promise<void> {\n if (targets.length === 0) return;\n\n const targetSet = new Set(targets);\n const remaining = (await this.readMountOverrides(blockletDid)).filter(\n (override) => !targetSet.has(override.target),\n );\n const cfg = await this.configSpace(blockletDid);\n if (!cfg) return;\n const configPath = \"/mounts.toml\";\n\n if (remaining.length === 0) {\n try {\n await cfg.delete!(configPath);\n return;\n } catch {\n // Some backends may not support delete; an empty file is equivalent.\n }\n }\n\n await cfg.write!(configPath, { content: this.stringifyMountsToml(remaining) });\n }\n\n // ── Private helpers ──\n\n private parseMountsToml(content: string): MountOverride[] {\n try {\n const result: MountOverride[] = [];\n const lines = content.split(\"\\n\");\n let current: Partial<MountOverride> | null = null;\n let inOptions = false;\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed === \"[[mounts]]\") {\n if (current?.target && current.uri) {\n result.push({\n target: current.target,\n uri: current.uri,\n ...(current.options ? { options: current.options } : {}),\n });\n }\n current = {};\n inOptions = false;\n continue;\n }\n if (!current) continue;\n if (trimmed === \"[mounts.options]\") {\n inOptions = true;\n if (!current.options) current.options = {};\n continue;\n }\n\n // Parse key = value (string, number, boolean, array)\n const match = trimmed.match(/^(\\w+)\\s*=\\s*(.+)$/);\n if (match) {\n const [, key, rawVal] = match;\n let value: unknown;\n const v = rawVal!.trim();\n if (v.startsWith('\"') && v.endsWith('\"')) value = v.slice(1, -1);\n else if (v === \"true\") value = true;\n else if (v === \"false\") value = false;\n else if (v.startsWith(\"[\") && v.endsWith(\"]\")) {\n value = v\n .slice(1, -1)\n .split(\",\")\n .map((s) => s.trim().replace(/^\"|\"$/g, \"\"))\n .filter(Boolean);\n } else if (!Number.isNaN(Number(v))) value = Number(v);\n else value = v;\n\n if (inOptions && current.options) {\n current.options[key!] = value;\n } else {\n if (key === \"path\") current.target = value as string;\n else if (key === \"uri\") current.uri = value as string;\n }\n }\n }\n if (current?.target && current.uri) {\n result.push({\n target: current.target,\n uri: current.uri,\n ...(current.options ? { options: current.options } : {}),\n });\n }\n return result;\n } catch {\n return [];\n }\n }\n\n private stringifyMountsToml(overrides: MountOverride[]): string {\n return overrides\n .map((o) => {\n let block = `[[mounts]]\\npath = \"${o.target}\"\\nuri = \"${o.uri}\"`;\n if (o.options && Object.keys(o.options).length > 0) {\n block += \"\\n\\n[mounts.options]\";\n for (const [k, v] of Object.entries(o.options)) {\n if (typeof v === \"string\") block += `\\n${k} = \"${v}\"`;\n else if (typeof v === \"number\" || typeof v === \"boolean\") block += `\\n${k} = ${v}`;\n else if (Array.isArray(v)) block += `\\n${k} = [${v.map((i) => `\"${i}\"`).join(\", \")}]`;\n }\n }\n return block;\n })\n .join(\"\\n\\n\");\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
const require_url_normalize = require('./url-normalize.cjs');
|
|
2
|
+
|
|
3
|
+
//#region src/dedup-backfill.ts
|
|
4
|
+
/**
|
|
5
|
+
* dedup-backfill — one-time backfill of the dedup-on-write secondary index
|
|
6
|
+
* (`ds_entries.dedup_scope` / `dedup_key`) for records written BEFORE #1159
|
|
7
|
+
* landed (planning: dedup-on-write, issue #1159).
|
|
8
|
+
*
|
|
9
|
+
* Pre-#1159 the dedup entry lived at `{collection}/{cid(url)}.json` and had no
|
|
10
|
+
* `dedup_key` column; older layouts (`{slug}.json`, `{uuid}/item.json`) never
|
|
11
|
+
* had one either. After #1159 dedup resolves through the
|
|
12
|
+
* `(scope, dedup_scope, dedup_key)` UNIQUE index, so those historical rows are
|
|
13
|
+
* INVISIBLE to `findByDedupKey` — an already-saved URL's first re-save creates
|
|
14
|
+
* a duplicate. This sweep populates their columns so a re-save merges in place.
|
|
15
|
+
*
|
|
16
|
+
* Design (mirrors ctime-backfill / inline-content-migration):
|
|
17
|
+
* - OFFLINE, code-driven. The core loop {@link backfillDedup} is backend-
|
|
18
|
+
* agnostic; the SQL lives in {@link DedupBackfillBackend} adapters (D1 binding
|
|
19
|
+
* vs Node SQLite vs the CF D1 HTTP API) so it is unit-testable against a mock.
|
|
20
|
+
* - **Host-language normalization** — the `dedup_key` is
|
|
21
|
+
* `normalizeItemUrl(record[dedupBy])`, computed in JS with the SAME function
|
|
22
|
+
* the write path uses. It CANNOT be done in SQL (`json_extract` would store
|
|
23
|
+
* the RAW url, which would never match the Worker's normalized lookups), so
|
|
24
|
+
* the sweep MUST read the record body (`content` column) and normalize here.
|
|
25
|
+
* - **JS uniqueness pre-filter, never DB rejection.** The `(scope, dedup_scope,
|
|
26
|
+
* dedup_key)` UNIQUE index means two rows can't share a key. A `seen` set —
|
|
27
|
+
* seeded from the keys ALREADY indexed (e.g. entries a post-#1159 client
|
|
28
|
+
* already wrote) and grown as the sweep assigns — decides the winner in JS so
|
|
29
|
+
* NO `UPDATE` ever violates the constraint. This matters over the D1 HTTP
|
|
30
|
+
* channel where a constraint error is a 4xx that would abort the whole run.
|
|
31
|
+
* A genuine pre-existing duplicate is left un-indexed (reported), NEVER merged
|
|
32
|
+
* (the tool does not touch content).
|
|
33
|
+
* - **WHERE pins `scope` + the collection prefix**, binds everything as
|
|
34
|
+
* parameters — never a cross-scope / cross-collection UPDATE.
|
|
35
|
+
* - **Deterministic `ORDER BY path ASC` keyset cursor** — resumable across
|
|
36
|
+
* invocations; among genuine duplicates the FIRST in path order wins the key.
|
|
37
|
+
* - **Idempotent**: `UPDATE … WHERE dedup_key IS NULL` — a re-run never
|
|
38
|
+
* re-touches an already-populated row (0 write). The cursor advances past
|
|
39
|
+
* EVERY scanned row so one invocation can't loop.
|
|
40
|
+
* - **Only dedup_scope/dedup_key change** — never cid/size/content/mtime/ctime
|
|
41
|
+
* (index-only; no changelog / no sync fan-out).
|
|
42
|
+
*/
|
|
43
|
+
/** Extract the record body's `dedupBy` field as a non-empty string, or null. */
|
|
44
|
+
function extractKeyField(content, dedupBy) {
|
|
45
|
+
if (content == null) return null;
|
|
46
|
+
let body;
|
|
47
|
+
try {
|
|
48
|
+
body = JSON.parse(content);
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return null;
|
|
53
|
+
const v = body[dedupBy];
|
|
54
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Page over NULL-`dedup_key` FILE rows in one `(scope, collection)` and populate
|
|
58
|
+
* their dedup columns. Returns stats; resumable across calls (a fresh call
|
|
59
|
+
* re-seeds `seen` from the DB, so previously-indexed keys stay winners).
|
|
60
|
+
*/
|
|
61
|
+
async function backfillDedup(backend, opts) {
|
|
62
|
+
const pageSize = opts.pageSize ?? 100;
|
|
63
|
+
const maxPages = opts.maxPages ?? Number.POSITIVE_INFINITY;
|
|
64
|
+
const dryRun = opts.dryRun ?? false;
|
|
65
|
+
const log = opts.log ?? (() => {});
|
|
66
|
+
const stats = {
|
|
67
|
+
scanned: 0,
|
|
68
|
+
indexed: 0,
|
|
69
|
+
skippedNoContent: 0,
|
|
70
|
+
skippedNoField: 0,
|
|
71
|
+
skippedDuplicate: 0,
|
|
72
|
+
pages: 0,
|
|
73
|
+
lastCursor: "",
|
|
74
|
+
done: false
|
|
75
|
+
};
|
|
76
|
+
const seen = new Set(await backend.existingKeys());
|
|
77
|
+
let cursor = "";
|
|
78
|
+
while (stats.pages < maxPages) {
|
|
79
|
+
const rows = await backend.selectCandidates(cursor, pageSize);
|
|
80
|
+
if (rows.length === 0) {
|
|
81
|
+
stats.done = true;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
stats.pages++;
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
stats.scanned++;
|
|
87
|
+
cursor = row.path;
|
|
88
|
+
const raw = extractKeyField(row.content, opts.dedupBy);
|
|
89
|
+
if (raw === null) {
|
|
90
|
+
if (row.content == null) stats.skippedNoContent++;
|
|
91
|
+
else stats.skippedNoField++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const key = require_url_normalize.normalizeItemUrl(raw);
|
|
95
|
+
if (seen.has(key)) {
|
|
96
|
+
stats.skippedDuplicate++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
seen.add(key);
|
|
100
|
+
if (dryRun) {
|
|
101
|
+
stats.indexed++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (await backend.setDedupKey(row.path, key) > 0) stats.indexed++;
|
|
105
|
+
}
|
|
106
|
+
stats.lastCursor = cursor;
|
|
107
|
+
log(`[dedup-backfill] page ${stats.pages}: scanned=${stats.scanned} indexed=${stats.indexed} dup=${stats.skippedDuplicate} cursor=${cursor}`);
|
|
108
|
+
}
|
|
109
|
+
return stats;
|
|
110
|
+
}
|
|
111
|
+
const SELECT_SQL = "SELECT path, content FROM ds_entries WHERE scope = ? AND kind = 'file' AND dedup_key IS NULL AND path > ? AND substr(path, 1, ?) = ? ORDER BY path ASC LIMIT ?";
|
|
112
|
+
const EXISTING_KEYS_SQL = "SELECT dedup_key FROM ds_entries WHERE scope = ? AND dedup_scope = ? AND dedup_key IS NOT NULL";
|
|
113
|
+
const UPDATE_SQL = "UPDATE ds_entries SET dedup_scope = ?, dedup_key = ? WHERE scope = ? AND path = ? AND kind = 'file' AND dedup_key IS NULL";
|
|
114
|
+
const DISTINCT_SCOPES_SQL = "SELECT DISTINCT scope FROM ds_entries WHERE kind = 'file' AND dedup_key IS NULL AND substr(path, 1, ?) = ?";
|
|
115
|
+
/** The `path` prefix (`{collection}/`) + its length, for the substr filter. */
|
|
116
|
+
function collectionPrefix(collection) {
|
|
117
|
+
const prefix = collection.endsWith("/") ? collection : `${collection}/`;
|
|
118
|
+
return {
|
|
119
|
+
prefix,
|
|
120
|
+
len: prefix.length
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** D1 (Cloudflare binding) backend adapter for one (scope, collection). */
|
|
124
|
+
function makeD1DedupBackfillBackend(db, scope, collection) {
|
|
125
|
+
const { prefix, len } = collectionPrefix(collection);
|
|
126
|
+
return {
|
|
127
|
+
async existingKeys() {
|
|
128
|
+
return ((await db.prepare(EXISTING_KEYS_SQL).bind(scope, collection).all()).results ?? []).map((r) => String(r.dedup_key));
|
|
129
|
+
},
|
|
130
|
+
async selectCandidates(cursor, limit) {
|
|
131
|
+
return ((await db.prepare(SELECT_SQL).bind(scope, cursor, len, prefix, limit).all()).results ?? []).map((r) => ({
|
|
132
|
+
path: String(r.path),
|
|
133
|
+
content: r.content === null || r.content === void 0 ? null : String(r.content)
|
|
134
|
+
}));
|
|
135
|
+
},
|
|
136
|
+
async setDedupKey(path, dedupKey) {
|
|
137
|
+
return (await db.prepare(UPDATE_SQL).bind(collection, dedupKey, scope, path).run()).meta?.changes ?? 0;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** Node SQLite (libsql) backend adapter for one (scope, collection). */
|
|
142
|
+
function makeSqliteDedupBackfillBackend(db, scope, collection) {
|
|
143
|
+
const { prefix, len } = collectionPrefix(collection);
|
|
144
|
+
return {
|
|
145
|
+
async existingKeys() {
|
|
146
|
+
return (await db.execute({
|
|
147
|
+
sql: EXISTING_KEYS_SQL,
|
|
148
|
+
args: [scope, collection]
|
|
149
|
+
})).rows.map((r) => String(r.dedup_key));
|
|
150
|
+
},
|
|
151
|
+
async selectCandidates(cursor, limit) {
|
|
152
|
+
return (await db.execute({
|
|
153
|
+
sql: SELECT_SQL,
|
|
154
|
+
args: [
|
|
155
|
+
scope,
|
|
156
|
+
cursor,
|
|
157
|
+
len,
|
|
158
|
+
prefix,
|
|
159
|
+
limit
|
|
160
|
+
]
|
|
161
|
+
})).rows.map((r) => ({
|
|
162
|
+
path: String(r.path),
|
|
163
|
+
content: r.content === null || r.content === void 0 ? null : String(r.content)
|
|
164
|
+
}));
|
|
165
|
+
},
|
|
166
|
+
async setDedupKey(path, dedupKey) {
|
|
167
|
+
return (await db.execute({
|
|
168
|
+
sql: UPDATE_SQL,
|
|
169
|
+
args: [
|
|
170
|
+
collection,
|
|
171
|
+
dedupKey,
|
|
172
|
+
scope,
|
|
173
|
+
path
|
|
174
|
+
]
|
|
175
|
+
})).rowsAffected ?? 0;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Enumerate scopes with any NULL-dedup_key file row under the collection (D1). */
|
|
180
|
+
async function distinctScopesWithDedupCandidatesD1(db, collection) {
|
|
181
|
+
const { prefix, len } = collectionPrefix(collection);
|
|
182
|
+
return ((await db.prepare(DISTINCT_SCOPES_SQL).bind(len, prefix).all()).results ?? []).map((r) => String(r.scope));
|
|
183
|
+
}
|
|
184
|
+
/** Enumerate candidate scopes under the collection (Node SQLite). */
|
|
185
|
+
async function distinctScopesWithDedupCandidatesSqlite(db, collection) {
|
|
186
|
+
const { prefix, len } = collectionPrefix(collection);
|
|
187
|
+
return (await db.execute({
|
|
188
|
+
sql: DISTINCT_SCOPES_SQL,
|
|
189
|
+
args: [len, prefix]
|
|
190
|
+
})).rows.map((r) => String(r.scope));
|
|
191
|
+
}
|
|
192
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
193
|
+
async function d1HttpQuery(cfg, sql, params) {
|
|
194
|
+
const f = cfg.fetchImpl ?? fetch;
|
|
195
|
+
const sleep = cfg.sleep ?? defaultSleep;
|
|
196
|
+
const maxRetries = cfg.maxRetries ?? 4;
|
|
197
|
+
const url = `https://api.cloudflare.com/client/v4/accounts/${cfg.accountId}/d1/database/${cfg.databaseId}/query`;
|
|
198
|
+
for (let attempt = 0;; attempt++) {
|
|
199
|
+
let status = 0;
|
|
200
|
+
let json;
|
|
201
|
+
let networkErr;
|
|
202
|
+
try {
|
|
203
|
+
const res = await f(url, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: {
|
|
206
|
+
authorization: `Bearer ${cfg.apiToken}`,
|
|
207
|
+
"content-type": "application/json"
|
|
208
|
+
},
|
|
209
|
+
body: JSON.stringify({
|
|
210
|
+
sql,
|
|
211
|
+
params
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
status = res.status;
|
|
215
|
+
json = await res.json();
|
|
216
|
+
} catch (e) {
|
|
217
|
+
networkErr = e;
|
|
218
|
+
}
|
|
219
|
+
if (!networkErr && status >= 200 && status < 300 && json?.success !== false) {
|
|
220
|
+
const r = json?.result?.[0];
|
|
221
|
+
return {
|
|
222
|
+
results: r?.results ?? [],
|
|
223
|
+
changes: r?.meta?.changes ?? 0
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const transient = networkErr != null || status >= 500 || status === 429;
|
|
227
|
+
const msg = networkErr ? `D1 HTTP network error: ${String(networkErr).slice(0, 160)}` : `D1 HTTP ${status}: ${JSON.stringify(json?.errors ?? json).slice(0, 200)}`;
|
|
228
|
+
if (!transient || attempt >= maxRetries) throw new Error(msg);
|
|
229
|
+
await sleep(Math.min(2e3, 200 * 2 ** attempt));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** CF D1 HTTP-API backend adapter for one (scope, collection) — offline channel. */
|
|
233
|
+
function makeD1HttpDedupBackfillBackend(cfg, scope, collection) {
|
|
234
|
+
const { prefix, len } = collectionPrefix(collection);
|
|
235
|
+
return {
|
|
236
|
+
async existingKeys() {
|
|
237
|
+
const { results } = await d1HttpQuery(cfg, EXISTING_KEYS_SQL, [scope, collection]);
|
|
238
|
+
return results.map((r) => String(r.dedup_key));
|
|
239
|
+
},
|
|
240
|
+
async selectCandidates(cursor, limit) {
|
|
241
|
+
const { results } = await d1HttpQuery(cfg, SELECT_SQL, [
|
|
242
|
+
scope,
|
|
243
|
+
cursor,
|
|
244
|
+
len,
|
|
245
|
+
prefix,
|
|
246
|
+
limit
|
|
247
|
+
]);
|
|
248
|
+
return results.map((r) => ({
|
|
249
|
+
path: String(r.path),
|
|
250
|
+
content: r.content === null || r.content === void 0 ? null : String(r.content)
|
|
251
|
+
}));
|
|
252
|
+
},
|
|
253
|
+
async setDedupKey(path, dedupKey) {
|
|
254
|
+
const { changes } = await d1HttpQuery(cfg, UPDATE_SQL, [
|
|
255
|
+
collection,
|
|
256
|
+
dedupKey,
|
|
257
|
+
scope,
|
|
258
|
+
path
|
|
259
|
+
]);
|
|
260
|
+
return changes;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** List distinct candidate scopes under the collection on a CF D1 over HTTP. */
|
|
265
|
+
async function distinctScopesWithDedupCandidatesD1Http(cfg, collection) {
|
|
266
|
+
const { prefix, len } = collectionPrefix(collection);
|
|
267
|
+
const { results } = await d1HttpQuery(cfg, DISTINCT_SCOPES_SQL, [len, prefix]);
|
|
268
|
+
return results.map((r) => String(r.scope));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
//#endregion
|
|
272
|
+
exports.backfillDedup = backfillDedup;
|
|
273
|
+
exports.distinctScopesWithDedupCandidatesD1 = distinctScopesWithDedupCandidatesD1;
|
|
274
|
+
exports.distinctScopesWithDedupCandidatesD1Http = distinctScopesWithDedupCandidatesD1Http;
|
|
275
|
+
exports.distinctScopesWithDedupCandidatesSqlite = distinctScopesWithDedupCandidatesSqlite;
|
|
276
|
+
exports.makeD1DedupBackfillBackend = makeD1DedupBackfillBackend;
|
|
277
|
+
exports.makeD1HttpDedupBackfillBackend = makeD1HttpDedupBackfillBackend;
|
|
278
|
+
exports.makeSqliteDedupBackfillBackend = makeSqliteDedupBackfillBackend;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
//#region src/dedup-backfill.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* dedup-backfill — one-time backfill of the dedup-on-write secondary index
|
|
4
|
+
* (`ds_entries.dedup_scope` / `dedup_key`) for records written BEFORE #1159
|
|
5
|
+
* landed (planning: dedup-on-write, issue #1159).
|
|
6
|
+
*
|
|
7
|
+
* Pre-#1159 the dedup entry lived at `{collection}/{cid(url)}.json` and had no
|
|
8
|
+
* `dedup_key` column; older layouts (`{slug}.json`, `{uuid}/item.json`) never
|
|
9
|
+
* had one either. After #1159 dedup resolves through the
|
|
10
|
+
* `(scope, dedup_scope, dedup_key)` UNIQUE index, so those historical rows are
|
|
11
|
+
* INVISIBLE to `findByDedupKey` — an already-saved URL's first re-save creates
|
|
12
|
+
* a duplicate. This sweep populates their columns so a re-save merges in place.
|
|
13
|
+
*
|
|
14
|
+
* Design (mirrors ctime-backfill / inline-content-migration):
|
|
15
|
+
* - OFFLINE, code-driven. The core loop {@link backfillDedup} is backend-
|
|
16
|
+
* agnostic; the SQL lives in {@link DedupBackfillBackend} adapters (D1 binding
|
|
17
|
+
* vs Node SQLite vs the CF D1 HTTP API) so it is unit-testable against a mock.
|
|
18
|
+
* - **Host-language normalization** — the `dedup_key` is
|
|
19
|
+
* `normalizeItemUrl(record[dedupBy])`, computed in JS with the SAME function
|
|
20
|
+
* the write path uses. It CANNOT be done in SQL (`json_extract` would store
|
|
21
|
+
* the RAW url, which would never match the Worker's normalized lookups), so
|
|
22
|
+
* the sweep MUST read the record body (`content` column) and normalize here.
|
|
23
|
+
* - **JS uniqueness pre-filter, never DB rejection.** The `(scope, dedup_scope,
|
|
24
|
+
* dedup_key)` UNIQUE index means two rows can't share a key. A `seen` set —
|
|
25
|
+
* seeded from the keys ALREADY indexed (e.g. entries a post-#1159 client
|
|
26
|
+
* already wrote) and grown as the sweep assigns — decides the winner in JS so
|
|
27
|
+
* NO `UPDATE` ever violates the constraint. This matters over the D1 HTTP
|
|
28
|
+
* channel where a constraint error is a 4xx that would abort the whole run.
|
|
29
|
+
* A genuine pre-existing duplicate is left un-indexed (reported), NEVER merged
|
|
30
|
+
* (the tool does not touch content).
|
|
31
|
+
* - **WHERE pins `scope` + the collection prefix**, binds everything as
|
|
32
|
+
* parameters — never a cross-scope / cross-collection UPDATE.
|
|
33
|
+
* - **Deterministic `ORDER BY path ASC` keyset cursor** — resumable across
|
|
34
|
+
* invocations; among genuine duplicates the FIRST in path order wins the key.
|
|
35
|
+
* - **Idempotent**: `UPDATE … WHERE dedup_key IS NULL` — a re-run never
|
|
36
|
+
* re-touches an already-populated row (0 write). The cursor advances past
|
|
37
|
+
* EVERY scanned row so one invocation can't loop.
|
|
38
|
+
* - **Only dedup_scope/dedup_key change** — never cid/size/content/mtime/ctime
|
|
39
|
+
* (index-only; no changelog / no sync fan-out).
|
|
40
|
+
*/
|
|
41
|
+
/** A dedup-index candidate: a FILE row with a NULL `dedup_key`. */
|
|
42
|
+
interface DedupBackfillCandidate {
|
|
43
|
+
path: string;
|
|
44
|
+
/** Raw `entries.content` inline record body (host-parsed here, never in SQL). */
|
|
45
|
+
content: string | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Backend-specific SQL for the sweep (D1 binding vs Node SQLite vs D1 HTTP).
|
|
49
|
+
* One `(scope, collection)` per adapter — `collection` is the full stored
|
|
50
|
+
* collection path (e.g. `blocklets/aside/user/items`), which is BOTH the value
|
|
51
|
+
* written to `dedup_scope` and the `path` prefix that scopes the sweep.
|
|
52
|
+
*/
|
|
53
|
+
interface DedupBackfillBackend {
|
|
54
|
+
/**
|
|
55
|
+
* `dedup_key`s already claimed within this `(scope, collection)` — seeds the
|
|
56
|
+
* uniqueness `seen` set so the sweep never collides with a row a post-#1159
|
|
57
|
+
* client already indexed.
|
|
58
|
+
*/
|
|
59
|
+
existingKeys(): Promise<string[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Select up to `limit` FILE rows with `dedup_key IS NULL` under the collection
|
|
62
|
+
* and `path > cursor`, ordered `path ASC` (the keyset cursor). Empty ⇒ done.
|
|
63
|
+
*/
|
|
64
|
+
selectCandidates(cursor: string, limit: number): Promise<DedupBackfillCandidate[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Set `dedup_scope` (= the collection) + `dedup_key` on the row, guarded
|
|
67
|
+
* `AND dedup_key IS NULL` so an already-populated row is a 0-row no-op.
|
|
68
|
+
* Returns the number of rows actually changed (0 or 1).
|
|
69
|
+
*/
|
|
70
|
+
setDedupKey(path: string, dedupKey: string): Promise<number>;
|
|
71
|
+
}
|
|
72
|
+
interface DedupBackfillOptions {
|
|
73
|
+
/** Record field to dedup by (e.g. `"url"`). Required. */
|
|
74
|
+
dedupBy: string;
|
|
75
|
+
/** Rows per page. Default 100 (content is heavier than a metadata blob). */
|
|
76
|
+
pageSize?: number;
|
|
77
|
+
/** Cap pages this invocation (cross-invocation paging). Default unbounded. */
|
|
78
|
+
maxPages?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Preview only — scan + count, run NO UPDATE. `indexed` then reports rows that
|
|
81
|
+
* WOULD be indexed; all skip counts are exact in both modes.
|
|
82
|
+
*/
|
|
83
|
+
dryRun?: boolean;
|
|
84
|
+
log?: (msg: string) => void;
|
|
85
|
+
}
|
|
86
|
+
interface DedupBackfillStats {
|
|
87
|
+
/** FILE rows examined (dedup_key IS NULL under the collection). */
|
|
88
|
+
scanned: number;
|
|
89
|
+
/** Rows whose dedup columns were populated (or WOULD be, in dryRun). */
|
|
90
|
+
indexed: number;
|
|
91
|
+
/** Rows with no inline `content` — cannot extract the field, left as-is. */
|
|
92
|
+
skippedNoContent: number;
|
|
93
|
+
/** Rows whose body lacked a usable `dedupBy` field — not dedup-able. */
|
|
94
|
+
skippedNoField: number;
|
|
95
|
+
/** Rows whose key was already claimed — a genuine duplicate, left un-indexed. */
|
|
96
|
+
skippedDuplicate: number;
|
|
97
|
+
pages: number;
|
|
98
|
+
lastCursor: string;
|
|
99
|
+
/** True when a page came back empty (the sweep reached the end). */
|
|
100
|
+
done: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Page over NULL-`dedup_key` FILE rows in one `(scope, collection)` and populate
|
|
104
|
+
* their dedup columns. Returns stats; resumable across calls (a fresh call
|
|
105
|
+
* re-seeds `seen` from the DB, so previously-indexed keys stay winners).
|
|
106
|
+
*/
|
|
107
|
+
declare function backfillDedup(backend: DedupBackfillBackend, opts: DedupBackfillOptions): Promise<DedupBackfillStats>;
|
|
108
|
+
/** Minimal D1 shape used by the sweep (subset of `D1Database`). */
|
|
109
|
+
interface D1Like {
|
|
110
|
+
prepare(sql: string): {
|
|
111
|
+
bind(...args: (string | number)[]): {
|
|
112
|
+
all(): Promise<{
|
|
113
|
+
results?: unknown[];
|
|
114
|
+
}>;
|
|
115
|
+
run(): Promise<{
|
|
116
|
+
meta?: {
|
|
117
|
+
changes?: number;
|
|
118
|
+
};
|
|
119
|
+
}>;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** D1 (Cloudflare binding) backend adapter for one (scope, collection). */
|
|
124
|
+
declare function makeD1DedupBackfillBackend(db: D1Like, scope: string, collection: string): DedupBackfillBackend;
|
|
125
|
+
/**
|
|
126
|
+
* Minimal libsql client shape (Node `_metadata.db`). `args` narrowed to the
|
|
127
|
+
* exact bound types so a real `@libsql/client` `Client` is assignable.
|
|
128
|
+
*/
|
|
129
|
+
interface LibsqlLike {
|
|
130
|
+
execute(stmt: {
|
|
131
|
+
sql: string;
|
|
132
|
+
args: (string | number)[];
|
|
133
|
+
}): Promise<{
|
|
134
|
+
rows: Array<Record<string, unknown>>;
|
|
135
|
+
rowsAffected?: number;
|
|
136
|
+
}>;
|
|
137
|
+
}
|
|
138
|
+
/** Node SQLite (libsql) backend adapter for one (scope, collection). */
|
|
139
|
+
declare function makeSqliteDedupBackfillBackend(db: LibsqlLike, scope: string, collection: string): DedupBackfillBackend;
|
|
140
|
+
/** Enumerate scopes with any NULL-dedup_key file row under the collection (D1). */
|
|
141
|
+
declare function distinctScopesWithDedupCandidatesD1(db: D1Like, collection: string): Promise<string[]>;
|
|
142
|
+
/** Enumerate candidate scopes under the collection (Node SQLite). */
|
|
143
|
+
declare function distinctScopesWithDedupCandidatesSqlite(db: LibsqlLike, collection: string): Promise<string[]>;
|
|
144
|
+
/**
|
|
145
|
+
* Config for the offline CF D1 HTTP backend: a local Node process holding a
|
|
146
|
+
* Cloudflare API token with D1-edit runs the SAME paged sweep SQL the Worker
|
|
147
|
+
* would. No R2 — dedup backfill is pure index UPDATEs (record bodies come from
|
|
148
|
+
* the inline `content` column). `fetchImpl` / `sleep` are injectable for tests.
|
|
149
|
+
*/
|
|
150
|
+
interface DedupBackfillD1HttpConfig {
|
|
151
|
+
accountId: string;
|
|
152
|
+
databaseId: string;
|
|
153
|
+
apiToken: string;
|
|
154
|
+
fetchImpl?: typeof fetch;
|
|
155
|
+
/** Bounded retries for TRANSIENT failures (5xx / 429 / network). Default 4. */
|
|
156
|
+
maxRetries?: number;
|
|
157
|
+
sleep?: (ms: number) => Promise<void>;
|
|
158
|
+
}
|
|
159
|
+
/** CF D1 HTTP-API backend adapter for one (scope, collection) — offline channel. */
|
|
160
|
+
declare function makeD1HttpDedupBackfillBackend(cfg: DedupBackfillD1HttpConfig, scope: string, collection: string): DedupBackfillBackend;
|
|
161
|
+
/** List distinct candidate scopes under the collection on a CF D1 over HTTP. */
|
|
162
|
+
declare function distinctScopesWithDedupCandidatesD1Http(cfg: DedupBackfillD1HttpConfig, collection: string): Promise<string[]>;
|
|
163
|
+
//#endregion
|
|
164
|
+
export { D1Like, DedupBackfillBackend, DedupBackfillCandidate, DedupBackfillD1HttpConfig, DedupBackfillOptions, DedupBackfillStats, LibsqlLike, backfillDedup, distinctScopesWithDedupCandidatesD1, distinctScopesWithDedupCandidatesD1Http, distinctScopesWithDedupCandidatesSqlite, makeD1DedupBackfillBackend, makeD1HttpDedupBackfillBackend, makeSqliteDedupBackfillBackend };
|
|
165
|
+
//# sourceMappingURL=dedup-backfill.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedup-backfill.d.cts","names":[],"sources":["../src/dedup-backfill.ts"],"mappings":";;AA2CA;;;;;AAYA;;;;;;;;;;;;;;;;;;;;;;AAoBA;;;;;;;;;;;;UAhCiB,sBAAA;EACf,IAAA;;EAEA,OAAA;AAAA;;;;;;;UASe,oBAAA;EAiDX;;AAsBN;;;EAjEE,YAAA,IAAgB,OAAA;EAmEV;;;;EA9DN,gBAAA,CAAiB,MAAA,UAAgB,KAAA,WAAgB,OAAA,CAAQ,sBAAA;EA6DhD;;;;;EAvDT,WAAA,CAAY,IAAA,UAAc,QAAA,WAAmB,OAAA;AAAA;AAAA,UAG9B,oBAAA;EA2IA;EAzIf,OAAA;;EAEA,QAAA;EAwIA;EAtIA,QAAA;EAuIE;;;;EAlIF,MAAA;EACA,GAAA,IAAO,GAAA;AAAA;AAAA,UAGQ,kBAAA;EAgIe;EA9H9B,OAAA;EA8HqC;EA5HrC,OAAA;EAkIwC;EAhIxC,gBAAA;EAoIqB;EAlIrB,cAAA;EA+HA;EA7HA,gBAAA;EACA,KAAA;EACA,UAAA;EA8HqB;EA5HrB,IAAA;AAAA;;;;;;iBAsBoB,aAAA,CACpB,OAAA,EAAS,oBAAA,EACT,IAAA,EAAM,oBAAA,GACL,OAAA,CAAQ,kBAAA;;UAqFM,MAAA;EACf,OAAA,CAAQ,GAAA;IACN,IAAA,IAAQ,IAAA;MACN,GAAA,IAAO,OAAA;QAAU,OAAA;MAAA;MACjB,GAAA,IAAO,OAAA;QAAU,IAAA;UAAS,OAAA;QAAA;MAAA;IAAA;EAAA;AAAA;;iBAMhB,0BAAA,CACd,EAAA,EAAI,MAAA,EACJ,KAAA,UACA,UAAA,WACC,oBAAA;;;;;UA2Bc,UAAA;EACf,OAAA,CAAQ,IAAA;IAAQ,GAAA;IAAa,IAAA;EAAA,IAA8B,OAAA;IACzD,IAAA,EAAM,KAAA,CAAM,MAAA;IACZ,YAAA;EAAA;AAAA;;iBAKY,8BAAA,CACd,EAAA,EAAI,UAAA,EACJ,KAAA,UACA,UAAA,WACC,oBAAA;;iBAsBmB,mCAAA,CACpB,EAAA,EAAI,MAAA,EACJ,UAAA,WACC,OAAA;;iBAOmB,uCAAA,CACpB,EAAA,EAAI,UAAA,EACJ,UAAA,WACC,OAAA;;;;;;;UAac,yBAAA;EACf,SAAA;EACA,UAAA;EACA,QAAA;EACA,SAAA,UAAmB,KAAA;EAHnB;EAKA,UAAA;EACA,KAAA,IAAS,EAAA,aAAe,OAAA;AAAA;;iBAkDV,8BAAA,CACd,GAAA,EAAK,yBAAA,EACL,KAAA,UACA,UAAA,WACC,oBAAA;;iBAsBmB,uCAAA,CACpB,GAAA,EAAK,yBAAA,EACL,UAAA,WACC,OAAA"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
//#region src/dedup-backfill.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* dedup-backfill — one-time backfill of the dedup-on-write secondary index
|
|
4
|
+
* (`ds_entries.dedup_scope` / `dedup_key`) for records written BEFORE #1159
|
|
5
|
+
* landed (planning: dedup-on-write, issue #1159).
|
|
6
|
+
*
|
|
7
|
+
* Pre-#1159 the dedup entry lived at `{collection}/{cid(url)}.json` and had no
|
|
8
|
+
* `dedup_key` column; older layouts (`{slug}.json`, `{uuid}/item.json`) never
|
|
9
|
+
* had one either. After #1159 dedup resolves through the
|
|
10
|
+
* `(scope, dedup_scope, dedup_key)` UNIQUE index, so those historical rows are
|
|
11
|
+
* INVISIBLE to `findByDedupKey` — an already-saved URL's first re-save creates
|
|
12
|
+
* a duplicate. This sweep populates their columns so a re-save merges in place.
|
|
13
|
+
*
|
|
14
|
+
* Design (mirrors ctime-backfill / inline-content-migration):
|
|
15
|
+
* - OFFLINE, code-driven. The core loop {@link backfillDedup} is backend-
|
|
16
|
+
* agnostic; the SQL lives in {@link DedupBackfillBackend} adapters (D1 binding
|
|
17
|
+
* vs Node SQLite vs the CF D1 HTTP API) so it is unit-testable against a mock.
|
|
18
|
+
* - **Host-language normalization** — the `dedup_key` is
|
|
19
|
+
* `normalizeItemUrl(record[dedupBy])`, computed in JS with the SAME function
|
|
20
|
+
* the write path uses. It CANNOT be done in SQL (`json_extract` would store
|
|
21
|
+
* the RAW url, which would never match the Worker's normalized lookups), so
|
|
22
|
+
* the sweep MUST read the record body (`content` column) and normalize here.
|
|
23
|
+
* - **JS uniqueness pre-filter, never DB rejection.** The `(scope, dedup_scope,
|
|
24
|
+
* dedup_key)` UNIQUE index means two rows can't share a key. A `seen` set —
|
|
25
|
+
* seeded from the keys ALREADY indexed (e.g. entries a post-#1159 client
|
|
26
|
+
* already wrote) and grown as the sweep assigns — decides the winner in JS so
|
|
27
|
+
* NO `UPDATE` ever violates the constraint. This matters over the D1 HTTP
|
|
28
|
+
* channel where a constraint error is a 4xx that would abort the whole run.
|
|
29
|
+
* A genuine pre-existing duplicate is left un-indexed (reported), NEVER merged
|
|
30
|
+
* (the tool does not touch content).
|
|
31
|
+
* - **WHERE pins `scope` + the collection prefix**, binds everything as
|
|
32
|
+
* parameters — never a cross-scope / cross-collection UPDATE.
|
|
33
|
+
* - **Deterministic `ORDER BY path ASC` keyset cursor** — resumable across
|
|
34
|
+
* invocations; among genuine duplicates the FIRST in path order wins the key.
|
|
35
|
+
* - **Idempotent**: `UPDATE … WHERE dedup_key IS NULL` — a re-run never
|
|
36
|
+
* re-touches an already-populated row (0 write). The cursor advances past
|
|
37
|
+
* EVERY scanned row so one invocation can't loop.
|
|
38
|
+
* - **Only dedup_scope/dedup_key change** — never cid/size/content/mtime/ctime
|
|
39
|
+
* (index-only; no changelog / no sync fan-out).
|
|
40
|
+
*/
|
|
41
|
+
/** A dedup-index candidate: a FILE row with a NULL `dedup_key`. */
|
|
42
|
+
interface DedupBackfillCandidate {
|
|
43
|
+
path: string;
|
|
44
|
+
/** Raw `entries.content` inline record body (host-parsed here, never in SQL). */
|
|
45
|
+
content: string | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Backend-specific SQL for the sweep (D1 binding vs Node SQLite vs D1 HTTP).
|
|
49
|
+
* One `(scope, collection)` per adapter — `collection` is the full stored
|
|
50
|
+
* collection path (e.g. `blocklets/aside/user/items`), which is BOTH the value
|
|
51
|
+
* written to `dedup_scope` and the `path` prefix that scopes the sweep.
|
|
52
|
+
*/
|
|
53
|
+
interface DedupBackfillBackend {
|
|
54
|
+
/**
|
|
55
|
+
* `dedup_key`s already claimed within this `(scope, collection)` — seeds the
|
|
56
|
+
* uniqueness `seen` set so the sweep never collides with a row a post-#1159
|
|
57
|
+
* client already indexed.
|
|
58
|
+
*/
|
|
59
|
+
existingKeys(): Promise<string[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Select up to `limit` FILE rows with `dedup_key IS NULL` under the collection
|
|
62
|
+
* and `path > cursor`, ordered `path ASC` (the keyset cursor). Empty ⇒ done.
|
|
63
|
+
*/
|
|
64
|
+
selectCandidates(cursor: string, limit: number): Promise<DedupBackfillCandidate[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Set `dedup_scope` (= the collection) + `dedup_key` on the row, guarded
|
|
67
|
+
* `AND dedup_key IS NULL` so an already-populated row is a 0-row no-op.
|
|
68
|
+
* Returns the number of rows actually changed (0 or 1).
|
|
69
|
+
*/
|
|
70
|
+
setDedupKey(path: string, dedupKey: string): Promise<number>;
|
|
71
|
+
}
|
|
72
|
+
interface DedupBackfillOptions {
|
|
73
|
+
/** Record field to dedup by (e.g. `"url"`). Required. */
|
|
74
|
+
dedupBy: string;
|
|
75
|
+
/** Rows per page. Default 100 (content is heavier than a metadata blob). */
|
|
76
|
+
pageSize?: number;
|
|
77
|
+
/** Cap pages this invocation (cross-invocation paging). Default unbounded. */
|
|
78
|
+
maxPages?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Preview only — scan + count, run NO UPDATE. `indexed` then reports rows that
|
|
81
|
+
* WOULD be indexed; all skip counts are exact in both modes.
|
|
82
|
+
*/
|
|
83
|
+
dryRun?: boolean;
|
|
84
|
+
log?: (msg: string) => void;
|
|
85
|
+
}
|
|
86
|
+
interface DedupBackfillStats {
|
|
87
|
+
/** FILE rows examined (dedup_key IS NULL under the collection). */
|
|
88
|
+
scanned: number;
|
|
89
|
+
/** Rows whose dedup columns were populated (or WOULD be, in dryRun). */
|
|
90
|
+
indexed: number;
|
|
91
|
+
/** Rows with no inline `content` — cannot extract the field, left as-is. */
|
|
92
|
+
skippedNoContent: number;
|
|
93
|
+
/** Rows whose body lacked a usable `dedupBy` field — not dedup-able. */
|
|
94
|
+
skippedNoField: number;
|
|
95
|
+
/** Rows whose key was already claimed — a genuine duplicate, left un-indexed. */
|
|
96
|
+
skippedDuplicate: number;
|
|
97
|
+
pages: number;
|
|
98
|
+
lastCursor: string;
|
|
99
|
+
/** True when a page came back empty (the sweep reached the end). */
|
|
100
|
+
done: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Page over NULL-`dedup_key` FILE rows in one `(scope, collection)` and populate
|
|
104
|
+
* their dedup columns. Returns stats; resumable across calls (a fresh call
|
|
105
|
+
* re-seeds `seen` from the DB, so previously-indexed keys stay winners).
|
|
106
|
+
*/
|
|
107
|
+
declare function backfillDedup(backend: DedupBackfillBackend, opts: DedupBackfillOptions): Promise<DedupBackfillStats>;
|
|
108
|
+
/** Minimal D1 shape used by the sweep (subset of `D1Database`). */
|
|
109
|
+
interface D1Like {
|
|
110
|
+
prepare(sql: string): {
|
|
111
|
+
bind(...args: (string | number)[]): {
|
|
112
|
+
all(): Promise<{
|
|
113
|
+
results?: unknown[];
|
|
114
|
+
}>;
|
|
115
|
+
run(): Promise<{
|
|
116
|
+
meta?: {
|
|
117
|
+
changes?: number;
|
|
118
|
+
};
|
|
119
|
+
}>;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** D1 (Cloudflare binding) backend adapter for one (scope, collection). */
|
|
124
|
+
declare function makeD1DedupBackfillBackend(db: D1Like, scope: string, collection: string): DedupBackfillBackend;
|
|
125
|
+
/**
|
|
126
|
+
* Minimal libsql client shape (Node `_metadata.db`). `args` narrowed to the
|
|
127
|
+
* exact bound types so a real `@libsql/client` `Client` is assignable.
|
|
128
|
+
*/
|
|
129
|
+
interface LibsqlLike {
|
|
130
|
+
execute(stmt: {
|
|
131
|
+
sql: string;
|
|
132
|
+
args: (string | number)[];
|
|
133
|
+
}): Promise<{
|
|
134
|
+
rows: Array<Record<string, unknown>>;
|
|
135
|
+
rowsAffected?: number;
|
|
136
|
+
}>;
|
|
137
|
+
}
|
|
138
|
+
/** Node SQLite (libsql) backend adapter for one (scope, collection). */
|
|
139
|
+
declare function makeSqliteDedupBackfillBackend(db: LibsqlLike, scope: string, collection: string): DedupBackfillBackend;
|
|
140
|
+
/** Enumerate scopes with any NULL-dedup_key file row under the collection (D1). */
|
|
141
|
+
declare function distinctScopesWithDedupCandidatesD1(db: D1Like, collection: string): Promise<string[]>;
|
|
142
|
+
/** Enumerate candidate scopes under the collection (Node SQLite). */
|
|
143
|
+
declare function distinctScopesWithDedupCandidatesSqlite(db: LibsqlLike, collection: string): Promise<string[]>;
|
|
144
|
+
/**
|
|
145
|
+
* Config for the offline CF D1 HTTP backend: a local Node process holding a
|
|
146
|
+
* Cloudflare API token with D1-edit runs the SAME paged sweep SQL the Worker
|
|
147
|
+
* would. No R2 — dedup backfill is pure index UPDATEs (record bodies come from
|
|
148
|
+
* the inline `content` column). `fetchImpl` / `sleep` are injectable for tests.
|
|
149
|
+
*/
|
|
150
|
+
interface DedupBackfillD1HttpConfig {
|
|
151
|
+
accountId: string;
|
|
152
|
+
databaseId: string;
|
|
153
|
+
apiToken: string;
|
|
154
|
+
fetchImpl?: typeof fetch;
|
|
155
|
+
/** Bounded retries for TRANSIENT failures (5xx / 429 / network). Default 4. */
|
|
156
|
+
maxRetries?: number;
|
|
157
|
+
sleep?: (ms: number) => Promise<void>;
|
|
158
|
+
}
|
|
159
|
+
/** CF D1 HTTP-API backend adapter for one (scope, collection) — offline channel. */
|
|
160
|
+
declare function makeD1HttpDedupBackfillBackend(cfg: DedupBackfillD1HttpConfig, scope: string, collection: string): DedupBackfillBackend;
|
|
161
|
+
/** List distinct candidate scopes under the collection on a CF D1 over HTTP. */
|
|
162
|
+
declare function distinctScopesWithDedupCandidatesD1Http(cfg: DedupBackfillD1HttpConfig, collection: string): Promise<string[]>;
|
|
163
|
+
//#endregion
|
|
164
|
+
export { D1Like, DedupBackfillBackend, DedupBackfillCandidate, DedupBackfillD1HttpConfig, DedupBackfillOptions, DedupBackfillStats, LibsqlLike, backfillDedup, distinctScopesWithDedupCandidatesD1, distinctScopesWithDedupCandidatesD1Http, distinctScopesWithDedupCandidatesSqlite, makeD1DedupBackfillBackend, makeD1HttpDedupBackfillBackend, makeSqliteDedupBackfillBackend };
|
|
165
|
+
//# sourceMappingURL=dedup-backfill.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedup-backfill.d.mts","names":[],"sources":["../src/dedup-backfill.ts"],"mappings":";;AA2CA;;;;;AAYA;;;;;;;;;;;;;;;;;;;;;;AAoBA;;;;;;;;;;;;UAhCiB,sBAAA;EACf,IAAA;;EAEA,OAAA;AAAA;;;;;;;UASe,oBAAA;EAiDX;;AAsBN;;;EAjEE,YAAA,IAAgB,OAAA;EAmEV;;;;EA9DN,gBAAA,CAAiB,MAAA,UAAgB,KAAA,WAAgB,OAAA,CAAQ,sBAAA;EA6DhD;;;;;EAvDT,WAAA,CAAY,IAAA,UAAc,QAAA,WAAmB,OAAA;AAAA;AAAA,UAG9B,oBAAA;EA2IA;EAzIf,OAAA;;EAEA,QAAA;EAwIA;EAtIA,QAAA;EAuIE;;;;EAlIF,MAAA;EACA,GAAA,IAAO,GAAA;AAAA;AAAA,UAGQ,kBAAA;EAgIe;EA9H9B,OAAA;EA8HqC;EA5HrC,OAAA;EAkIwC;EAhIxC,gBAAA;EAoIqB;EAlIrB,cAAA;EA+HA;EA7HA,gBAAA;EACA,KAAA;EACA,UAAA;EA8HqB;EA5HrB,IAAA;AAAA;;;;;;iBAsBoB,aAAA,CACpB,OAAA,EAAS,oBAAA,EACT,IAAA,EAAM,oBAAA,GACL,OAAA,CAAQ,kBAAA;;UAqFM,MAAA;EACf,OAAA,CAAQ,GAAA;IACN,IAAA,IAAQ,IAAA;MACN,GAAA,IAAO,OAAA;QAAU,OAAA;MAAA;MACjB,GAAA,IAAO,OAAA;QAAU,IAAA;UAAS,OAAA;QAAA;MAAA;IAAA;EAAA;AAAA;;iBAMhB,0BAAA,CACd,EAAA,EAAI,MAAA,EACJ,KAAA,UACA,UAAA,WACC,oBAAA;;;;;UA2Bc,UAAA;EACf,OAAA,CAAQ,IAAA;IAAQ,GAAA;IAAa,IAAA;EAAA,IAA8B,OAAA;IACzD,IAAA,EAAM,KAAA,CAAM,MAAA;IACZ,YAAA;EAAA;AAAA;;iBAKY,8BAAA,CACd,EAAA,EAAI,UAAA,EACJ,KAAA,UACA,UAAA,WACC,oBAAA;;iBAsBmB,mCAAA,CACpB,EAAA,EAAI,MAAA,EACJ,UAAA,WACC,OAAA;;iBAOmB,uCAAA,CACpB,EAAA,EAAI,UAAA,EACJ,UAAA,WACC,OAAA;;;;;;;UAac,yBAAA;EACf,SAAA;EACA,UAAA;EACA,QAAA;EACA,SAAA,UAAmB,KAAA;EAHnB;EAKA,UAAA;EACA,KAAA,IAAS,EAAA,aAAe,OAAA;AAAA;;iBAkDV,8BAAA,CACd,GAAA,EAAK,yBAAA,EACL,KAAA,UACA,UAAA,WACC,oBAAA;;iBAsBmB,uCAAA,CACpB,GAAA,EAAK,yBAAA,EACL,UAAA,WACC,OAAA"}
|