@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,273 @@
|
|
|
1
|
+
import { normalizeItemUrl } from "./url-normalize.mjs";
|
|
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 = 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
|
+
export { backfillDedup, distinctScopesWithDedupCandidatesD1, distinctScopesWithDedupCandidatesD1Http, distinctScopesWithDedupCandidatesSqlite, makeD1DedupBackfillBackend, makeD1HttpDedupBackfillBackend, makeSqliteDedupBackfillBackend };
|
|
273
|
+
//# sourceMappingURL=dedup-backfill.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedup-backfill.mjs","names":[],"sources":["../src/dedup-backfill.ts"],"sourcesContent":["/**\n * dedup-backfill — one-time backfill of the dedup-on-write secondary index\n * (`ds_entries.dedup_scope` / `dedup_key`) for records written BEFORE #1159\n * landed (planning: dedup-on-write, issue #1159).\n *\n * Pre-#1159 the dedup entry lived at `{collection}/{cid(url)}.json` and had no\n * `dedup_key` column; older layouts (`{slug}.json`, `{uuid}/item.json`) never\n * had one either. After #1159 dedup resolves through the\n * `(scope, dedup_scope, dedup_key)` UNIQUE index, so those historical rows are\n * INVISIBLE to `findByDedupKey` — an already-saved URL's first re-save creates\n * a duplicate. This sweep populates their columns so a re-save merges in place.\n *\n * Design (mirrors ctime-backfill / inline-content-migration):\n * - OFFLINE, code-driven. The core loop {@link backfillDedup} is backend-\n * agnostic; the SQL lives in {@link DedupBackfillBackend} adapters (D1 binding\n * vs Node SQLite vs the CF D1 HTTP API) so it is unit-testable against a mock.\n * - **Host-language normalization** — the `dedup_key` is\n * `normalizeItemUrl(record[dedupBy])`, computed in JS with the SAME function\n * the write path uses. It CANNOT be done in SQL (`json_extract` would store\n * the RAW url, which would never match the Worker's normalized lookups), so\n * the sweep MUST read the record body (`content` column) and normalize here.\n * - **JS uniqueness pre-filter, never DB rejection.** The `(scope, dedup_scope,\n * dedup_key)` UNIQUE index means two rows can't share a key. A `seen` set —\n * seeded from the keys ALREADY indexed (e.g. entries a post-#1159 client\n * already wrote) and grown as the sweep assigns — decides the winner in JS so\n * NO `UPDATE` ever violates the constraint. This matters over the D1 HTTP\n * channel where a constraint error is a 4xx that would abort the whole run.\n * A genuine pre-existing duplicate is left un-indexed (reported), NEVER merged\n * (the tool does not touch content).\n * - **WHERE pins `scope` + the collection prefix**, binds everything as\n * parameters — never a cross-scope / cross-collection UPDATE.\n * - **Deterministic `ORDER BY path ASC` keyset cursor** — resumable across\n * invocations; among genuine duplicates the FIRST in path order wins the key.\n * - **Idempotent**: `UPDATE … WHERE dedup_key IS NULL` — a re-run never\n * re-touches an already-populated row (0 write). The cursor advances past\n * EVERY scanned row so one invocation can't loop.\n * - **Only dedup_scope/dedup_key change** — never cid/size/content/mtime/ctime\n * (index-only; no changelog / no sync fan-out).\n */\n\nimport { normalizeItemUrl } from \"./url-normalize.js\";\n\n/** A dedup-index candidate: a FILE row with a NULL `dedup_key`. */\nexport interface DedupBackfillCandidate {\n path: string;\n /** Raw `entries.content` inline record body (host-parsed here, never in SQL). */\n content: string | null;\n}\n\n/**\n * Backend-specific SQL for the sweep (D1 binding vs Node SQLite vs D1 HTTP).\n * One `(scope, collection)` per adapter — `collection` is the full stored\n * collection path (e.g. `blocklets/aside/user/items`), which is BOTH the value\n * written to `dedup_scope` and the `path` prefix that scopes the sweep.\n */\nexport interface DedupBackfillBackend {\n /**\n * `dedup_key`s already claimed within this `(scope, collection)` — seeds the\n * uniqueness `seen` set so the sweep never collides with a row a post-#1159\n * client already indexed.\n */\n existingKeys(): Promise<string[]>;\n /**\n * Select up to `limit` FILE rows with `dedup_key IS NULL` under the collection\n * and `path > cursor`, ordered `path ASC` (the keyset cursor). Empty ⇒ done.\n */\n selectCandidates(cursor: string, limit: number): Promise<DedupBackfillCandidate[]>;\n /**\n * Set `dedup_scope` (= the collection) + `dedup_key` on the row, guarded\n * `AND dedup_key IS NULL` so an already-populated row is a 0-row no-op.\n * Returns the number of rows actually changed (0 or 1).\n */\n setDedupKey(path: string, dedupKey: string): Promise<number>;\n}\n\nexport interface DedupBackfillOptions {\n /** Record field to dedup by (e.g. `\"url\"`). Required. */\n dedupBy: string;\n /** Rows per page. Default 100 (content is heavier than a metadata blob). */\n pageSize?: number;\n /** Cap pages this invocation (cross-invocation paging). Default unbounded. */\n maxPages?: number;\n /**\n * Preview only — scan + count, run NO UPDATE. `indexed` then reports rows that\n * WOULD be indexed; all skip counts are exact in both modes.\n */\n dryRun?: boolean;\n log?: (msg: string) => void;\n}\n\nexport interface DedupBackfillStats {\n /** FILE rows examined (dedup_key IS NULL under the collection). */\n scanned: number;\n /** Rows whose dedup columns were populated (or WOULD be, in dryRun). */\n indexed: number;\n /** Rows with no inline `content` — cannot extract the field, left as-is. */\n skippedNoContent: number;\n /** Rows whose body lacked a usable `dedupBy` field — not dedup-able. */\n skippedNoField: number;\n /** Rows whose key was already claimed — a genuine duplicate, left un-indexed. */\n skippedDuplicate: number;\n pages: number;\n lastCursor: string;\n /** True when a page came back empty (the sweep reached the end). */\n done: boolean;\n}\n\n/** Extract the record body's `dedupBy` field as a non-empty string, or null. */\nfunction extractKeyField(content: string | null, dedupBy: string): string | null {\n if (content == null) return null;\n let body: unknown;\n try {\n body = JSON.parse(content);\n } catch {\n return null;\n }\n if (body === null || typeof body !== \"object\" || Array.isArray(body)) return null;\n const v = (body as Record<string, unknown>)[dedupBy];\n return typeof v === \"string\" && v.length > 0 ? v : null;\n}\n\n/**\n * Page over NULL-`dedup_key` FILE rows in one `(scope, collection)` and populate\n * their dedup columns. Returns stats; resumable across calls (a fresh call\n * re-seeds `seen` from the DB, so previously-indexed keys stay winners).\n */\nexport async function backfillDedup(\n backend: DedupBackfillBackend,\n opts: DedupBackfillOptions,\n): Promise<DedupBackfillStats> {\n const pageSize = opts.pageSize ?? 100;\n const maxPages = opts.maxPages ?? Number.POSITIVE_INFINITY;\n const dryRun = opts.dryRun ?? false;\n const log = opts.log ?? (() => {});\n\n const stats: DedupBackfillStats = {\n scanned: 0,\n indexed: 0,\n skippedNoContent: 0,\n skippedNoField: 0,\n skippedDuplicate: 0,\n pages: 0,\n lastCursor: \"\",\n done: false,\n };\n\n // Seed the uniqueness set from keys already claimed under this collection\n // (e.g. entries a post-#1159 client already wrote) so the sweep defers to them.\n const seen = new Set<string>(await backend.existingKeys());\n\n let cursor = \"\";\n while (stats.pages < maxPages) {\n const rows = await backend.selectCandidates(cursor, pageSize);\n if (rows.length === 0) {\n stats.done = true;\n break;\n }\n stats.pages++;\n // SERIAL — the `seen` check+add must be atomic per key (a concurrent pool\n // could let two rows both claim one key and collide at the DB).\n for (const row of rows) {\n stats.scanned++;\n cursor = row.path; // advance past EVERY scanned row so one pass can't loop\n const raw = extractKeyField(row.content, opts.dedupBy);\n if (raw === null) {\n if (row.content == null) stats.skippedNoContent++;\n else stats.skippedNoField++;\n continue;\n }\n const key = normalizeItemUrl(raw);\n if (seen.has(key)) {\n stats.skippedDuplicate++; // pre-existing duplicate — leave un-indexed\n continue;\n }\n seen.add(key);\n if (dryRun) {\n stats.indexed++; // would-index (no UPDATE issued)\n continue;\n }\n const changed = await backend.setDedupKey(row.path, key);\n if (changed > 0) stats.indexed++;\n }\n stats.lastCursor = cursor;\n log(\n `[dedup-backfill] page ${stats.pages}: scanned=${stats.scanned} indexed=${stats.indexed} dup=${stats.skippedDuplicate} cursor=${cursor}`,\n );\n }\n return stats;\n}\n\n// ── SQL (shared by all adapters; entries PK is (scope, path)) ────────────────\n//\n// `substr(path,1,?)=?` scopes the sweep to ONE collection prefix (the sweep is\n// per-app, e.g. `blocklets/aside/user/items/`). Not index-optimized, but this is\n// a bounded one-time offline sweep, never a hot path.\nconst SELECT_SQL =\n \"SELECT path, content FROM ds_entries WHERE scope = ? AND kind = 'file' \" +\n \"AND dedup_key IS NULL AND path > ? AND substr(path, 1, ?) = ? ORDER BY path ASC LIMIT ?\";\nconst EXISTING_KEYS_SQL =\n \"SELECT dedup_key FROM ds_entries WHERE scope = ? AND dedup_scope = ? AND dedup_key IS NOT NULL\";\n// Guarded `AND dedup_key IS NULL` → an already-populated row is a 0-row no-op\n// (idempotent re-runs). Bind order: (dedup_scope, dedup_key, scope, path).\nconst UPDATE_SQL =\n \"UPDATE ds_entries SET dedup_scope = ?, dedup_key = ? WHERE scope = ? AND path = ? AND kind = 'file' AND dedup_key IS NULL\";\nconst DISTINCT_SCOPES_SQL =\n \"SELECT DISTINCT scope FROM ds_entries WHERE kind = 'file' AND dedup_key IS NULL AND substr(path, 1, ?) = ?\";\n\n/** The `path` prefix (`{collection}/`) + its length, for the substr filter. */\nfunction collectionPrefix(collection: string): { prefix: string; len: number } {\n const prefix = collection.endsWith(\"/\") ? collection : `${collection}/`;\n return { prefix, len: prefix.length };\n}\n\n/** Minimal D1 shape used by the sweep (subset of `D1Database`). */\nexport interface D1Like {\n prepare(sql: string): {\n bind(...args: (string | number)[]): {\n all(): Promise<{ results?: unknown[] }>;\n run(): Promise<{ meta?: { changes?: number } }>;\n };\n };\n}\n\n/** D1 (Cloudflare binding) backend adapter for one (scope, collection). */\nexport function makeD1DedupBackfillBackend(\n db: D1Like,\n scope: string,\n collection: string,\n): DedupBackfillBackend {\n const { prefix, len } = collectionPrefix(collection);\n return {\n async existingKeys() {\n const res = await db.prepare(EXISTING_KEYS_SQL).bind(scope, collection).all();\n return ((res.results ?? []) as Array<Record<string, unknown>>).map((r) =>\n String(r.dedup_key),\n );\n },\n async selectCandidates(cursor, limit) {\n const res = await db.prepare(SELECT_SQL).bind(scope, cursor, len, prefix, limit).all();\n return ((res.results ?? []) as Array<Record<string, unknown>>).map((r) => ({\n path: String(r.path),\n content: r.content === null || r.content === undefined ? null : String(r.content),\n }));\n },\n async setDedupKey(path, dedupKey) {\n const r = await db.prepare(UPDATE_SQL).bind(collection, dedupKey, scope, path).run();\n return r.meta?.changes ?? 0;\n },\n };\n}\n\n/**\n * Minimal libsql client shape (Node `_metadata.db`). `args` narrowed to the\n * exact bound types so a real `@libsql/client` `Client` is assignable.\n */\nexport interface LibsqlLike {\n execute(stmt: { sql: string; args: (string | number)[] }): Promise<{\n rows: Array<Record<string, unknown>>;\n rowsAffected?: number;\n }>;\n}\n\n/** Node SQLite (libsql) backend adapter for one (scope, collection). */\nexport function makeSqliteDedupBackfillBackend(\n db: LibsqlLike,\n scope: string,\n collection: string,\n): DedupBackfillBackend {\n const { prefix, len } = collectionPrefix(collection);\n return {\n async existingKeys() {\n const res = await db.execute({ sql: EXISTING_KEYS_SQL, args: [scope, collection] });\n return res.rows.map((r) => String(r.dedup_key));\n },\n async selectCandidates(cursor, limit) {\n const res = await db.execute({ sql: SELECT_SQL, args: [scope, cursor, len, prefix, limit] });\n return res.rows.map((r) => ({\n path: String(r.path),\n content: r.content === null || r.content === undefined ? null : String(r.content),\n }));\n },\n async setDedupKey(path, dedupKey) {\n const r = await db.execute({ sql: UPDATE_SQL, args: [collection, dedupKey, scope, path] });\n return r.rowsAffected ?? 0;\n },\n };\n}\n\n/** Enumerate scopes with any NULL-dedup_key file row under the collection (D1). */\nexport async function distinctScopesWithDedupCandidatesD1(\n db: D1Like,\n collection: string,\n): Promise<string[]> {\n const { prefix, len } = collectionPrefix(collection);\n const res = await db.prepare(DISTINCT_SCOPES_SQL).bind(len, prefix).all();\n return ((res.results ?? []) as Array<Record<string, unknown>>).map((r) => String(r.scope));\n}\n\n/** Enumerate candidate scopes under the collection (Node SQLite). */\nexport async function distinctScopesWithDedupCandidatesSqlite(\n db: LibsqlLike,\n collection: string,\n): Promise<string[]> {\n const { prefix, len } = collectionPrefix(collection);\n const res = await db.execute({ sql: DISTINCT_SCOPES_SQL, args: [len, prefix] });\n return res.rows.map((r) => String(r.scope));\n}\n\n// ── CF D1 over the HTTP API (offline admin channel for staging/prod) ─────────\n/**\n * Config for the offline CF D1 HTTP backend: a local Node process holding a\n * Cloudflare API token with D1-edit runs the SAME paged sweep SQL the Worker\n * would. No R2 — dedup backfill is pure index UPDATEs (record bodies come from\n * the inline `content` column). `fetchImpl` / `sleep` are injectable for tests.\n */\nexport interface DedupBackfillD1HttpConfig {\n accountId: string;\n databaseId: string;\n apiToken: string;\n fetchImpl?: typeof fetch;\n /** Bounded retries for TRANSIENT failures (5xx / 429 / network). Default 4. */\n maxRetries?: number;\n sleep?: (ms: number) => Promise<void>;\n}\n\nconst defaultSleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));\n\nasync function d1HttpQuery(\n cfg: DedupBackfillD1HttpConfig,\n sql: string,\n params: (string | number)[],\n): Promise<{ results: Array<Record<string, unknown>>; changes: number }> {\n const f = cfg.fetchImpl ?? fetch;\n const sleep = cfg.sleep ?? defaultSleep;\n const maxRetries = cfg.maxRetries ?? 4;\n const url = `https://api.cloudflare.com/client/v4/accounts/${cfg.accountId}/d1/database/${cfg.databaseId}/query`;\n for (let attempt = 0; ; attempt++) {\n let status = 0;\n let json:\n | {\n success?: boolean;\n errors?: unknown;\n result?: Array<{ results?: Array<Record<string, unknown>>; meta?: { changes?: number } }>;\n }\n | undefined;\n let networkErr: unknown;\n try {\n const res = await f(url, {\n method: \"POST\",\n headers: { authorization: `Bearer ${cfg.apiToken}`, \"content-type\": \"application/json\" },\n body: JSON.stringify({ sql, params }),\n });\n status = res.status;\n json = await res.json();\n } catch (e) {\n networkErr = e; // network/parse failure → transient\n }\n if (!networkErr && status >= 200 && status < 300 && json?.success !== false) {\n const r = json?.result?.[0];\n return { results: r?.results ?? [], changes: r?.meta?.changes ?? 0 };\n }\n // 5xx / 429 / network → transient (retry); 4xx (auth/SQL) → permanent.\n const transient = networkErr != null || status >= 500 || status === 429;\n const msg = networkErr\n ? `D1 HTTP network error: ${String(networkErr).slice(0, 160)}`\n : `D1 HTTP ${status}: ${JSON.stringify(json?.errors ?? json).slice(0, 200)}`;\n if (!transient || attempt >= maxRetries) throw new Error(msg);\n await sleep(Math.min(2000, 200 * 2 ** attempt));\n }\n}\n\n/** CF D1 HTTP-API backend adapter for one (scope, collection) — offline channel. */\nexport function makeD1HttpDedupBackfillBackend(\n cfg: DedupBackfillD1HttpConfig,\n scope: string,\n collection: string,\n): DedupBackfillBackend {\n const { prefix, len } = collectionPrefix(collection);\n return {\n async existingKeys() {\n const { results } = await d1HttpQuery(cfg, EXISTING_KEYS_SQL, [scope, collection]);\n return results.map((r) => String(r.dedup_key));\n },\n async selectCandidates(cursor, limit) {\n const { results } = await d1HttpQuery(cfg, SELECT_SQL, [scope, cursor, len, prefix, limit]);\n return results.map((r) => ({\n path: String(r.path),\n content: r.content === null || r.content === undefined ? null : String(r.content),\n }));\n },\n async setDedupKey(path, dedupKey) {\n const { changes } = await d1HttpQuery(cfg, UPDATE_SQL, [collection, dedupKey, scope, path]);\n return changes;\n },\n };\n}\n\n/** List distinct candidate scopes under the collection on a CF D1 over HTTP. */\nexport async function distinctScopesWithDedupCandidatesD1Http(\n cfg: DedupBackfillD1HttpConfig,\n collection: string,\n): Promise<string[]> {\n const { prefix, len } = collectionPrefix(collection);\n const { results } = await d1HttpQuery(cfg, DISTINCT_SCOPES_SQL, [len, prefix]);\n return results.map((r) => String(r.scope));\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/did-utils.ts
|
|
3
|
+
/**
|
|
4
|
+
* ID validation and normalization utilities.
|
|
5
|
+
*
|
|
6
|
+
* All storage paths use bare identifiers (e.g., "z3hxxx", "persona"),
|
|
7
|
+
* never full DID format (e.g., "did:abt:z3hxxx"). The normalizeId()
|
|
8
|
+
* function strips any "did:method:" prefix at API boundaries.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Validate an identifier string for safety.
|
|
12
|
+
* Accepts both bare IDs ("persona") and full DIDs ("did:abt:persona").
|
|
13
|
+
* Rejects path separators, traversal patterns, and null bytes.
|
|
14
|
+
*/
|
|
15
|
+
function validateId(id) {
|
|
16
|
+
if (!id || typeof id !== "string") throw new Error("ID must be a non-empty string");
|
|
17
|
+
if (id.length > 128) throw new Error(`ID exceeds max length: ${id.length}`);
|
|
18
|
+
if (/[/\\]/.test(id)) throw new Error(`ID contains path separator: ${id}`);
|
|
19
|
+
if (id.includes("..")) throw new Error(`ID contains path traversal: ${id}`);
|
|
20
|
+
if (id.includes("\0")) throw new Error(`ID contains null byte: ${id}`);
|
|
21
|
+
let decoded = id;
|
|
22
|
+
try {
|
|
23
|
+
decoded = decodeURIComponent(id);
|
|
24
|
+
} catch {}
|
|
25
|
+
if (decoded !== id) {
|
|
26
|
+
if (/[/\\]/.test(decoded)) throw new Error(`ID contains encoded path separator: ${id}`);
|
|
27
|
+
if (decoded.includes("..")) throw new Error(`ID contains encoded traversal: ${id}`);
|
|
28
|
+
if (decoded.includes("\0")) throw new Error(`ID contains encoded null byte: ${id}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Normalize an identifier by stripping any "did:method:" prefix.
|
|
33
|
+
*
|
|
34
|
+
* "did:abt:persona" → "persona"
|
|
35
|
+
* "did:web:example.com" → "example.com"
|
|
36
|
+
* "persona" → "persona" (unchanged)
|
|
37
|
+
*/
|
|
38
|
+
function normalizeId(input) {
|
|
39
|
+
return input.match(/^did:[a-z0-9]+:(.+)$/i)?.[1] ?? input;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
exports.normalizeId = normalizeId;
|
|
44
|
+
exports.validateId = validateId;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/did-utils.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* ID validation and normalization utilities.
|
|
4
|
+
*
|
|
5
|
+
* All storage paths use bare identifiers (e.g., "z3hxxx", "persona"),
|
|
6
|
+
* never full DID format (e.g., "did:abt:z3hxxx"). The normalizeId()
|
|
7
|
+
* function strips any "did:method:" prefix at API boundaries.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Validate an identifier string for safety.
|
|
11
|
+
* Accepts both bare IDs ("persona") and full DIDs ("did:abt:persona").
|
|
12
|
+
* Rejects path separators, traversal patterns, and null bytes.
|
|
13
|
+
*/
|
|
14
|
+
declare function validateId(id: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Normalize an identifier by stripping any "did:method:" prefix.
|
|
17
|
+
*
|
|
18
|
+
* "did:abt:persona" → "persona"
|
|
19
|
+
* "did:web:example.com" → "example.com"
|
|
20
|
+
* "persona" → "persona" (unchanged)
|
|
21
|
+
*/
|
|
22
|
+
declare function normalizeId(input: string): string;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { normalizeId, validateId };
|
|
25
|
+
//# sourceMappingURL=did-utils.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"did-utils.d.cts","names":[],"sources":["../src/did-utils.ts"],"mappings":";;AAaA;;;;;AAsCA;;;;;;iBAtCgB,UAAA,CAAW,EAAA;;;;;;;;iBAsCX,WAAA,CAAY,KAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/did-utils.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* ID validation and normalization utilities.
|
|
4
|
+
*
|
|
5
|
+
* All storage paths use bare identifiers (e.g., "z3hxxx", "persona"),
|
|
6
|
+
* never full DID format (e.g., "did:abt:z3hxxx"). The normalizeId()
|
|
7
|
+
* function strips any "did:method:" prefix at API boundaries.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Validate an identifier string for safety.
|
|
11
|
+
* Accepts both bare IDs ("persona") and full DIDs ("did:abt:persona").
|
|
12
|
+
* Rejects path separators, traversal patterns, and null bytes.
|
|
13
|
+
*/
|
|
14
|
+
declare function validateId(id: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Normalize an identifier by stripping any "did:method:" prefix.
|
|
17
|
+
*
|
|
18
|
+
* "did:abt:persona" → "persona"
|
|
19
|
+
* "did:web:example.com" → "example.com"
|
|
20
|
+
* "persona" → "persona" (unchanged)
|
|
21
|
+
*/
|
|
22
|
+
declare function normalizeId(input: string): string;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { normalizeId, validateId };
|
|
25
|
+
//# sourceMappingURL=did-utils.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"did-utils.d.mts","names":[],"sources":["../src/did-utils.ts"],"mappings":";;AAaA;;;;;AAsCA;;;;;;iBAtCgB,UAAA,CAAW,EAAA;;;;;;;;iBAsCX,WAAA,CAAY,KAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/did-utils.ts
|
|
2
|
+
/**
|
|
3
|
+
* ID validation and normalization utilities.
|
|
4
|
+
*
|
|
5
|
+
* All storage paths use bare identifiers (e.g., "z3hxxx", "persona"),
|
|
6
|
+
* never full DID format (e.g., "did:abt:z3hxxx"). The normalizeId()
|
|
7
|
+
* function strips any "did:method:" prefix at API boundaries.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Validate an identifier string for safety.
|
|
11
|
+
* Accepts both bare IDs ("persona") and full DIDs ("did:abt:persona").
|
|
12
|
+
* Rejects path separators, traversal patterns, and null bytes.
|
|
13
|
+
*/
|
|
14
|
+
function validateId(id) {
|
|
15
|
+
if (!id || typeof id !== "string") throw new Error("ID must be a non-empty string");
|
|
16
|
+
if (id.length > 128) throw new Error(`ID exceeds max length: ${id.length}`);
|
|
17
|
+
if (/[/\\]/.test(id)) throw new Error(`ID contains path separator: ${id}`);
|
|
18
|
+
if (id.includes("..")) throw new Error(`ID contains path traversal: ${id}`);
|
|
19
|
+
if (id.includes("\0")) throw new Error(`ID contains null byte: ${id}`);
|
|
20
|
+
let decoded = id;
|
|
21
|
+
try {
|
|
22
|
+
decoded = decodeURIComponent(id);
|
|
23
|
+
} catch {}
|
|
24
|
+
if (decoded !== id) {
|
|
25
|
+
if (/[/\\]/.test(decoded)) throw new Error(`ID contains encoded path separator: ${id}`);
|
|
26
|
+
if (decoded.includes("..")) throw new Error(`ID contains encoded traversal: ${id}`);
|
|
27
|
+
if (decoded.includes("\0")) throw new Error(`ID contains encoded null byte: ${id}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Normalize an identifier by stripping any "did:method:" prefix.
|
|
32
|
+
*
|
|
33
|
+
* "did:abt:persona" → "persona"
|
|
34
|
+
* "did:web:example.com" → "example.com"
|
|
35
|
+
* "persona" → "persona" (unchanged)
|
|
36
|
+
*/
|
|
37
|
+
function normalizeId(input) {
|
|
38
|
+
return input.match(/^did:[a-z0-9]+:(.+)$/i)?.[1] ?? input;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { normalizeId, validateId };
|
|
43
|
+
//# sourceMappingURL=did-utils.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"did-utils.mjs","names":[],"sources":["../src/did-utils.ts"],"sourcesContent":["/**\n * ID validation and normalization utilities.\n *\n * All storage paths use bare identifiers (e.g., \"z3hxxx\", \"persona\"),\n * never full DID format (e.g., \"did:abt:z3hxxx\"). The normalizeId()\n * function strips any \"did:method:\" prefix at API boundaries.\n */\n\n/**\n * Validate an identifier string for safety.\n * Accepts both bare IDs (\"persona\") and full DIDs (\"did:abt:persona\").\n * Rejects path separators, traversal patterns, and null bytes.\n */\nexport function validateId(id: string): void {\n if (!id || typeof id !== \"string\") {\n throw new Error(\"ID must be a non-empty string\");\n }\n // Length limit: DID bare identifiers are 36-60 bytes; 128 gives 2x headroom\n if (id.length > 128) {\n throw new Error(`ID exceeds max length: ${id.length}`);\n }\n if (/[/\\\\]/.test(id)) {\n throw new Error(`ID contains path separator: ${id}`);\n }\n if (id.includes(\"..\")) {\n throw new Error(`ID contains path traversal: ${id}`);\n }\n if (id.includes(\"\\0\")) {\n throw new Error(`ID contains null byte: ${id}`);\n }\n // Decode-and-recheck: catch %2e%2e and similar percent-encoded bypasses\n let decoded = id;\n try {\n decoded = decodeURIComponent(id);\n } catch {\n /* treat as literal if malformed */\n }\n if (decoded !== id) {\n if (/[/\\\\]/.test(decoded)) throw new Error(`ID contains encoded path separator: ${id}`);\n if (decoded.includes(\"..\")) throw new Error(`ID contains encoded traversal: ${id}`);\n if (decoded.includes(\"\\0\")) throw new Error(`ID contains encoded null byte: ${id}`);\n }\n}\n\n/**\n * Normalize an identifier by stripping any \"did:method:\" prefix.\n *\n * \"did:abt:persona\" → \"persona\"\n * \"did:web:example.com\" → \"example.com\"\n * \"persona\" → \"persona\" (unchanged)\n */\nexport function normalizeId(input: string): string {\n const match = input.match(/^did:[a-z0-9]+:(.+)$/i);\n return match?.[1] ?? input;\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,SAAgB,WAAW,IAAkB;AAC3C,KAAI,CAAC,MAAM,OAAO,OAAO,SACvB,OAAM,IAAI,MAAM,gCAAgC;AAGlD,KAAI,GAAG,SAAS,IACd,OAAM,IAAI,MAAM,0BAA0B,GAAG,SAAS;AAExD,KAAI,QAAQ,KAAK,GAAG,CAClB,OAAM,IAAI,MAAM,+BAA+B,KAAK;AAEtD,KAAI,GAAG,SAAS,KAAK,CACnB,OAAM,IAAI,MAAM,+BAA+B,KAAK;AAEtD,KAAI,GAAG,SAAS,KAAK,CACnB,OAAM,IAAI,MAAM,0BAA0B,KAAK;CAGjD,IAAI,UAAU;AACd,KAAI;AACF,YAAU,mBAAmB,GAAG;SAC1B;AAGR,KAAI,YAAY,IAAI;AAClB,MAAI,QAAQ,KAAK,QAAQ,CAAE,OAAM,IAAI,MAAM,uCAAuC,KAAK;AACvF,MAAI,QAAQ,SAAS,KAAK,CAAE,OAAM,IAAI,MAAM,kCAAkC,KAAK;AACnF,MAAI,QAAQ,SAAS,KAAK,CAAE,OAAM,IAAI,MAAM,kCAAkC,KAAK;;;;;;;;;;AAWvF,SAAgB,YAAY,OAAuB;AAEjD,QADc,MAAM,MAAM,wBAAwB,GACnC,MAAM"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/dirty-path-queue.ts
|
|
3
|
+
/**
|
|
4
|
+
* Apply one changelog-derived write to a Memory dirty map entry, reproducing the
|
|
5
|
+
* `ds_changelog_to_ds_dirty_path` trigger's merge/guard EXACTLY:
|
|
6
|
+
* - no existing row → the incoming row (attempts/backoff/error zeroed).
|
|
7
|
+
* - incoming.latestSeq > existing.latestSeq → overwrite + reset attempts/
|
|
8
|
+
* next_attempt_at/last_error.
|
|
9
|
+
* - stale incoming (seq <= existing) → keep existing unchanged (no rollback).
|
|
10
|
+
*
|
|
11
|
+
* Returned value is what the map should store for `(scope, path)`.
|
|
12
|
+
*/
|
|
13
|
+
function applyDirtyUpsert(existing, incoming) {
|
|
14
|
+
const next = {
|
|
15
|
+
scope: incoming.scope,
|
|
16
|
+
path: incoming.path,
|
|
17
|
+
latestSeq: incoming.latestSeq,
|
|
18
|
+
op: incoming.op,
|
|
19
|
+
cid: incoming.cid,
|
|
20
|
+
mtime: incoming.mtime,
|
|
21
|
+
updatedAt: incoming.updatedAt,
|
|
22
|
+
attempts: 0,
|
|
23
|
+
nextAttemptAt: 0,
|
|
24
|
+
lastError: null
|
|
25
|
+
};
|
|
26
|
+
if (!existing) return next;
|
|
27
|
+
return incoming.latestSeq > existing.latestSeq ? next : existing;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
exports.applyDirtyUpsert = applyDirtyUpsert;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { AFSRoot } from "@aigne/afs";
|
|
2
|
+
|
|
3
|
+
//#region src/dirty-path-queue.d.ts
|
|
4
|
+
/** Latest op merged for a dirty path. Mirrors `ds_changelog.op` / the CHECK. */
|
|
5
|
+
type DirtyPathOp = "put" | "delete";
|
|
6
|
+
/**
|
|
7
|
+
* A dirty row as handed to the follower. The follower uses `op/cid/latestSeq`
|
|
8
|
+
* for scheduling + the ack race-guard ONLY — it re-reads current live state
|
|
9
|
+
* before writing the index (design §4.3), so `cid` here is never trusted as the
|
|
10
|
+
* final content. `path` is the form the queue returns: full DID-space path from
|
|
11
|
+
* a raw backend, or the prefix-STRIPPED `originalPath` from
|
|
12
|
+
* {@link PrefixedTreeIndex} (e.g. `items/x.json`).
|
|
13
|
+
*/
|
|
14
|
+
interface DirtyPathRow {
|
|
15
|
+
path: string;
|
|
16
|
+
latestSeq: number;
|
|
17
|
+
op: DirtyPathOp;
|
|
18
|
+
cid?: string | null;
|
|
19
|
+
mtime: number;
|
|
20
|
+
attempts: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Full stored shape of a `ds_dirty_path` row (all columns). SQL backends store
|
|
24
|
+
* it as a table row; the Memory backend keeps a `Map<\`${scope}\0${path}\`,
|
|
25
|
+
* DirtyPathStoredRow>` and mutates it through {@link applyDirtyUpsert} so its
|
|
26
|
+
* merge/guard semantics match the D1/SQLite trigger byte-for-byte.
|
|
27
|
+
*/
|
|
28
|
+
interface DirtyPathStoredRow {
|
|
29
|
+
scope: string;
|
|
30
|
+
path: string;
|
|
31
|
+
latestSeq: number;
|
|
32
|
+
op: DirtyPathOp;
|
|
33
|
+
cid: string | null;
|
|
34
|
+
mtime: number;
|
|
35
|
+
updatedAt: number;
|
|
36
|
+
attempts: number;
|
|
37
|
+
nextAttemptAt: number;
|
|
38
|
+
lastError: string | null;
|
|
39
|
+
}
|
|
40
|
+
interface ListDirtyPathOptions {
|
|
41
|
+
/** Full DID-space path prefix to scan (e.g. `blocklets/{instanceDid}/user/`). */
|
|
42
|
+
prefix: string;
|
|
43
|
+
/** Max dirty rows to return this tick. */
|
|
44
|
+
limit: number;
|
|
45
|
+
/** Clock override (ms) for the `next_attempt_at <= now` ready filter; defaults to the wall clock. */
|
|
46
|
+
now?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Source-side dirty queue — list ready rows, ack a processed row, or record a
|
|
50
|
+
* failed attempt. All three carry the `latestSeq` race guard (design §8): an
|
|
51
|
+
* ack/mark that targets a `latest_seq` a concurrent write has already superseded
|
|
52
|
+
* affects 0 rows and returns `false`, leaving the newer dirty row for the next
|
|
53
|
+
* tick.
|
|
54
|
+
*/
|
|
55
|
+
interface DirtyPathQueue {
|
|
56
|
+
/** Ready dirty rows under `prefix` with `next_attempt_at <= now`, capped at `limit`. */
|
|
57
|
+
listDirtyPaths(options: ListDirtyPathOptions): Promise<DirtyPathRow[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Delete the dirty row for `path` iff its `latest_seq` still equals
|
|
60
|
+
* `latestSeq` (nothing newer arrived). Returns `true` when a row was removed,
|
|
61
|
+
* `false` when the guard lost (a newer write bumped latest_seq — leave it).
|
|
62
|
+
*/
|
|
63
|
+
ackDirtyPath(path: string, latestSeq: number): Promise<boolean>;
|
|
64
|
+
/**
|
|
65
|
+
* Record a failed attempt: `attempts+1`, set `last_error`/`next_attempt_at`,
|
|
66
|
+
* guarded by the SAME `latest_seq` equality so a fresh write's reset is never
|
|
67
|
+
* clobbered. Returns `true` when the guarded row was updated.
|
|
68
|
+
*/
|
|
69
|
+
markDirtyPathError(path: string, latestSeq: number, error: string, nextAttemptAt: number): Promise<boolean>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The follower's two source planes, wired explicitly so the AFS read plane and
|
|
73
|
+
* the dirty ack plane never get conflated (design §7):
|
|
74
|
+
* - `contentAfs`: read/stat CURRENT live content only.
|
|
75
|
+
* - `dirtyQueue`: list/ack/fail dirty rows only.
|
|
76
|
+
* They may share one underlying source-side TreeIndex, but ack/fail is a
|
|
77
|
+
* runtime-internal capability — it is never exposed as a `/user` AFS action.
|
|
78
|
+
*/
|
|
79
|
+
interface DirtyPathFollowerSource {
|
|
80
|
+
contentAfs: AFSRoot;
|
|
81
|
+
dirtyQueue: DirtyPathQueue;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Apply one changelog-derived write to a Memory dirty map entry, reproducing the
|
|
85
|
+
* `ds_changelog_to_ds_dirty_path` trigger's merge/guard EXACTLY:
|
|
86
|
+
* - no existing row → the incoming row (attempts/backoff/error zeroed).
|
|
87
|
+
* - incoming.latestSeq > existing.latestSeq → overwrite + reset attempts/
|
|
88
|
+
* next_attempt_at/last_error.
|
|
89
|
+
* - stale incoming (seq <= existing) → keep existing unchanged (no rollback).
|
|
90
|
+
*
|
|
91
|
+
* Returned value is what the map should store for `(scope, path)`.
|
|
92
|
+
*/
|
|
93
|
+
declare function applyDirtyUpsert(existing: DirtyPathStoredRow | undefined, incoming: {
|
|
94
|
+
scope: string;
|
|
95
|
+
path: string;
|
|
96
|
+
latestSeq: number;
|
|
97
|
+
op: DirtyPathOp;
|
|
98
|
+
cid: string | null;
|
|
99
|
+
mtime: number;
|
|
100
|
+
updatedAt: number;
|
|
101
|
+
}): DirtyPathStoredRow;
|
|
102
|
+
//#endregion
|
|
103
|
+
export { DirtyPathFollowerSource, DirtyPathOp, DirtyPathQueue, DirtyPathRow, DirtyPathStoredRow, ListDirtyPathOptions, applyDirtyUpsert };
|
|
104
|
+
//# sourceMappingURL=dirty-path-queue.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dirty-path-queue.d.cts","names":[],"sources":["../src/dirty-path-queue.ts"],"mappings":";;;;KAwCY,WAAA;;;;;;;;;UAUK,YAAA;EACf,IAAA;EACA,SAAA;EACA,EAAA,EAAI,WAAA;EACJ,GAAA;EACA,KAAA;EACA,QAAA;AAAA;;;;;;;UASe,kBAAA;EACf,KAAA;EACA,IAAA;EACA,SAAA;EACA,EAAA,EAAI,WAAA;EACJ,GAAA;EACA,KAAA;EACA,SAAA;EACA,QAAA;EACA,aAAA;EACA,SAAA;AAAA;AAAA,UAGe,oBAAA;EA8CA;EA5Cf,MAAA;;EAEA,KAAA;EA2CA;EAzCA,GAAA;AAAA;;;;AAuDF;;;;UA7CiB,cAAA;EAwDd;EAtDD,cAAA,CAAe,OAAA,EAAS,oBAAA,GAAuB,OAAA,CAAQ,YAAA;EAsDpC;;;;;EAhDnB,YAAA,CAAa,IAAA,UAAc,SAAA,WAAoB,OAAA;EA2C7C;;;;;EArCF,kBAAA,CACE,IAAA,UACA,SAAA,UACA,KAAA,UACA,aAAA,WACC,OAAA;AAAA;;;;;;;;;UAWY,uBAAA;EACf,UAAA,EAAY,OAAA;EACZ,UAAA,EAAY,cAAA;AAAA;;;;;;;;;;;iBAaE,gBAAA,CACd,QAAA,EAAU,kBAAA,cACV,QAAA;EACE,KAAA;EACA,IAAA;EACA,SAAA;EACA,EAAA,EAAI,WAAA;EACJ,GAAA;EACA,KAAA;EACA,SAAA;AAAA,IAED,kBAAA"}
|