@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,1315 @@
|
|
|
1
|
+
import { computeCID } from "./cid.mjs";
|
|
2
|
+
import { DOWNLOAD_MAX_ENTRIES } from "./download-actions.mjs";
|
|
3
|
+
import { detectMimeType } from "./mime-utils.mjs";
|
|
4
|
+
import { DEFAULT_INLINE_THRESHOLD, decodeInlineText } from "./inline-content.mjs";
|
|
5
|
+
import { BatchOverBudgetError, BatchUnsupportedError, DownloadForbiddenError } from "./remote-afs.mjs";
|
|
6
|
+
import { AFSCompactedError, AFSConflictError, makeNsLog } from "@aigne/afs";
|
|
7
|
+
import { firstNonEmpty } from "@aigne/afs/utils/env";
|
|
8
|
+
|
|
9
|
+
//#region src/sync-engine.ts
|
|
10
|
+
const log = makeNsLog("provider:did-space");
|
|
11
|
+
/** Client-side batch packing constants (wire-contract §9 CF tier — packing
|
|
12
|
+
* to the CF table is slightly conservative against Node but always valid;
|
|
13
|
+
* the client never needs to know the server runtime, design §7.3). */
|
|
14
|
+
const BATCH_PUT_MAX_ENTRIES = 20;
|
|
15
|
+
const BATCH_DELETE_MAX_ENTRIES = 40;
|
|
16
|
+
const BATCH_PUT_MAX_BYTES = 1024 * 1024;
|
|
17
|
+
const BATCH_READ_MAX_ENTRIES = 20;
|
|
18
|
+
const BATCH_READ_MAX_BYTES = 2 * 1024 * 1024;
|
|
19
|
+
/**
|
|
20
|
+
* Push batch size. Each round, push() fetches up to this many unpushed
|
|
21
|
+
* changelog rows in a single SQLite query and uploads them one by one.
|
|
22
|
+
* Lower than PULL_BATCH_LIMIT because each row is a separate PUT/DELETE
|
|
23
|
+
* round-trip (vs. PULL which fetches many change rows in a single call).
|
|
24
|
+
*/
|
|
25
|
+
const PUSH_BATCH_LIMIT = 100;
|
|
26
|
+
/**
|
|
27
|
+
* Client-side upload-plane ACTIVATION threshold (design §6.1 / §15 decision 4).
|
|
28
|
+
* The upload plane (prepare → out-of-band PUT → commit) is taken ONLY for large
|
|
29
|
+
* backlogs — the Aside pain point of a ~2000-row small-record first full sync.
|
|
30
|
+
* Small/common writes stay on the proven `batchWrite` path. A push uses the
|
|
31
|
+
* upload plane only when its put backlog is ≥ MIN_ENTRIES (peeked) AND the
|
|
32
|
+
* server advertises grant capability.
|
|
33
|
+
*
|
|
34
|
+
* Value 200 is deliberate, not a placeholder: lowering it does nothing for the
|
|
35
|
+
* 2000 case (2000 ≥ 200 already activates) and would only pull the 100–200 band
|
|
36
|
+
* onto the upload plane — MORE round-trips than batchWrite's ceil(N/20) POSTs.
|
|
37
|
+
* Only lower with a measured 100–200 wall-clock crossover.
|
|
38
|
+
*/
|
|
39
|
+
const AFS_DID_SPACE_UPLOAD_MIN_ENTRIES = 200;
|
|
40
|
+
const AFS_DID_SPACE_UPLOAD_MIN_BYTES = 8 * 1024 * 1024;
|
|
41
|
+
/**
|
|
42
|
+
* Browser upload-plane SINGLE-FILE size threshold (upload-browser §4.5 / T4.2).
|
|
43
|
+
* Distinct from the native-sync count/bytes batch gates above: the browser
|
|
44
|
+
* orchestrator triages by individual file size — a file LARGER than this goes
|
|
45
|
+
* the grant plane (bytes direct to R2, out of the worker); smaller files fall to
|
|
46
|
+
* the legacy WS batchWrite. The browser orchestrator mirrors this value in its
|
|
47
|
+
* inlined page JS (it cannot import this const). Conservative default — only
|
|
48
|
+
* clearly-large files grant; measurement-gated, calibrated in P6.
|
|
49
|
+
*/
|
|
50
|
+
const AFS_DID_SPACE_UPLOAD_MIN_SIZE = 4 * 1024 * 1024;
|
|
51
|
+
/**
|
|
52
|
+
* Upload-plane put-CHUNK caps (the load-bearing fix for Aside 2000 small writes).
|
|
53
|
+
* Once the upload plane is active, each prepare/commit round packs up to this
|
|
54
|
+
* many entries — DECOUPLED from `BATCH_PUT_MAX_ENTRIES` (20). Reusing the batch
|
|
55
|
+
* cap would turn a 2000-entry backlog into ~100 prepare/commit rounds; at 100 it
|
|
56
|
+
* is ~20. The effective chunk is `min(CHUNK_MAX_ENTRIES, PUSH_BATCH_LIMIT)`, and
|
|
57
|
+
* `PUSH_BATCH_LIMIT` is also 100, so a full window is one chunk → 20 rounds for
|
|
58
|
+
* 2000. The Cloudflare commit-sim probe (100 entries ≈ 425ms wall) justifies 100
|
|
59
|
+
* as a safe client default; raising it past PUSH_BATCH_LIMIT is inert.
|
|
60
|
+
*/
|
|
61
|
+
const AFS_DID_SPACE_UPLOAD_CHUNK_MAX_ENTRIES = 100;
|
|
62
|
+
const AFS_DID_SPACE_UPLOAD_CHUNK_MAX_BYTES = 8 * 1024 * 1024;
|
|
63
|
+
/**
|
|
64
|
+
* Out-of-band PUT fan-out for the upload plane (independent tuning, design §3).
|
|
65
|
+
* Each presigned PUT leaves the worker, so the client can run several in flight;
|
|
66
|
+
* 8 is the deterministic default. Adaptive 8→4 backoff under R2 5xx/429 is a
|
|
67
|
+
* follow-up, intentionally NOT in the first patch.
|
|
68
|
+
*/
|
|
69
|
+
const AFS_DID_SPACE_UPLOAD_PUT_CONCURRENCY = 8;
|
|
70
|
+
/**
|
|
71
|
+
* Flush local_seq to SQLite every N successful pushes. Trade-off: lower
|
|
72
|
+
* = less progress lost on crash, higher = fewer DB writes. 10 is the
|
|
73
|
+
* round-trip frequency Task 3.6 spec mandates.
|
|
74
|
+
*/
|
|
75
|
+
const PUSH_PROGRESS_FLUSH_INTERVAL = 10;
|
|
76
|
+
/**
|
|
77
|
+
* Pull batch size. Each `.changelog` cursor list call fetches up to this
|
|
78
|
+
* many rows. Matches the server's MAX_LIMIT hard cap
|
|
79
|
+
* (`services/arc-computer/src/handlers/space-api.ts` — .changelog list
|
|
80
|
+
* branch inside `_createSpaceHandler`).
|
|
81
|
+
*/
|
|
82
|
+
const PULL_BATCH_LIMIT = 1e3;
|
|
83
|
+
/**
|
|
84
|
+
* Concurrency cap for `.objects/{cid}` reads inside pull(). Higher = faster
|
|
85
|
+
* under large pulls, but risks server-side rate limits and memory
|
|
86
|
+
* pressure on the client. 4 is the spec default.
|
|
87
|
+
*/
|
|
88
|
+
const PULL_FETCH_CONCURRENCY = 4;
|
|
89
|
+
/**
|
|
90
|
+
* Download-plane size gate (design §6.3 / T3.5) — CALIBRATED by P4/T4.4 staging
|
|
91
|
+
* measurement (2026-06-21, aside.afsd.io, logs/staging-download-plane-*.log).
|
|
92
|
+
* An object whose size is ≥ this routes to the download plane (prepare-download
|
|
93
|
+
* → direct R2 GET → computeCID → internalWrite), keeping its bytes out of the
|
|
94
|
+
* worker; smaller objects / text / inline-cache hits stay on `batchRead` (D1
|
|
95
|
+
* inline serves small text with ZERO R2 GETs — faster there). A null/missing
|
|
96
|
+
* size is treated as small (never route into the plane on missing size).
|
|
97
|
+
*
|
|
98
|
+
* MEASURED breaking point: a single in-worker `.objects/{cid}` read (arrayBuffer
|
|
99
|
+
* + base64-over-JSON) is fine at 5 MiB (~930ms) but HARD-FAILS with HTTP 503
|
|
100
|
+
* (Worker exceeded resource limits) at 8 / 12 / 18 MiB — the download plane
|
|
101
|
+
* served all of those correctly (8 MiB ~0.8s warm). So the worker simply CANNOT
|
|
102
|
+
* read a single object ≳8 MiB; 4 MiB sits safely below that failure zone (and
|
|
103
|
+
* covers the reported 5.26 MiB pain case) while keeping the common small/medium
|
|
104
|
+
* objects on the locally-fast worker path.
|
|
105
|
+
*/
|
|
106
|
+
const DOWNLOAD_PLANE_MIN_BYTES = 4 * 1024 * 1024;
|
|
107
|
+
/**
|
|
108
|
+
* Out-of-band GET fan-out for the download plane (design §6.3). The client reads
|
|
109
|
+
* directly from R2 and is NOT bound by the Workers 6-simultaneous-connection cap,
|
|
110
|
+
* so the default is higher than the in-worker batchRead. Default 8; TS/Node may
|
|
111
|
+
* override via `AFS_DID_SPACE_DOWNLOAD_GET_CONCURRENCY` (a single-point int parse
|
|
112
|
+
* — NOT parseBool; the `AFS_DID_SPACE_DOWNLOAD_*` family, mirror of the shipped
|
|
113
|
+
* `AFS_DID_SPACE_UPLOAD_*`). Swift/Kotlin clients use their own platform constant.
|
|
114
|
+
*/
|
|
115
|
+
const DOWNLOAD_GET_CONCURRENCY_DEFAULT = 8;
|
|
116
|
+
function resolveDownloadGetConcurrency() {
|
|
117
|
+
const raw = typeof process !== "undefined" && process.env ? firstNonEmpty(process.env.AFS_DID_SPACE_DOWNLOAD_GET_CONCURRENCY) : void 0;
|
|
118
|
+
const n = raw !== void 0 ? Number.parseInt(raw, 10) : NaN;
|
|
119
|
+
return Number.isInteger(n) && n > 0 ? n : DOWNLOAD_GET_CONCURRENCY_DEFAULT;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Wall-clock budget for a single sync() call. Protects against runaway
|
|
123
|
+
* pulls on wildly out-of-date clients. pull() checks Date.now() against
|
|
124
|
+
* the deadline at the top of every batch loop iteration.
|
|
125
|
+
*/
|
|
126
|
+
const SYNC_WALL_CLOCK_BUDGET_MS = 25e3;
|
|
127
|
+
/**
|
|
128
|
+
* Thrown by {@link SyncEngine.resolveConflict} when the server's 409
|
|
129
|
+
* conflict response is missing required fields (notably `currentMtime`).
|
|
130
|
+
* The LWW decision needs both timestamps, so a malformed response is
|
|
131
|
+
* treated as fatal to the in-flight push leg.
|
|
132
|
+
*
|
|
133
|
+
* Phase 5 Task 5.1 (G11): replaced the Task 3.6 `ConflictTODOError`
|
|
134
|
+
* stub with the real LWW resolver. The resolver handles every well-formed
|
|
135
|
+
* conflict shape inline; this error class only fires on protocol breakage.
|
|
136
|
+
*/
|
|
137
|
+
var ConflictResolutionError = class extends Error {
|
|
138
|
+
change;
|
|
139
|
+
reason;
|
|
140
|
+
constructor(change, reason) {
|
|
141
|
+
super(`resolveConflict on ${change.path}: ${reason}`);
|
|
142
|
+
this.name = "ConflictResolutionError";
|
|
143
|
+
this.change = change;
|
|
144
|
+
this.reason = reason;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Thrown by {@link SyncEngine.fullResync} when the cloud-side manifest
|
|
149
|
+
* snapshot exceeds the size budget the server is willing to materialise
|
|
150
|
+
* in one shot. The server signals this via `result.meta.truncated === true`
|
|
151
|
+
* on the `cloudAfs.list("", { maxDepth: -1 })` response.
|
|
152
|
+
*
|
|
153
|
+
* Recovery: the caller (typically a higher-level scheduler) is expected
|
|
154
|
+
* to either narrow the scope (sub-tree manifest, future feature) or
|
|
155
|
+
* back off and retry once the cloud has compacted the manifest. There
|
|
156
|
+
* is no automatic recovery inside SyncEngine — applying a partial
|
|
157
|
+
* manifest would silently lose data.
|
|
158
|
+
*/
|
|
159
|
+
var ManifestTooLargeError = class extends Error {
|
|
160
|
+
constructor(message = "fullResync: cloud manifest is too large to materialise in one shot") {
|
|
161
|
+
super(message);
|
|
162
|
+
this.name = "ManifestTooLargeError";
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Thrown when an object read returns 404 for a cid that is the **current**
|
|
167
|
+
* state of a path — design §4.7.2, the GC-go-live safety contract. Enabling
|
|
168
|
+
* object GC makes a current-cid 404 possible (a GC bug or the residual
|
|
169
|
+
* re-reference race); it MUST be a loud, hard error — NEVER silently treated as
|
|
170
|
+
* convergence. The cursor must stay pinned (do not advance past the failing
|
|
171
|
+
* change) so the next sync revisits it once the object is restored/re-pushed.
|
|
172
|
+
*
|
|
173
|
+
* Distinct from a **superseded**-cid 404 (an old changelog row a later same-path
|
|
174
|
+
* row supersedes), which is benign — skip + advance (the Path-Y offline
|
|
175
|
+
* tradeoff, §0.2). Classification: see {@link classifyMissingObject}.
|
|
176
|
+
*/
|
|
177
|
+
var CurrentObjectMissingError = class extends Error {
|
|
178
|
+
path;
|
|
179
|
+
cid;
|
|
180
|
+
source;
|
|
181
|
+
constructor(path, cid, source) {
|
|
182
|
+
super(`SyncEngine: current object missing (${source}) for path ${path} cid ${cid} — object GC may have reclaimed a live cid; cursor left pinned for retry`);
|
|
183
|
+
this.name = "CurrentObjectMissingError";
|
|
184
|
+
this.path = path;
|
|
185
|
+
this.cid = cid;
|
|
186
|
+
this.source = source;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Classify an object-read 404 during incremental apply (design §4.7.2).
|
|
191
|
+
*
|
|
192
|
+
* `lastCidForPath` maps each path in the batch to the cid of its LAST row
|
|
193
|
+
* (max seq) — `null` if that last row is a delete. Fail-closed:
|
|
194
|
+
* - cid === the path's last-row cid ⇒ "current" (this cid is the path's
|
|
195
|
+
* final state in the batch) ⇒ hard error.
|
|
196
|
+
* - path absent from the map (unknown cid, not in this batch) ⇒ "current"
|
|
197
|
+
* (fail closed — never benign-skip something we can't prove superseded).
|
|
198
|
+
* - otherwise a later same-path row supersedes this cid ⇒ "superseded" ⇒ skip.
|
|
199
|
+
*/
|
|
200
|
+
function classifyMissingObject(path, cid, lastCidForPath) {
|
|
201
|
+
if (!lastCidForPath.has(path)) return "current";
|
|
202
|
+
return lastCidForPath.get(path) === cid ? "current" : "superseded";
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Simple promise-based mutex used to serialise concurrent sync() calls.
|
|
206
|
+
* Callers `await acquire()` to claim the lock and MUST `release()` in a
|
|
207
|
+
* finally block. FIFO ordering is not guaranteed — the spec only
|
|
208
|
+
* requires mutual exclusion.
|
|
209
|
+
*/
|
|
210
|
+
var AsyncMutex = class {
|
|
211
|
+
chain = Promise.resolve();
|
|
212
|
+
async acquire() {
|
|
213
|
+
let release;
|
|
214
|
+
const next = new Promise((resolve) => {
|
|
215
|
+
release = resolve;
|
|
216
|
+
});
|
|
217
|
+
const prev = this.chain;
|
|
218
|
+
this.chain = this.chain.then(() => next);
|
|
219
|
+
await prev;
|
|
220
|
+
return release;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Local push + pull + GC orchestrator. One instance per DIDSpaceProvider /
|
|
225
|
+
* per userDid. Instances are NOT thread-safe at the data level — the
|
|
226
|
+
* internal mutex only serialises `sync()` calls; callers that mutate
|
|
227
|
+
* local state while a sync is in flight are expected to do so via
|
|
228
|
+
* `provider.internalWrite/Delete` so the changelog row is visible to the
|
|
229
|
+
* next push batch.
|
|
230
|
+
*/
|
|
231
|
+
var SyncEngine = class {
|
|
232
|
+
localChangelog;
|
|
233
|
+
syncStateStore;
|
|
234
|
+
cloudAfs;
|
|
235
|
+
provider;
|
|
236
|
+
selfDeviceId;
|
|
237
|
+
wallClockBudgetMs;
|
|
238
|
+
mutex = new AsyncMutex();
|
|
239
|
+
/**
|
|
240
|
+
* Per-INSTANCE capability-fallback memory (design §7.1): set the first
|
|
241
|
+
* time the server answers a batch envelope with the exact
|
|
242
|
+
* "Unsupported op 'batch…'" 400. Never set by any other 400 (e.g.
|
|
243
|
+
* BATCH_OVER_BUDGET halves and retries instead).
|
|
244
|
+
*/
|
|
245
|
+
batchFallback = false;
|
|
246
|
+
/**
|
|
247
|
+
* Per-INSTANCE upload-plane capability memory (design §7.1, mirrors
|
|
248
|
+
* batchFallback). null = not yet probed; set on the first `push()` that
|
|
249
|
+
* reaches the threshold. `false` (Node / old server / signer-less) keeps the
|
|
250
|
+
* engine on batchWrite forever for this instance.
|
|
251
|
+
*/
|
|
252
|
+
uploadPlaneCapable = null;
|
|
253
|
+
/** Set per-push() when this push routes large put-chunks through the upload plane. */
|
|
254
|
+
uploadPlaneActive = false;
|
|
255
|
+
/**
|
|
256
|
+
* Per-INSTANCE download-plane capability memory (design §7.3, mirrors
|
|
257
|
+
* uploadPlaneCapable). null = not yet probed. `false` (Node / old server /
|
|
258
|
+
* signer-less) keeps the engine on batchRead forever for this instance — it is
|
|
259
|
+
* NOT reset per session (unlike {@link downloadFallback}): a server that never
|
|
260
|
+
* advertised the capability won't start mid-process, so re-probing every sync
|
|
261
|
+
* would just waste a `/.actions` list. A server UPGRADE needs a client restart.
|
|
262
|
+
*/
|
|
263
|
+
downloadPlaneCapable = null;
|
|
264
|
+
/**
|
|
265
|
+
* Runtime fallback-on-403 latch (design §7.3): set when a presigned GET is
|
|
266
|
+
* rejected 401/403 (token lacks GetObject / bad signature). Once set, the whole
|
|
267
|
+
* sync SESSION reverts to batchRead and never re-tries the plane — so a misconfig
|
|
268
|
+
* costs at most ONE 403 per object before the first hit flips it, then nothing.
|
|
269
|
+
* Reset at the start of each sync() so a fixed token recovers without restart.
|
|
270
|
+
*/
|
|
271
|
+
downloadFallback = false;
|
|
272
|
+
/** Resolved once per instance — the out-of-band GET fan-out (env-overridable). */
|
|
273
|
+
downloadGetConcurrency = resolveDownloadGetConcurrency();
|
|
274
|
+
constructor(opts) {
|
|
275
|
+
if (!opts.deviceId) throw new Error("SyncEngine: deviceId is required");
|
|
276
|
+
this.localChangelog = opts.localChangelog;
|
|
277
|
+
this.syncStateStore = opts.syncStateStore;
|
|
278
|
+
this.cloudAfs = opts.cloudAfs;
|
|
279
|
+
this.provider = opts.provider;
|
|
280
|
+
this.selfDeviceId = opts.deviceId;
|
|
281
|
+
this.wallClockBudgetMs = opts.wallClockBudgetMs ?? SYNC_WALL_CLOCK_BUDGET_MS;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Run a single sync round — push → pull → gc. Concurrent sync() calls
|
|
285
|
+
* on the same instance are serialised by the internal mutex. Always
|
|
286
|
+
* returns a {@link SyncResult}; throws only on unhandled errors from
|
|
287
|
+
* the underlying store/network layers.
|
|
288
|
+
*/
|
|
289
|
+
async sync(opts = {}) {
|
|
290
|
+
const release = await this.mutex.acquire();
|
|
291
|
+
const deadline = Date.now() + this.wallClockBudgetMs;
|
|
292
|
+
this.downloadFallback = false;
|
|
293
|
+
const result = {
|
|
294
|
+
ok: false,
|
|
295
|
+
aborted: false,
|
|
296
|
+
pushed: 0,
|
|
297
|
+
pulled: 0,
|
|
298
|
+
gcDeleted: 0,
|
|
299
|
+
fullResynced: false,
|
|
300
|
+
endedAt: 0
|
|
301
|
+
};
|
|
302
|
+
try {
|
|
303
|
+
if (opts.signal?.aborted) {
|
|
304
|
+
result.aborted = true;
|
|
305
|
+
result.endedAt = Date.now();
|
|
306
|
+
await this.syncStateStore.save({ syncStatus: { kind: "idle" } });
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
await this.syncStateStore.save({ syncStatus: { kind: "syncing" } });
|
|
310
|
+
try {
|
|
311
|
+
if ((await this.syncStateStore.load()).lastSyncAt === 0) {
|
|
312
|
+
result.pulled = (await this.fullResync({
|
|
313
|
+
signal: opts.signal,
|
|
314
|
+
deadline
|
|
315
|
+
})).pulled;
|
|
316
|
+
result.fullResynced = true;
|
|
317
|
+
} else {
|
|
318
|
+
result.pushed = (await this.push({
|
|
319
|
+
signal: opts.signal,
|
|
320
|
+
deadline
|
|
321
|
+
})).pushed;
|
|
322
|
+
const pullResult = await this.pull({
|
|
323
|
+
signal: opts.signal,
|
|
324
|
+
deadline
|
|
325
|
+
});
|
|
326
|
+
result.pulled = pullResult.pulled;
|
|
327
|
+
if (pullResult.fullResynced) result.fullResynced = true;
|
|
328
|
+
}
|
|
329
|
+
result.gcDeleted = (await this.localChangelog.gc()).deleted;
|
|
330
|
+
await this.syncStateStore.save({
|
|
331
|
+
syncStatus: { kind: "active" },
|
|
332
|
+
lastSyncAt: Date.now()
|
|
333
|
+
});
|
|
334
|
+
result.ok = true;
|
|
335
|
+
} catch (e) {
|
|
336
|
+
if (opts.signal?.aborted) {
|
|
337
|
+
result.aborted = true;
|
|
338
|
+
await this.syncStateStore.save({ syncStatus: { kind: "idle" } });
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
342
|
+
await this.syncStateStore.save({ syncStatus: {
|
|
343
|
+
kind: "error",
|
|
344
|
+
message
|
|
345
|
+
} });
|
|
346
|
+
throw e;
|
|
347
|
+
}
|
|
348
|
+
} finally {
|
|
349
|
+
result.endedAt = Date.now();
|
|
350
|
+
release();
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Upload unpushed local changes to the cloud, advancing local_seq in
|
|
356
|
+
* SQLite every `PUSH_PROGRESS_FLUSH_INTERVAL` rows. On any error
|
|
357
|
+
* mid-batch, flush the already-advanced cursor before re-throwing so
|
|
358
|
+
* the next sync() resumes where this one left off.
|
|
359
|
+
*
|
|
360
|
+
* Per-row semantics:
|
|
361
|
+
* - `put` row → `cloudAfs.write(path, content, {...})`
|
|
362
|
+
* - `delete` row → `cloudAfs.delete(path, {...})`
|
|
363
|
+
* - `put` row whose file has been deleted locally since the row was
|
|
364
|
+
* written → skip (cursor advances; the following delete row will
|
|
365
|
+
* tell the cloud what happened).
|
|
366
|
+
* - 409 conflict → delegate to {@link SyncEngine.resolveConflict}
|
|
367
|
+
* (LWW + tiebreak — see G11 in native-sync-design.md).
|
|
368
|
+
*/
|
|
369
|
+
async push(opts) {
|
|
370
|
+
let cursor = (await this.syncStateStore.load()).localSeq;
|
|
371
|
+
let pushed = 0;
|
|
372
|
+
let processedSinceFlush = 0;
|
|
373
|
+
this.uploadPlaneActive = await this.shouldUseUploadPlane(cursor);
|
|
374
|
+
const batchCapable = () => !this.batchFallback && typeof this.cloudAfs.batchWrite === "function" && typeof this.cloudAfs.batchDelete === "function";
|
|
375
|
+
try {
|
|
376
|
+
while (true) {
|
|
377
|
+
if (opts.signal?.aborted) return { pushed };
|
|
378
|
+
if (Date.now() >= opts.deadline) return { pushed };
|
|
379
|
+
const batch = await this.localChangelog.getUnpushed(cursor, PUSH_BATCH_LIMIT);
|
|
380
|
+
if (batch.length === 0) break;
|
|
381
|
+
if (batchCapable()) {
|
|
382
|
+
const r = await this.pushBatchLeg(batch, cursor, opts);
|
|
383
|
+
cursor = r.cursor;
|
|
384
|
+
pushed += r.pushed;
|
|
385
|
+
await this.syncStateStore.save({ localSeq: cursor });
|
|
386
|
+
if (r.stop) return { pushed };
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
for (const change of batch) {
|
|
390
|
+
if (opts.signal?.aborted) return { pushed };
|
|
391
|
+
if (Date.now() >= opts.deadline) return { pushed };
|
|
392
|
+
await this.pushOne(change);
|
|
393
|
+
cursor = change.seq;
|
|
394
|
+
pushed += 1;
|
|
395
|
+
processedSinceFlush += 1;
|
|
396
|
+
if (processedSinceFlush >= PUSH_PROGRESS_FLUSH_INTERVAL) {
|
|
397
|
+
await this.syncStateStore.save({ localSeq: cursor });
|
|
398
|
+
processedSinceFlush = 0;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} finally {
|
|
403
|
+
if (cursor > 0) await this.syncStateStore.save({ localSeq: cursor });
|
|
404
|
+
}
|
|
405
|
+
return { pushed };
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Batch push leg (P4, design §7.2): split the changelog window into
|
|
409
|
+
* MAXIMAL same-op runs in seq order (op type never mixes inside a run —
|
|
410
|
+
* put(a)→delete(a) is two runs, so same-path cross-op order rides the
|
|
411
|
+
* sequential awaits); pack each run by maxEntries + Σ-decoded-bytes +
|
|
412
|
+
* UNIQUE-path-per-chunk (a duplicate path cuts the chunk so the server's
|
|
413
|
+
* one-shot preread stays serial-equivalent — and the SECOND chunk's
|
|
414
|
+
* ifMatch sees the first chunk's result); consume results strictly in
|
|
415
|
+
* order. Returns the advanced cursor; `stop` = abort/deadline/tail-skip.
|
|
416
|
+
*/
|
|
417
|
+
async pushBatchLeg(batch, cursor, opts) {
|
|
418
|
+
let pushed = 0;
|
|
419
|
+
const runs = [];
|
|
420
|
+
for (const change of batch) {
|
|
421
|
+
const last = runs[runs.length - 1];
|
|
422
|
+
if (last && last[0].op === change.op) last.push(change);
|
|
423
|
+
else runs.push([change]);
|
|
424
|
+
}
|
|
425
|
+
for (const run of runs) {
|
|
426
|
+
let index = 0;
|
|
427
|
+
while (index < run.length) {
|
|
428
|
+
if (opts.signal?.aborted || Date.now() >= opts.deadline) return {
|
|
429
|
+
cursor,
|
|
430
|
+
pushed,
|
|
431
|
+
stop: true
|
|
432
|
+
};
|
|
433
|
+
const putUploadPlane = run[0].op === "put" && this.uploadPlaneActive;
|
|
434
|
+
const maxEntries = run[0].op !== "put" ? BATCH_DELETE_MAX_ENTRIES : putUploadPlane ? AFS_DID_SPACE_UPLOAD_CHUNK_MAX_ENTRIES : BATCH_PUT_MAX_ENTRIES;
|
|
435
|
+
const maxBytes = putUploadPlane ? AFS_DID_SPACE_UPLOAD_CHUNK_MAX_BYTES : BATCH_PUT_MAX_BYTES;
|
|
436
|
+
const chunk = [];
|
|
437
|
+
const seen = /* @__PURE__ */ new Set();
|
|
438
|
+
let chunkBytes = 0;
|
|
439
|
+
while (index < run.length && chunk.length < maxEntries) {
|
|
440
|
+
const change = run[index];
|
|
441
|
+
if (seen.has(change.path)) break;
|
|
442
|
+
if (change.op === "put") {
|
|
443
|
+
const bytes = await this.provider.readBytes(change.path);
|
|
444
|
+
if (bytes === null) {
|
|
445
|
+
cursor = change.seq;
|
|
446
|
+
index++;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (chunk.length > 0 && chunkBytes + bytes.length > maxBytes) break;
|
|
450
|
+
chunkBytes += bytes.length;
|
|
451
|
+
chunk.push({
|
|
452
|
+
change,
|
|
453
|
+
bytes
|
|
454
|
+
});
|
|
455
|
+
} else chunk.push({
|
|
456
|
+
change,
|
|
457
|
+
bytes: null
|
|
458
|
+
});
|
|
459
|
+
seen.add(change.path);
|
|
460
|
+
index++;
|
|
461
|
+
}
|
|
462
|
+
if (chunk.length === 0) continue;
|
|
463
|
+
const outcome = await this.sendPushChunk(chunk);
|
|
464
|
+
if (outcome.fallback) {
|
|
465
|
+
this.batchFallback = true;
|
|
466
|
+
return {
|
|
467
|
+
cursor,
|
|
468
|
+
pushed,
|
|
469
|
+
stop: false
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
for (const consumed of outcome.consumed) {
|
|
473
|
+
cursor = consumed.seq;
|
|
474
|
+
if (consumed.counted) pushed += 1;
|
|
475
|
+
}
|
|
476
|
+
if (outcome.consumed.length > 0 || outcome.error) await this.syncStateStore.save({ localSeq: cursor });
|
|
477
|
+
if (outcome.error) throw outcome.error;
|
|
478
|
+
if (outcome.tailSkipped) return {
|
|
479
|
+
cursor,
|
|
480
|
+
pushed,
|
|
481
|
+
stop: false
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
cursor,
|
|
487
|
+
pushed,
|
|
488
|
+
stop: false
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Send one packed chunk, halving on envelope over-budget (down to 1 entry
|
|
493
|
+
* = pushOne, the literal old behavior — bounded recursion by construction:
|
|
494
|
+
* each level halves a strictly smaller chunk).
|
|
495
|
+
*/
|
|
496
|
+
async sendPushChunk(chunk) {
|
|
497
|
+
const op = chunk[0].change.op;
|
|
498
|
+
let result;
|
|
499
|
+
try {
|
|
500
|
+
if (op === "put") result = this.uploadPlaneActive ? await this.uploadPlaneWriteChunk(chunk) : await this.cloudAfs.batchWrite(await Promise.all(chunk.map(async ({ change, bytes }) => {
|
|
501
|
+
const appMeta = await this.readAppMeta(change.path, bytes);
|
|
502
|
+
return {
|
|
503
|
+
path: normaliseWirePath(change.path),
|
|
504
|
+
content: appMeta ? {
|
|
505
|
+
content: bytes,
|
|
506
|
+
meta: appMeta
|
|
507
|
+
} : { content: bytes },
|
|
508
|
+
options: {
|
|
509
|
+
...change.old_cid ? { ifMatch: change.old_cid } : {},
|
|
510
|
+
meta: {
|
|
511
|
+
deviceId: change.device_id,
|
|
512
|
+
mtime: change.mtime
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
})));
|
|
517
|
+
else result = await this.cloudAfs.batchDelete(chunk.map(({ change }) => ({
|
|
518
|
+
path: normaliseWirePath(change.path),
|
|
519
|
+
options: { meta: {
|
|
520
|
+
deviceId: change.device_id,
|
|
521
|
+
mtime: change.mtime,
|
|
522
|
+
...change.old_cid ? { ifMatch: change.old_cid } : {}
|
|
523
|
+
} }
|
|
524
|
+
})));
|
|
525
|
+
} catch (e) {
|
|
526
|
+
if (e instanceof BatchUnsupportedError) return {
|
|
527
|
+
consumed: [],
|
|
528
|
+
tailSkipped: false,
|
|
529
|
+
fallback: true
|
|
530
|
+
};
|
|
531
|
+
if (e instanceof BatchOverBudgetError) {
|
|
532
|
+
if (chunk.length === 1) {
|
|
533
|
+
await this.pushOne(chunk[0].change);
|
|
534
|
+
return {
|
|
535
|
+
consumed: [{
|
|
536
|
+
seq: chunk[0].change.seq,
|
|
537
|
+
counted: true
|
|
538
|
+
}],
|
|
539
|
+
tailSkipped: false,
|
|
540
|
+
fallback: false
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
const mid = Math.ceil(chunk.length / 2);
|
|
544
|
+
const left = await this.sendPushChunk(chunk.slice(0, mid));
|
|
545
|
+
if (left.fallback || left.tailSkipped || left.error) return left;
|
|
546
|
+
const right = await this.sendPushChunk(chunk.slice(mid));
|
|
547
|
+
return {
|
|
548
|
+
consumed: [...left.consumed, ...right.consumed],
|
|
549
|
+
tailSkipped: right.tailSkipped,
|
|
550
|
+
fallback: right.fallback,
|
|
551
|
+
error: right.error
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
throw e;
|
|
555
|
+
}
|
|
556
|
+
if (!Array.isArray(result.results) || result.results.length !== chunk.length) throw new Error(`SyncEngine.push: batch results length ${result.results?.length ?? "?"} != chunk length ${chunk.length} (protocol violation)`);
|
|
557
|
+
const consumed = [];
|
|
558
|
+
for (let i = 0; i < result.results.length; i++) {
|
|
559
|
+
const r = result.results[i];
|
|
560
|
+
const change = chunk[i].change;
|
|
561
|
+
if (r.success) {
|
|
562
|
+
consumed.push({
|
|
563
|
+
seq: change.seq,
|
|
564
|
+
counted: true
|
|
565
|
+
});
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (r.code === "BATCH_TAIL_SKIPPED") return {
|
|
569
|
+
consumed,
|
|
570
|
+
tailSkipped: true,
|
|
571
|
+
fallback: false
|
|
572
|
+
};
|
|
573
|
+
if (r.conflict) {
|
|
574
|
+
await this.resolveConflict(change, r.conflict);
|
|
575
|
+
consumed.push({
|
|
576
|
+
seq: change.seq,
|
|
577
|
+
counted: true
|
|
578
|
+
});
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
consumed,
|
|
583
|
+
tailSkipped: false,
|
|
584
|
+
fallback: false,
|
|
585
|
+
error: /* @__PURE__ */ new Error(`SyncEngine.push: batch entry failed at seq=${change.seq} path=${change.path}: ${r.error ?? r.code ?? r.status}`)
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
consumed,
|
|
590
|
+
tailSkipped: false,
|
|
591
|
+
fallback: false
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Decide whether THIS push routes large put-chunks through the upload plane
|
|
596
|
+
* (design §6.1 / decision 4). True only when (a) the adapter has the
|
|
597
|
+
* prepare/commit/uploadObject surface, (b) the server advertises grant
|
|
598
|
+
* capability (memoized per instance — Node/old-server/signer-less → false →
|
|
599
|
+
* never), and (c) the put backlog meets the conservative threshold
|
|
600
|
+
* (≥ MIN_ENTRIES puts, or ≥ MIN_BYTES) via a cheap bounded peek.
|
|
601
|
+
*/
|
|
602
|
+
async shouldUseUploadPlane(cursor) {
|
|
603
|
+
if (typeof this.cloudAfs.prepareUpload !== "function" || typeof this.cloudAfs.commitUpload !== "function" || typeof this.cloudAfs.uploadObject !== "function") return false;
|
|
604
|
+
if (this.uploadPlaneCapable === null) try {
|
|
605
|
+
this.uploadPlaneCapable = typeof this.cloudAfs.uploadGrantCapability === "function" ? await this.cloudAfs.uploadGrantCapability() : false;
|
|
606
|
+
} catch {
|
|
607
|
+
this.uploadPlaneCapable = false;
|
|
608
|
+
}
|
|
609
|
+
if (!this.uploadPlaneCapable) return false;
|
|
610
|
+
const puts = (await this.localChangelog.getUnpushed(cursor, AFS_DID_SPACE_UPLOAD_MIN_ENTRIES + 1)).filter((c) => c.op === "put");
|
|
611
|
+
if (puts.length >= AFS_DID_SPACE_UPLOAD_MIN_ENTRIES) return true;
|
|
612
|
+
return puts.reduce((sum, c) => sum + (c.size ?? 0), 0) >= AFS_DID_SPACE_UPLOAD_MIN_BYTES;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Upload-plane transport for one put chunk (design §6.1 / §8). Mirrors a
|
|
616
|
+
* `batchWrite` call but moves content OUT of the worker:
|
|
617
|
+
* 1. prepare-upload (reuses each row's local `cid` — never re-hashed);
|
|
618
|
+
* 2. out-of-band PUT each grant (bounded concurrency);
|
|
619
|
+
* 3. commit-upload ONLY the entries whose PUT returned 200 (commit-after-
|
|
620
|
+
* 200, decision 2 — the sole dangling defense). PUT-failed entries get a
|
|
621
|
+
* per-entry failure so the cursor pins there for retry next round.
|
|
622
|
+
* Returns the SAME `RpcBatchResult` shape `sendPushChunk` consumes. If a grant
|
|
623
|
+
* is unexpectedly absent (capability was advertised but a row got none), bail
|
|
624
|
+
* via `BatchUnsupportedError` → the engine reverts to the legacy path.
|
|
625
|
+
*/
|
|
626
|
+
/**
|
|
627
|
+
* Application metadata for a push entry, derived from its own bytes via the
|
|
628
|
+
* optional provider hook (did-space-metadata G3). Returns undefined when the
|
|
629
|
+
* provider exposes none — so the engine emits no metadata and the wire stays
|
|
630
|
+
* pre-G3 byte-identical.
|
|
631
|
+
*/
|
|
632
|
+
async readAppMeta(path, bytes) {
|
|
633
|
+
const read = this.provider.readApplicationMetadata;
|
|
634
|
+
if (!read) return void 0;
|
|
635
|
+
try {
|
|
636
|
+
const meta = await read.call(this.provider, path, bytes);
|
|
637
|
+
return meta && Object.keys(meta).length > 0 ? meta : void 0;
|
|
638
|
+
} catch {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async uploadPlaneWriteChunk(chunk) {
|
|
643
|
+
const cids = await Promise.all(chunk.map(({ bytes }) => computeCID(bytes)));
|
|
644
|
+
const toEntry = (change, size, cid) => {
|
|
645
|
+
const wirePath = normaliseWirePath(change.path);
|
|
646
|
+
const mimeType = detectMimeType(wirePath);
|
|
647
|
+
return {
|
|
648
|
+
path: wirePath,
|
|
649
|
+
cid,
|
|
650
|
+
size,
|
|
651
|
+
...mimeType ? { mimeType } : {},
|
|
652
|
+
...change.old_cid ? { ifMatch: change.old_cid } : {},
|
|
653
|
+
deviceId: change.device_id,
|
|
654
|
+
mtime: change.mtime
|
|
655
|
+
};
|
|
656
|
+
};
|
|
657
|
+
const inlineCandidate = (bytes, mimeType) => {
|
|
658
|
+
if (bytes.length > DEFAULT_INLINE_THRESHOLD) return void 0;
|
|
659
|
+
return decodeInlineText(bytes, mimeType) ?? void 0;
|
|
660
|
+
};
|
|
661
|
+
const prep = await this.cloudAfs.prepareUpload(chunk.map(({ change, bytes }, i) => toEntry(change, bytes.length, cids[i])));
|
|
662
|
+
if (prep.entries.length !== chunk.length || prep.entries.some((e) => !e.grant)) throw new BatchUnsupportedError();
|
|
663
|
+
const putOk = new Array(chunk.length).fill(false);
|
|
664
|
+
await this.boundedAll(chunk.length, AFS_DID_SPACE_UPLOAD_PUT_CONCURRENCY, async (i) => {
|
|
665
|
+
putOk[i] = await this.cloudAfs.uploadObject(prep.entries[i].grant, chunk[i].bytes) === 200;
|
|
666
|
+
});
|
|
667
|
+
const commitEntries = [];
|
|
668
|
+
const commitIndex = /* @__PURE__ */ new Map();
|
|
669
|
+
for (const [i, { change, bytes }] of chunk.entries()) {
|
|
670
|
+
if (!putOk[i]) continue;
|
|
671
|
+
commitIndex.set(i, commitEntries.length);
|
|
672
|
+
const base = toEntry(change, bytes.length, cids[i]);
|
|
673
|
+
const content = inlineCandidate(bytes, base.mimeType);
|
|
674
|
+
const metadata = await this.readAppMeta(change.path, bytes);
|
|
675
|
+
commitEntries.push({
|
|
676
|
+
...base,
|
|
677
|
+
...content !== void 0 ? { content } : {},
|
|
678
|
+
...metadata ? { metadata } : {}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
const commit = commitEntries.length > 0 ? await this.cloudAfs.commitUpload(prep.sessionId, commitEntries) : {
|
|
682
|
+
results: [],
|
|
683
|
+
succeeded: 0,
|
|
684
|
+
failed: 0
|
|
685
|
+
};
|
|
686
|
+
const results = chunk.map(({ change }, i) => {
|
|
687
|
+
const ci = commitIndex.get(i);
|
|
688
|
+
if (ci !== void 0) return commit.results[ci] ?? {
|
|
689
|
+
path: normaliseWirePath(change.path),
|
|
690
|
+
success: false,
|
|
691
|
+
status: 500,
|
|
692
|
+
error: "missing commit result"
|
|
693
|
+
};
|
|
694
|
+
return {
|
|
695
|
+
path: normaliseWirePath(change.path),
|
|
696
|
+
success: false,
|
|
697
|
+
status: 503,
|
|
698
|
+
code: "UPLOAD_PUT_FAILED",
|
|
699
|
+
error: "presigned PUT did not return 200"
|
|
700
|
+
};
|
|
701
|
+
});
|
|
702
|
+
let succeeded = 0;
|
|
703
|
+
for (const r of results) if (r.success) succeeded++;
|
|
704
|
+
return {
|
|
705
|
+
results,
|
|
706
|
+
succeeded,
|
|
707
|
+
failed: results.length - succeeded
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
/** Run `fn(0..n-1)` with at most `limit` in flight (push-side PUT fan-out). */
|
|
711
|
+
async boundedAll(n, limit, fn) {
|
|
712
|
+
let next = 0;
|
|
713
|
+
const workers = Array.from({ length: Math.min(limit, n) }, async () => {
|
|
714
|
+
while (next < n) await fn(next++);
|
|
715
|
+
});
|
|
716
|
+
await Promise.all(workers);
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Upload a single change row. Split from push() so both the normal
|
|
720
|
+
* loop and future retry paths can call it.
|
|
721
|
+
*
|
|
722
|
+
* On 409 conflict, delegates to {@link resolveConflict}, which decides
|
|
723
|
+
* by LWW + tiebreak whether to force-overwrite the cloud or adopt the
|
|
724
|
+
* cloud's version locally. Either way, after resolveConflict returns,
|
|
725
|
+
* the row is considered "handled" and the push cursor advances past it.
|
|
726
|
+
*/
|
|
727
|
+
async pushOne(change) {
|
|
728
|
+
const wirePath = normaliseWirePath(change.path);
|
|
729
|
+
if (change.op === "put") {
|
|
730
|
+
if (change.cid === null) throw new Error(`SyncEngine.push: put row with null cid at seq=${change.seq} path=${change.path}`);
|
|
731
|
+
const bytes = await this.provider.readBytes(change.path);
|
|
732
|
+
if (bytes === null) return;
|
|
733
|
+
const appMeta = await this.readAppMeta(change.path, bytes);
|
|
734
|
+
try {
|
|
735
|
+
await this.cloudAfs.write(wirePath, {
|
|
736
|
+
content: bytes,
|
|
737
|
+
meta: {
|
|
738
|
+
...appMeta,
|
|
739
|
+
extra: {
|
|
740
|
+
contentCid: change.cid,
|
|
741
|
+
deviceId: change.device_id,
|
|
742
|
+
mtime: change.mtime,
|
|
743
|
+
ifMatch: change.old_cid ?? void 0
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
} catch (e) {
|
|
748
|
+
if (e instanceof AFSConflictError) {
|
|
749
|
+
await this.resolveConflict(change, {
|
|
750
|
+
currentCid: e.currentCid,
|
|
751
|
+
currentMtime: e.currentMtime,
|
|
752
|
+
currentDeviceId: e.currentDeviceId
|
|
753
|
+
});
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
throw e;
|
|
757
|
+
}
|
|
758
|
+
} else if (change.op === "delete") try {
|
|
759
|
+
await this.cloudAfs.delete(wirePath, { meta: {
|
|
760
|
+
deviceId: change.device_id,
|
|
761
|
+
mtime: change.mtime,
|
|
762
|
+
ifMatch: change.old_cid ?? void 0
|
|
763
|
+
} });
|
|
764
|
+
} catch (e) {
|
|
765
|
+
if (e instanceof AFSConflictError) {
|
|
766
|
+
await this.resolveConflict(change, {
|
|
767
|
+
currentCid: e.currentCid,
|
|
768
|
+
currentMtime: e.currentMtime,
|
|
769
|
+
currentDeviceId: e.currentDeviceId
|
|
770
|
+
});
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
throw e;
|
|
774
|
+
}
|
|
775
|
+
else throw new Error(`SyncEngine.push: unexpected op=${change.op} at seq=${change.seq}`);
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Resolve a push-side 409 conflict via Last-Write-Wins (LWW).
|
|
779
|
+
*
|
|
780
|
+
* Decision matrix (see `native-sync-design.md` G11 for the full 7-row
|
|
781
|
+
* matrix and rationale):
|
|
782
|
+
*
|
|
783
|
+
* 1. `localMtime > cloudMtime` → **local wins**: force-overwrite the
|
|
784
|
+
* cloud (resend without `If-Match`). The server still validates
|
|
785
|
+
* `X-Content-CID`, so a corrupted upload still fails.
|
|
786
|
+
*
|
|
787
|
+
* 2. `localMtime < cloudMtime` → **cloud wins**: adopt the cloud's
|
|
788
|
+
* version locally via `provider.internalWrite/Delete` with the
|
|
789
|
+
* ORIGIN deviceId from the conflict response. Echo-loop Layer II.
|
|
790
|
+
*
|
|
791
|
+
* 3. `localMtime === cloudMtime` → **tiebreak by CID lexicographic
|
|
792
|
+
* order** (round 7 decision). The larger cid wins. delete ops
|
|
793
|
+
* treat their cid as the empty string, so a `put` with non-empty
|
|
794
|
+
* cid always beats a `delete` at the same mtime.
|
|
795
|
+
*
|
|
796
|
+
* Special cases handled here:
|
|
797
|
+
* - delete vs delete (both null cid) → idempotent no-op (defensive;
|
|
798
|
+
* in practice the server's delete handler turns DELETE 404 into
|
|
799
|
+
* success and the conflict never reaches here).
|
|
800
|
+
* - same mtime + same cid → idempotent natural convergence (the
|
|
801
|
+
* local-wins path is taken but the resend produces the same cid
|
|
802
|
+
* the cloud already has).
|
|
803
|
+
*
|
|
804
|
+
* mtime skew defense: G5.c (Task 3.3) rejects writes with
|
|
805
|
+
* `mtime > serverNow + 5min` at the header layer, so a single device
|
|
806
|
+
* with a wildly bad clock cannot poison the LWW decision globally.
|
|
807
|
+
*/
|
|
808
|
+
async resolveConflict(change, resp) {
|
|
809
|
+
if (resp.currentMtime === null) throw new ConflictResolutionError(change, "missing currentMtime in conflict response");
|
|
810
|
+
const localMtime = change.mtime;
|
|
811
|
+
const cloudMtime = resp.currentMtime;
|
|
812
|
+
if (change.op === "delete" && resp.currentCid === null) return;
|
|
813
|
+
const localCid = change.cid ?? "";
|
|
814
|
+
const cloudCid = resp.currentCid ?? "";
|
|
815
|
+
if (localMtime > cloudMtime || localMtime === cloudMtime && localCid > cloudCid) {
|
|
816
|
+
const wirePath = normaliseWirePath(change.path);
|
|
817
|
+
if (change.op === "put") {
|
|
818
|
+
const bytes = await this.provider.readBytes(change.path);
|
|
819
|
+
if (bytes === null) return;
|
|
820
|
+
const appMeta = await this.readAppMeta(change.path, bytes);
|
|
821
|
+
await this.cloudAfs.write(wirePath, {
|
|
822
|
+
content: bytes,
|
|
823
|
+
meta: {
|
|
824
|
+
...appMeta,
|
|
825
|
+
extra: {
|
|
826
|
+
contentCid: change.cid,
|
|
827
|
+
deviceId: change.device_id,
|
|
828
|
+
mtime: change.mtime
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
} else await this.cloudAfs.delete(wirePath, { meta: {
|
|
833
|
+
deviceId: change.device_id,
|
|
834
|
+
mtime: change.mtime
|
|
835
|
+
} });
|
|
836
|
+
} else {
|
|
837
|
+
const wirePath = normaliseWirePath(change.path);
|
|
838
|
+
const originDeviceId = resp.currentDeviceId ?? "unknown";
|
|
839
|
+
if (resp.currentCid !== null) {
|
|
840
|
+
let bytes;
|
|
841
|
+
try {
|
|
842
|
+
bytes = (await this.cloudAfs.read(`/.objects/${resp.currentCid}`)).data?.content;
|
|
843
|
+
} catch (e) {
|
|
844
|
+
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
845
|
+
throw new CurrentObjectMissingError(wirePath, resp.currentCid, "conflict");
|
|
846
|
+
}
|
|
847
|
+
if (bytes === void 0) throw new CurrentObjectMissingError(wirePath, resp.currentCid, "conflict");
|
|
848
|
+
await this.provider.internalWrite(wirePath, {
|
|
849
|
+
content: bytes,
|
|
850
|
+
deviceId: originDeviceId,
|
|
851
|
+
mtime: cloudMtime,
|
|
852
|
+
metadata: await this.readAppMeta(wirePath, bytes)
|
|
853
|
+
});
|
|
854
|
+
} else await this.provider.internalDelete(wirePath, {
|
|
855
|
+
deviceId: originDeviceId,
|
|
856
|
+
mtime: cloudMtime
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Fetch and apply incoming changes from the cloud. Loops until either
|
|
862
|
+
* the server reports `hasMore === false`, the deadline passes, or the
|
|
863
|
+
* abort signal fires. On 409 Compacted, delegates to fullResync().
|
|
864
|
+
*
|
|
865
|
+
* Apply order: sequential in seq order within a batch (SQLite
|
|
866
|
+
* integrity + single origin-deviceId per row). Content fetching is
|
|
867
|
+
* parallel up to `PULL_FETCH_CONCURRENCY`.
|
|
868
|
+
*/
|
|
869
|
+
async pull(opts) {
|
|
870
|
+
const initialCloudSeq = (await this.syncStateStore.load()).cloudSeq;
|
|
871
|
+
let cloudCursor = initialCloudSeq > 0 ? String(initialCloudSeq) : void 0;
|
|
872
|
+
let pulled = 0;
|
|
873
|
+
try {
|
|
874
|
+
while (true) {
|
|
875
|
+
if (opts.signal?.aborted) return {
|
|
876
|
+
pulled,
|
|
877
|
+
fullResynced: false
|
|
878
|
+
};
|
|
879
|
+
if (Date.now() >= opts.deadline) return {
|
|
880
|
+
pulled,
|
|
881
|
+
fullResynced: false
|
|
882
|
+
};
|
|
883
|
+
let listResult;
|
|
884
|
+
try {
|
|
885
|
+
listResult = await this.cloudAfs.list(".changelog", {
|
|
886
|
+
cursor: cloudCursor ?? "0",
|
|
887
|
+
limit: PULL_BATCH_LIMIT
|
|
888
|
+
});
|
|
889
|
+
} catch (e) {
|
|
890
|
+
if (e instanceof AFSCompactedError) {
|
|
891
|
+
const r = await this.fullResync({
|
|
892
|
+
signal: opts.signal,
|
|
893
|
+
deadline: opts.deadline
|
|
894
|
+
});
|
|
895
|
+
return {
|
|
896
|
+
pulled: pulled + r.pulled,
|
|
897
|
+
fullResynced: true
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
throw e;
|
|
901
|
+
}
|
|
902
|
+
const allChanges = listResult.data.map((entry) => {
|
|
903
|
+
const extra = entry.meta?.extra ?? {};
|
|
904
|
+
return {
|
|
905
|
+
seq: Number(extra.seq ?? 0),
|
|
906
|
+
originalPath: String(extra.originalPath ?? ""),
|
|
907
|
+
op: String(extra.op ?? ""),
|
|
908
|
+
cid: typeof extra.cid === "string" ? extra.cid : null,
|
|
909
|
+
oldCid: typeof extra.oldCid === "string" ? extra.oldCid : null,
|
|
910
|
+
size: typeof extra.size === "number" ? extra.size : null,
|
|
911
|
+
mtime: Number(extra.mtime ?? 0),
|
|
912
|
+
deviceId: String(extra.deviceId ?? "")
|
|
913
|
+
};
|
|
914
|
+
});
|
|
915
|
+
const hasMore = listResult.meta?.hasMore ?? false;
|
|
916
|
+
for (let i = 1; i < allChanges.length; i++) if (allChanges[i].seq <= allChanges[i - 1].seq) throw new Error("SyncEngine.pull: cloud returned out-of-order changes");
|
|
917
|
+
const foreignChanges = allChanges.filter((c) => c.deviceId !== this.selfDeviceId);
|
|
918
|
+
const putRows = foreignChanges.filter((c) => c.op === "put" && c.cid !== null);
|
|
919
|
+
const cidMap = /* @__PURE__ */ new Map();
|
|
920
|
+
await this.fetchContent(putRows, cidMap, opts);
|
|
921
|
+
const lastCidForPath = /* @__PURE__ */ new Map();
|
|
922
|
+
for (const c of foreignChanges) lastCidForPath.set(normaliseAfsPath(c.originalPath), c.op === "put" ? c.cid : null);
|
|
923
|
+
for (const change of foreignChanges) {
|
|
924
|
+
if (opts.signal?.aborted) return {
|
|
925
|
+
pulled,
|
|
926
|
+
fullResynced: false
|
|
927
|
+
};
|
|
928
|
+
const wirePath = normaliseAfsPath(change.originalPath);
|
|
929
|
+
if (change.op === "put") {
|
|
930
|
+
if (change.cid === null) continue;
|
|
931
|
+
const bytes = cidMap.get(change.cid);
|
|
932
|
+
if (bytes === void 0) {
|
|
933
|
+
if (classifyMissingObject(wirePath, change.cid, lastCidForPath) === "current") throw new CurrentObjectMissingError(wirePath, change.cid, "latest-put");
|
|
934
|
+
log.warn(`SyncEngine.pull: skipping superseded seq=${change.seq} path=${wirePath} (cid ${change.cid} reclaimed)`);
|
|
935
|
+
cloudCursor = String(change.seq);
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
await this.provider.internalWrite(wirePath, {
|
|
939
|
+
content: bytes,
|
|
940
|
+
deviceId: change.deviceId,
|
|
941
|
+
mtime: change.mtime,
|
|
942
|
+
metadata: await this.readAppMeta(wirePath, bytes)
|
|
943
|
+
});
|
|
944
|
+
} else if (change.op === "delete") await this.provider.internalDelete(wirePath, {
|
|
945
|
+
deviceId: change.deviceId,
|
|
946
|
+
mtime: change.mtime
|
|
947
|
+
});
|
|
948
|
+
cloudCursor = String(change.seq);
|
|
949
|
+
pulled += 1;
|
|
950
|
+
}
|
|
951
|
+
if (foreignChanges.length === 0 && allChanges.length > 0) cloudCursor = listResult.meta?.cursor ?? cloudCursor;
|
|
952
|
+
if (!hasMore) break;
|
|
953
|
+
}
|
|
954
|
+
} finally {
|
|
955
|
+
if (cloudCursor !== void 0) {
|
|
956
|
+
const numericCursor = Number(cloudCursor);
|
|
957
|
+
if (Number.isFinite(numericCursor) && numericCursor > 0) await this.syncStateStore.save({ cloudSeq: numericCursor });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
pulled,
|
|
962
|
+
fullResynced: false
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Fetch content bytes for a batch of `put` rows in parallel, bounded
|
|
967
|
+
* by {@link PULL_FETCH_CONCURRENCY}. Results land in `cidMap`;
|
|
968
|
+
* individual fetch failures (404, cid mismatch) are logged and the
|
|
969
|
+
* offending cid is omitted so the caller's serial apply pass can
|
|
970
|
+
* detect the gap and skip.
|
|
971
|
+
*/
|
|
972
|
+
/**
|
|
973
|
+
* Content fetch dispatcher (P4): chunked `batchRead(optional:true)` when
|
|
974
|
+
* the adapter has it (≤20 cids and ≤2MB ESTIMATED bytes per chunk — size
|
|
975
|
+
* from the changelog rows), single concurrent reads otherwise. Tail-
|
|
976
|
+
* skipped cids are simply re-queued for the next chunk (they were never
|
|
977
|
+
* read); `missing:true` cids stay out of the map so the apply loop's
|
|
978
|
+
* §4.7.2 current/superseded classification is untouched.
|
|
979
|
+
*/
|
|
980
|
+
async fetchContent(rows, cidMap, opts) {
|
|
981
|
+
const planeSatisfied = await this.fillBytesViaDownloadPlane(rows.filter((r) => r.cid !== null), cidMap, opts);
|
|
982
|
+
const remaining = planeSatisfied.size === 0 ? rows : rows.filter((r) => r.cid === null || !planeSatisfied.has(r.cid));
|
|
983
|
+
if (this.batchFallback || typeof this.cloudAfs.batchRead !== "function") return this.fetchContentConcurrent(remaining, cidMap, opts);
|
|
984
|
+
const sizeByCid = /* @__PURE__ */ new Map();
|
|
985
|
+
for (const r of remaining) if (r.cid !== null && !sizeByCid.has(r.cid)) sizeByCid.set(r.cid, r.size ?? 0);
|
|
986
|
+
const queue = Array.from(sizeByCid.keys());
|
|
987
|
+
while (queue.length > 0) {
|
|
988
|
+
if (opts.signal?.aborted) return;
|
|
989
|
+
const chunk = [];
|
|
990
|
+
let estBytes = 0;
|
|
991
|
+
while (queue.length > 0 && chunk.length < BATCH_READ_MAX_ENTRIES) {
|
|
992
|
+
const cid = queue[0];
|
|
993
|
+
const est = sizeByCid.get(cid) ?? 0;
|
|
994
|
+
if (chunk.length > 0 && estBytes + est > BATCH_READ_MAX_BYTES) break;
|
|
995
|
+
chunk.push(cid);
|
|
996
|
+
estBytes += est;
|
|
997
|
+
queue.shift();
|
|
998
|
+
}
|
|
999
|
+
let result;
|
|
1000
|
+
try {
|
|
1001
|
+
result = await this.cloudAfs.batchRead(chunk.map((cid) => ({
|
|
1002
|
+
path: `/.objects/${cid}`,
|
|
1003
|
+
optional: true
|
|
1004
|
+
})));
|
|
1005
|
+
} catch (e) {
|
|
1006
|
+
if (e instanceof BatchUnsupportedError) {
|
|
1007
|
+
this.batchFallback = true;
|
|
1008
|
+
const remaining$1 = [...chunk, ...queue].map((cid) => ({
|
|
1009
|
+
cid,
|
|
1010
|
+
size: sizeByCid.get(cid) ?? null
|
|
1011
|
+
}));
|
|
1012
|
+
return this.fetchContentConcurrent(remaining$1, cidMap, opts);
|
|
1013
|
+
}
|
|
1014
|
+
if (e instanceof BatchOverBudgetError && chunk.length > 1) {
|
|
1015
|
+
const half = Math.ceil(chunk.length / 2);
|
|
1016
|
+
for (const cid of chunk.slice(0, half)) await this.fetchOneObject(cid, cidMap, opts);
|
|
1017
|
+
queue.unshift(...chunk.slice(half));
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
throw e;
|
|
1021
|
+
}
|
|
1022
|
+
let skippedTail = false;
|
|
1023
|
+
for (let i = 0; i < result.results.length; i++) {
|
|
1024
|
+
const r = result.results[i];
|
|
1025
|
+
const cid = chunk[i];
|
|
1026
|
+
if (r.success) {
|
|
1027
|
+
const content = r.data?.content;
|
|
1028
|
+
const bytes = content instanceof Uint8Array ? content : typeof content === "string" ? new TextEncoder().encode(content) : void 0;
|
|
1029
|
+
if (bytes) {
|
|
1030
|
+
const actualCid = await computeCID(bytes);
|
|
1031
|
+
if (actualCid !== cid) log.warn(`SyncEngine.pull: cid mismatch for ${cid}: got ${actualCid}`);
|
|
1032
|
+
else cidMap.set(cid, bytes);
|
|
1033
|
+
}
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
if (r.code === "BATCH_TAIL_SKIPPED") {
|
|
1037
|
+
if (i === 0) for (const c of chunk.slice(i)) await this.fetchOneObject(c, cidMap, opts);
|
|
1038
|
+
else queue.unshift(...chunk.slice(i));
|
|
1039
|
+
skippedTail = true;
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (skippedTail) continue;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/** Single `.objects/{cid}` read with the same soft-failure semantics. */
|
|
1047
|
+
async fetchOneObject(cid, cidMap, opts) {
|
|
1048
|
+
if (opts.signal?.aborted) return;
|
|
1049
|
+
try {
|
|
1050
|
+
const bytes = (await this.cloudAfs.read(`/.objects/${cid}`)).data?.content;
|
|
1051
|
+
if (bytes) {
|
|
1052
|
+
const actualCid = await computeCID(bytes);
|
|
1053
|
+
if (actualCid === cid) cidMap.set(cid, bytes);
|
|
1054
|
+
else log.warn(`SyncEngine.pull: cid mismatch for ${cid}: got ${actualCid}`);
|
|
1055
|
+
}
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1058
|
+
log.warn(`SyncEngine.pull: read .objects/${cid} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
async fetchContentConcurrent(rows, cidMap, opts) {
|
|
1062
|
+
const uniqueCids = /* @__PURE__ */ new Set();
|
|
1063
|
+
for (const r of rows) if (r.cid !== null) uniqueCids.add(r.cid);
|
|
1064
|
+
const cids = Array.from(uniqueCids);
|
|
1065
|
+
let index = 0;
|
|
1066
|
+
const worker = async () => {
|
|
1067
|
+
while (index < cids.length) {
|
|
1068
|
+
if (opts.signal?.aborted) return;
|
|
1069
|
+
const cid = cids[index++];
|
|
1070
|
+
try {
|
|
1071
|
+
const bytes = (await this.cloudAfs.read(`/.objects/${cid}`)).data?.content;
|
|
1072
|
+
if (bytes) {
|
|
1073
|
+
const actualCid = await computeCID(bytes);
|
|
1074
|
+
if (actualCid !== cid) log.warn(`SyncEngine.pull: cid mismatch for ${cid}: got ${actualCid}`);
|
|
1075
|
+
else cidMap.set(cid, bytes);
|
|
1076
|
+
}
|
|
1077
|
+
} catch (e) {
|
|
1078
|
+
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1079
|
+
log.warn(`SyncEngine.pull: read .objects/${cid} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
const concurrency = Math.max(1, Math.min(PULL_FETCH_CONCURRENCY, cids.length));
|
|
1084
|
+
const workers = [];
|
|
1085
|
+
for (let i = 0; i < concurrency; i++) workers.push(worker());
|
|
1086
|
+
await Promise.all(workers);
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Whether the download plane can be used RIGHT NOW: the adapter speaks the
|
|
1090
|
+
* methods, the server advertised the INDEPENDENT capability (memoized per
|
|
1091
|
+
* instance, §7.3), and this session hasn't latched a 401/403 fallback.
|
|
1092
|
+
*/
|
|
1093
|
+
async downloadPlaneAvailable() {
|
|
1094
|
+
if (this.downloadFallback) return false;
|
|
1095
|
+
if (typeof this.cloudAfs.prepareDownload !== "function" || typeof this.cloudAfs.downloadObject !== "function") return false;
|
|
1096
|
+
if (this.downloadPlaneCapable === null) try {
|
|
1097
|
+
this.downloadPlaneCapable = typeof this.cloudAfs.downloadGrantCapability === "function" ? await this.cloudAfs.downloadGrantCapability() : false;
|
|
1098
|
+
} catch {
|
|
1099
|
+
this.downloadPlaneCapable = false;
|
|
1100
|
+
}
|
|
1101
|
+
return this.downloadPlaneCapable === true;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Fetch LARGE objects (size ≥ {@link DOWNLOAD_PLANE_MIN_BYTES}) via the
|
|
1105
|
+
* download plane — prepare-download → bounded-concurrency direct R2 GET →
|
|
1106
|
+
* computeCID → `bytesMap`. Bytes leave R2 directly (never through the worker /
|
|
1107
|
+
* base64). Returns the SET of cids it satisfied; the caller routes the rest
|
|
1108
|
+
* (small objects, Node `grant:null`, forbidden, 404, post-fallback) through
|
|
1109
|
+
* batchRead — so a true miss still raises the existing fail-closed path.
|
|
1110
|
+
*
|
|
1111
|
+
* fail-closed: a byte string whose `computeCID` ≠ the requested cid (R2 served
|
|
1112
|
+
* tampered/other bytes) is DROPPED, never written (§8.1). A 401/403 latches
|
|
1113
|
+
* `downloadFallback` for the whole session — at most one wall-bang (§7.3).
|
|
1114
|
+
*/
|
|
1115
|
+
async fillBytesViaDownloadPlane(entries, bytesMap, opts) {
|
|
1116
|
+
const satisfied = /* @__PURE__ */ new Set();
|
|
1117
|
+
if (!await this.downloadPlaneAvailable()) return satisfied;
|
|
1118
|
+
const bigCids = [];
|
|
1119
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1120
|
+
for (const e of entries) {
|
|
1121
|
+
if ((e.size ?? 0) < DOWNLOAD_PLANE_MIN_BYTES) continue;
|
|
1122
|
+
if (seen.has(e.cid)) continue;
|
|
1123
|
+
seen.add(e.cid);
|
|
1124
|
+
bigCids.push(e.cid);
|
|
1125
|
+
}
|
|
1126
|
+
if (bigCids.length === 0) return satisfied;
|
|
1127
|
+
for (let i = 0; i < bigCids.length; i += DOWNLOAD_MAX_ENTRIES) {
|
|
1128
|
+
if (opts.signal?.aborted || this.downloadFallback) break;
|
|
1129
|
+
const batch = bigCids.slice(i, i + DOWNLOAD_MAX_ENTRIES);
|
|
1130
|
+
let prep;
|
|
1131
|
+
try {
|
|
1132
|
+
prep = await this.cloudAfs.prepareDownload(batch.map((cid) => ({ cid })));
|
|
1133
|
+
} catch (e) {
|
|
1134
|
+
if (e instanceof BatchUnsupportedError) {
|
|
1135
|
+
this.downloadPlaneCapable = false;
|
|
1136
|
+
return satisfied;
|
|
1137
|
+
}
|
|
1138
|
+
throw e;
|
|
1139
|
+
}
|
|
1140
|
+
const grantByCid = /* @__PURE__ */ new Map();
|
|
1141
|
+
for (const pe of prep.entries) if (pe.status === "ok" && pe.grant) grantByCid.set(pe.cid, pe.grant);
|
|
1142
|
+
const grantedCids = batch.filter((c) => grantByCid.has(c));
|
|
1143
|
+
await this.boundedAll(grantedCids.length, this.downloadGetConcurrency, async (j) => {
|
|
1144
|
+
if (opts.signal?.aborted || this.downloadFallback) return;
|
|
1145
|
+
const cid = grantedCids[j];
|
|
1146
|
+
try {
|
|
1147
|
+
const bytes = await this.cloudAfs.downloadObject(grantByCid.get(cid));
|
|
1148
|
+
const actualCid = await computeCID(bytes);
|
|
1149
|
+
if (actualCid === cid) {
|
|
1150
|
+
bytesMap.set(cid, bytes);
|
|
1151
|
+
satisfied.add(cid);
|
|
1152
|
+
} else log.warn(`SyncEngine.pull: download-plane cid mismatch for ${cid}: got ${actualCid}`);
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
if (e instanceof DownloadForbiddenError) {
|
|
1155
|
+
this.downloadFallback = true;
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
return satisfied;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* First-sync / post-compaction recovery path. Fetches the full manifest
|
|
1166
|
+
* and reconciles the local store against it, skipping paths the local
|
|
1167
|
+
* changelog still has unpushed changes for (invariant I1).
|
|
1168
|
+
*
|
|
1169
|
+
* ORDERING IS CRITICAL — `pendingPaths` MUST be collected BEFORE the
|
|
1170
|
+
* manifest fetch so a delete written between pendingPaths collection
|
|
1171
|
+
* and apply does not get reverted by the manifest restoration.
|
|
1172
|
+
*/
|
|
1173
|
+
async fullResync(opts) {
|
|
1174
|
+
const unpushed = await this.localChangelog.getUnpushed(0, Number.MAX_SAFE_INTEGER);
|
|
1175
|
+
const pendingPaths = new Set(unpushed.map((c) => c.path));
|
|
1176
|
+
if (opts.signal?.aborted) return { pulled: 0 };
|
|
1177
|
+
const manifestResult = await this.cloudAfs.list("", { maxDepth: -1 });
|
|
1178
|
+
if (manifestResult.meta?.truncated === true) throw new ManifestTooLargeError();
|
|
1179
|
+
const manifestEntries = manifestResult.data.map((entry) => {
|
|
1180
|
+
const extra = entry.meta?.extra ?? {};
|
|
1181
|
+
const cid = typeof extra.cid === "string" ? extra.cid : typeof entry.meta?.cid === "string" ? entry.meta.cid : "";
|
|
1182
|
+
return {
|
|
1183
|
+
path: entry.path,
|
|
1184
|
+
cid,
|
|
1185
|
+
mtime: typeof extra.mtime === "number" ? extra.mtime : 0,
|
|
1186
|
+
size: typeof entry.meta?.size === "number" ? entry.meta.size : 0
|
|
1187
|
+
};
|
|
1188
|
+
});
|
|
1189
|
+
const manifestSeq = Number(manifestResult.meta?.cursor ?? 0);
|
|
1190
|
+
const manifestPathSet = /* @__PURE__ */ new Set();
|
|
1191
|
+
for (const e of manifestEntries) manifestPathSet.add(normaliseToLocalPath(e.path));
|
|
1192
|
+
let pulled = 0;
|
|
1193
|
+
const applicable = manifestEntries.filter((entry) => !pendingPaths.has(normaliseToLocalPath(entry.path)));
|
|
1194
|
+
const planeBytes = /* @__PURE__ */ new Map();
|
|
1195
|
+
const planeSatisfied = await this.fillBytesViaDownloadPlane(applicable.map((e) => ({
|
|
1196
|
+
cid: e.cid,
|
|
1197
|
+
size: e.size
|
|
1198
|
+
})), planeBytes, opts);
|
|
1199
|
+
for (const entry of applicable) {
|
|
1200
|
+
if (opts.signal?.aborted) return { pulled };
|
|
1201
|
+
if (Date.now() >= opts.deadline) return { pulled };
|
|
1202
|
+
const bytes = planeBytes.get(entry.cid);
|
|
1203
|
+
if (!bytes) continue;
|
|
1204
|
+
await this.provider.internalWrite(normaliseAfsPath(entry.path), {
|
|
1205
|
+
content: bytes,
|
|
1206
|
+
deviceId: this.selfDeviceId,
|
|
1207
|
+
mtime: entry.mtime,
|
|
1208
|
+
metadata: await this.readAppMeta(normaliseAfsPath(entry.path), bytes)
|
|
1209
|
+
});
|
|
1210
|
+
pulled += 1;
|
|
1211
|
+
}
|
|
1212
|
+
const viaBatch = planeSatisfied.size === 0 ? applicable : applicable.filter((e) => !planeSatisfied.has(e.cid));
|
|
1213
|
+
const useBatchRead = () => !this.batchFallback && typeof this.cloudAfs.batchRead === "function";
|
|
1214
|
+
for (let offset = 0; offset < viaBatch.length;) {
|
|
1215
|
+
if (opts.signal?.aborted) return { pulled };
|
|
1216
|
+
if (Date.now() >= opts.deadline) return { pulled };
|
|
1217
|
+
const chunk = [];
|
|
1218
|
+
while (offset < viaBatch.length && chunk.length < (useBatchRead() ? BATCH_READ_MAX_ENTRIES : 1)) {
|
|
1219
|
+
chunk.push(viaBatch[offset]);
|
|
1220
|
+
offset++;
|
|
1221
|
+
}
|
|
1222
|
+
const bytesByCid = /* @__PURE__ */ new Map();
|
|
1223
|
+
if (useBatchRead() && chunk.length > 0) {
|
|
1224
|
+
let result;
|
|
1225
|
+
try {
|
|
1226
|
+
result = await this.cloudAfs.batchRead(chunk.map((entry) => ({
|
|
1227
|
+
path: `/.objects/${entry.cid}`,
|
|
1228
|
+
optional: true
|
|
1229
|
+
})));
|
|
1230
|
+
} catch (e) {
|
|
1231
|
+
if (e instanceof BatchUnsupportedError) {
|
|
1232
|
+
this.batchFallback = true;
|
|
1233
|
+
offset -= chunk.length;
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
throw e;
|
|
1237
|
+
}
|
|
1238
|
+
for (let i = 0; i < result.results.length; i++) {
|
|
1239
|
+
const r = result.results[i];
|
|
1240
|
+
const entry = chunk[i];
|
|
1241
|
+
if (r.code === "BATCH_TAIL_SKIPPED") {
|
|
1242
|
+
offset -= chunk.length - i;
|
|
1243
|
+
break;
|
|
1244
|
+
}
|
|
1245
|
+
if (!r.success) throw new CurrentObjectMissingError(entry.path, entry.cid, "manifest");
|
|
1246
|
+
const content = r.data?.content;
|
|
1247
|
+
const bytes = content instanceof Uint8Array ? content : typeof content === "string" ? new TextEncoder().encode(content) : void 0;
|
|
1248
|
+
if (!bytes) throw new CurrentObjectMissingError(entry.path, entry.cid, "manifest");
|
|
1249
|
+
bytesByCid.set(entry.cid, bytes);
|
|
1250
|
+
}
|
|
1251
|
+
} else for (const entry of chunk) try {
|
|
1252
|
+
const content = (await this.cloudAfs.read(`/.objects/${entry.cid}`)).data?.content;
|
|
1253
|
+
if (!content) throw new CurrentObjectMissingError(entry.path, entry.cid, "manifest");
|
|
1254
|
+
bytesByCid.set(entry.cid, content);
|
|
1255
|
+
} catch (e) {
|
|
1256
|
+
if (e instanceof CurrentObjectMissingError) throw e;
|
|
1257
|
+
if (e instanceof Error && e.name === "AbortError") throw e;
|
|
1258
|
+
throw new CurrentObjectMissingError(entry.path, entry.cid, "manifest");
|
|
1259
|
+
}
|
|
1260
|
+
for (const entry of chunk) {
|
|
1261
|
+
if (opts.signal?.aborted) return { pulled };
|
|
1262
|
+
const bytes = bytesByCid.get(entry.cid);
|
|
1263
|
+
if (!bytes) continue;
|
|
1264
|
+
const wirePath = normaliseAfsPath(entry.path);
|
|
1265
|
+
await this.provider.internalWrite(wirePath, {
|
|
1266
|
+
content: bytes,
|
|
1267
|
+
deviceId: this.selfDeviceId,
|
|
1268
|
+
mtime: entry.mtime,
|
|
1269
|
+
metadata: await this.readAppMeta(wirePath, bytes)
|
|
1270
|
+
});
|
|
1271
|
+
pulled += 1;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (!opts.signal?.aborted) {
|
|
1275
|
+
const localPaths = await this.provider.listAllPaths();
|
|
1276
|
+
for (const p of localPaths) {
|
|
1277
|
+
if (opts.signal?.aborted) break;
|
|
1278
|
+
if (manifestPathSet.has(p)) continue;
|
|
1279
|
+
if (pendingPaths.has(p)) continue;
|
|
1280
|
+
await this.provider.internalDelete(normaliseAfsPath(p), { deviceId: this.selfDeviceId });
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (manifestSeq > 0) await this.syncStateStore.save({ cloudSeq: manifestSeq });
|
|
1284
|
+
return { pulled };
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
/**
|
|
1288
|
+
* Normalise an internal changelog path (no leading slash) to the AFS
|
|
1289
|
+
* wire path used by PUT/DELETE endpoints (always starts with `/`).
|
|
1290
|
+
*/
|
|
1291
|
+
function normaliseWirePath(path) {
|
|
1292
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Same as {@link normaliseWirePath} but applied to paths that may come
|
|
1296
|
+
* from either the manifest (no leading slash) or a `.changelog` row's
|
|
1297
|
+
* `meta.extra.originalPath` (no leading slash either — the server stores
|
|
1298
|
+
* without leading slash). Always returns a `/`-prefixed path suitable
|
|
1299
|
+
* for `provider.internalWrite/Delete`.
|
|
1300
|
+
*/
|
|
1301
|
+
function normaliseAfsPath(path) {
|
|
1302
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Inverse of {@link normaliseAfsPath}: strip the leading slash so the
|
|
1306
|
+
* path matches the `entries.path` / `changelog.path` column shape
|
|
1307
|
+
* (used by pendingPaths lookups).
|
|
1308
|
+
*/
|
|
1309
|
+
function normaliseToLocalPath(path) {
|
|
1310
|
+
return path.startsWith("/") ? path.slice(1) : path;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
//#endregion
|
|
1314
|
+
export { ConflictResolutionError, CurrentObjectMissingError, PULL_BATCH_LIMIT, PULL_FETCH_CONCURRENCY, PUSH_BATCH_LIMIT, PUSH_PROGRESS_FLUSH_INTERVAL, SYNC_WALL_CLOCK_BUDGET_MS, SyncEngine, classifyMissingObject };
|
|
1315
|
+
//# sourceMappingURL=sync-engine.mjs.map
|