@aigne/afs-did-space 1.12.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +26 -0
- package/README.md +129 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/_virtual/rolldown_runtime.cjs +29 -0
- package/dist/afs-metadata-context.cjs +20 -0
- package/dist/afs-metadata-context.d.cts +21 -0
- package/dist/afs-metadata-context.d.cts.map +1 -0
- package/dist/afs-metadata-context.d.mts +21 -0
- package/dist/afs-metadata-context.d.mts.map +1 -0
- package/dist/afs-metadata-context.mjs +18 -0
- package/dist/afs-metadata-context.mjs.map +1 -0
- package/dist/blocklet-hook.cjs +180 -0
- package/dist/blocklet-hook.d.cts +29 -0
- package/dist/blocklet-hook.d.cts.map +1 -0
- package/dist/blocklet-hook.d.mts +29 -0
- package/dist/blocklet-hook.d.mts.map +1 -0
- package/dist/blocklet-hook.mjs +178 -0
- package/dist/blocklet-hook.mjs.map +1 -0
- package/dist/changelog-schema.cjs +603 -0
- package/dist/changelog-schema.d.cts +395 -0
- package/dist/changelog-schema.d.cts.map +1 -0
- package/dist/changelog-schema.d.mts +395 -0
- package/dist/changelog-schema.d.mts.map +1 -0
- package/dist/changelog-schema.mjs +570 -0
- package/dist/changelog-schema.mjs.map +1 -0
- package/dist/changelog-write.cjs +80 -0
- package/dist/changelog-write.d.cts +70 -0
- package/dist/changelog-write.d.cts.map +1 -0
- package/dist/changelog-write.d.mts +70 -0
- package/dist/changelog-write.d.mts.map +1 -0
- package/dist/changelog-write.mjs +77 -0
- package/dist/changelog-write.mjs.map +1 -0
- package/dist/cid-byte-cache.cjs +126 -0
- package/dist/cid-byte-cache.d.cts +46 -0
- package/dist/cid-byte-cache.d.cts.map +1 -0
- package/dist/cid-byte-cache.d.mts +47 -0
- package/dist/cid-byte-cache.d.mts.map +1 -0
- package/dist/cid-byte-cache.mjs +125 -0
- package/dist/cid-byte-cache.mjs.map +1 -0
- package/dist/cid.cjs +68 -0
- package/dist/cid.d.cts +22 -0
- package/dist/cid.d.cts.map +1 -0
- package/dist/cid.d.mts +22 -0
- package/dist/cid.d.mts.map +1 -0
- package/dist/cid.mjs +65 -0
- package/dist/cid.mjs.map +1 -0
- package/dist/client.cjs +221 -0
- package/dist/client.d.cts +72 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.mts +72 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +220 -0
- package/dist/client.mjs.map +1 -0
- package/dist/cloudflare.cjs +510 -0
- package/dist/cloudflare.d.cts +307 -0
- package/dist/cloudflare.d.cts.map +1 -0
- package/dist/cloudflare.d.mts +308 -0
- package/dist/cloudflare.d.mts.map +1 -0
- package/dist/cloudflare.mjs +507 -0
- package/dist/cloudflare.mjs.map +1 -0
- package/dist/ctime-backfill.cjs +272 -0
- package/dist/ctime-backfill.d.cts +169 -0
- package/dist/ctime-backfill.d.cts.map +1 -0
- package/dist/ctime-backfill.d.mts +169 -0
- package/dist/ctime-backfill.d.mts.map +1 -0
- package/dist/ctime-backfill.mjs +267 -0
- package/dist/ctime-backfill.mjs.map +1 -0
- package/dist/d1-gc-queue.cjs +80 -0
- package/dist/d1-gc-queue.d.cts +59 -0
- package/dist/d1-gc-queue.d.cts.map +1 -0
- package/dist/d1-gc-queue.d.mts +59 -0
- package/dist/d1-gc-queue.d.mts.map +1 -0
- package/dist/d1-gc-queue.mjs +81 -0
- package/dist/d1-gc-queue.mjs.map +1 -0
- package/dist/d1-resolve-store.cjs +189 -0
- package/dist/d1-resolve-store.d.cts +93 -0
- package/dist/d1-resolve-store.d.cts.map +1 -0
- package/dist/d1-resolve-store.d.mts +93 -0
- package/dist/d1-resolve-store.d.mts.map +1 -0
- package/dist/d1-resolve-store.mjs +190 -0
- package/dist/d1-resolve-store.mjs.map +1 -0
- package/dist/d1-schema.cjs +361 -0
- package/dist/d1-schema.d.cts +237 -0
- package/dist/d1-schema.d.cts.map +1 -0
- package/dist/d1-schema.d.mts +237 -0
- package/dist/d1-schema.d.mts.map +1 -0
- package/dist/d1-schema.mjs +341 -0
- package/dist/d1-schema.mjs.map +1 -0
- package/dist/d1-share-store.cjs +0 -0
- package/dist/d1-share-store.d.cts +53 -0
- package/dist/d1-share-store.d.cts.map +1 -0
- package/dist/d1-share-store.d.mts +53 -0
- package/dist/d1-share-store.d.mts.map +1 -0
- package/dist/d1-share-store.mjs +0 -0
- package/dist/d1-share-store.mjs.map +1 -0
- package/dist/d1-tree-index.cjs +1098 -0
- package/dist/d1-tree-index.d.cts +203 -0
- package/dist/d1-tree-index.d.cts.map +1 -0
- package/dist/d1-tree-index.d.mts +203 -0
- package/dist/d1-tree-index.d.mts.map +1 -0
- package/dist/d1-tree-index.mjs +1094 -0
- package/dist/d1-tree-index.mjs.map +1 -0
- package/dist/data-backend.cjs +147 -0
- package/dist/data-backend.d.cts +30 -0
- package/dist/data-backend.d.cts.map +1 -0
- package/dist/data-backend.d.mts +30 -0
- package/dist/data-backend.d.mts.map +1 -0
- package/dist/data-backend.mjs +148 -0
- package/dist/data-backend.mjs.map +1 -0
- package/dist/dedup-backfill.cjs +278 -0
- package/dist/dedup-backfill.d.cts +165 -0
- package/dist/dedup-backfill.d.cts.map +1 -0
- package/dist/dedup-backfill.d.mts +165 -0
- package/dist/dedup-backfill.d.mts.map +1 -0
- package/dist/dedup-backfill.mjs +273 -0
- package/dist/dedup-backfill.mjs.map +1 -0
- package/dist/did-space-like.d.cts +2 -0
- package/dist/did-space-like.d.mts +2 -0
- package/dist/did-utils.cjs +44 -0
- package/dist/did-utils.d.cts +25 -0
- package/dist/did-utils.d.cts.map +1 -0
- package/dist/did-utils.d.mts +25 -0
- package/dist/did-utils.d.mts.map +1 -0
- package/dist/did-utils.mjs +43 -0
- package/dist/did-utils.mjs.map +1 -0
- package/dist/dirty-path-queue.cjs +31 -0
- package/dist/dirty-path-queue.d.cts +104 -0
- package/dist/dirty-path-queue.d.cts.map +1 -0
- package/dist/dirty-path-queue.d.mts +104 -0
- package/dist/dirty-path-queue.d.mts.map +1 -0
- package/dist/dirty-path-queue.mjs +31 -0
- package/dist/dirty-path-queue.mjs.map +1 -0
- package/dist/dirty-path-schema.cjs +142 -0
- package/dist/dirty-path-schema.d.cts +90 -0
- package/dist/dirty-path-schema.d.cts.map +1 -0
- package/dist/dirty-path-schema.d.mts +90 -0
- package/dist/dirty-path-schema.d.mts.map +1 -0
- package/dist/dirty-path-schema.mjs +136 -0
- package/dist/dirty-path-schema.mjs.map +1 -0
- package/dist/download-actions.cjs +43 -0
- package/dist/download-actions.mjs +40 -0
- package/dist/download-actions.mjs.map +1 -0
- package/dist/fs-object-store.cjs +153 -0
- package/dist/fs-object-store.d.cts +21 -0
- package/dist/fs-object-store.d.cts.map +1 -0
- package/dist/fs-object-store.d.mts +22 -0
- package/dist/fs-object-store.d.mts.map +1 -0
- package/dist/fs-object-store.mjs +153 -0
- package/dist/fs-object-store.mjs.map +1 -0
- package/dist/gc-queue.d.cts +43 -0
- package/dist/gc-queue.d.cts.map +1 -0
- package/dist/gc-queue.d.mts +43 -0
- package/dist/gc-queue.d.mts.map +1 -0
- package/dist/index.cjs +2712 -0
- package/dist/index.d.cts +504 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +505 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2537 -0
- package/dist/index.mjs.map +1 -0
- package/dist/inline-content-migration.cjs +212 -0
- package/dist/inline-content-migration.d.cts +110 -0
- package/dist/inline-content-migration.d.cts.map +1 -0
- package/dist/inline-content-migration.d.mts +111 -0
- package/dist/inline-content-migration.d.mts.map +1 -0
- package/dist/inline-content-migration.mjs +207 -0
- package/dist/inline-content-migration.mjs.map +1 -0
- package/dist/inline-content.cjs +145 -0
- package/dist/inline-content.d.cts +98 -0
- package/dist/inline-content.d.cts.map +1 -0
- package/dist/inline-content.d.mts +98 -0
- package/dist/inline-content.d.mts.map +1 -0
- package/dist/inline-content.mjs +137 -0
- package/dist/inline-content.mjs.map +1 -0
- package/dist/local-changelog.cjs +241 -0
- package/dist/local-changelog.d.cts +119 -0
- package/dist/local-changelog.d.cts.map +1 -0
- package/dist/local-changelog.d.mts +119 -0
- package/dist/local-changelog.d.mts.map +1 -0
- package/dist/local-changelog.mjs +240 -0
- package/dist/local-changelog.mjs.map +1 -0
- package/dist/local.cjs +435 -0
- package/dist/local.d.cts +254 -0
- package/dist/local.d.cts.map +1 -0
- package/dist/local.d.mts +254 -0
- package/dist/local.d.mts.map +1 -0
- package/dist/local.mjs +427 -0
- package/dist/local.mjs.map +1 -0
- package/dist/memory-gc-queue.cjs +36 -0
- package/dist/memory-gc-queue.d.cts +22 -0
- package/dist/memory-gc-queue.d.cts.map +1 -0
- package/dist/memory-gc-queue.d.mts +22 -0
- package/dist/memory-gc-queue.d.mts.map +1 -0
- package/dist/memory-gc-queue.mjs +36 -0
- package/dist/memory-gc-queue.mjs.map +1 -0
- package/dist/memory-tree-index.cjs +734 -0
- package/dist/memory-tree-index.d.cts +159 -0
- package/dist/memory-tree-index.d.cts.map +1 -0
- package/dist/memory-tree-index.d.mts +159 -0
- package/dist/memory-tree-index.d.mts.map +1 -0
- package/dist/memory-tree-index.mjs +735 -0
- package/dist/memory-tree-index.mjs.map +1 -0
- package/dist/metadata-db.cjs +562 -0
- package/dist/metadata-db.d.cts +102 -0
- package/dist/metadata-db.d.cts.map +1 -0
- package/dist/metadata-db.d.mts +102 -0
- package/dist/metadata-db.d.mts.map +1 -0
- package/dist/metadata-db.mjs +562 -0
- package/dist/metadata-db.mjs.map +1 -0
- package/dist/mime-utils.cjs +2 -0
- package/dist/mime-utils.d.cts +2 -0
- package/dist/mime-utils.d.mts +2 -0
- package/dist/mime-utils.mjs +3 -0
- package/dist/object-key.cjs +41 -0
- package/dist/object-key.d.cts +25 -0
- package/dist/object-key.d.cts.map +1 -0
- package/dist/object-key.d.mts +25 -0
- package/dist/object-key.d.mts.map +1 -0
- package/dist/object-key.mjs +40 -0
- package/dist/object-key.mjs.map +1 -0
- package/dist/packages/session/dist/index.cjs +788 -0
- package/dist/packages/session/dist/index.mjs +782 -0
- package/dist/packages/session/dist/index.mjs.map +1 -0
- package/dist/platform/r2/dist/types.d.cts +49 -0
- package/dist/platform/r2/dist/types.d.cts.map +1 -0
- package/dist/prefix-range.cjs +33 -0
- package/dist/prefix-range.d.cts +29 -0
- package/dist/prefix-range.d.cts.map +1 -0
- package/dist/prefix-range.d.mts +29 -0
- package/dist/prefix-range.d.mts.map +1 -0
- package/dist/prefix-range.mjs +33 -0
- package/dist/prefix-range.mjs.map +1 -0
- package/dist/prefixed-tree-index.cjs +238 -0
- package/dist/prefixed-tree-index.d.cts +85 -0
- package/dist/prefixed-tree-index.d.cts.map +1 -0
- package/dist/prefixed-tree-index.d.mts +85 -0
- package/dist/prefixed-tree-index.d.mts.map +1 -0
- package/dist/prefixed-tree-index.mjs +239 -0
- package/dist/prefixed-tree-index.mjs.map +1 -0
- package/dist/presign-signer.cjs +7 -0
- package/dist/presign-signer.d.cts +99 -0
- package/dist/presign-signer.d.cts.map +1 -0
- package/dist/presign-signer.d.mts +99 -0
- package/dist/presign-signer.d.mts.map +1 -0
- package/dist/presign-signer.mjs +7 -0
- package/dist/presign-signer.mjs.map +1 -0
- package/dist/providers/platform/r2/dist/index.d.mts +3 -0
- package/dist/providers/platform/r2/dist/types.d.mts +49 -0
- package/dist/providers/platform/r2/dist/types.d.mts.map +1 -0
- package/dist/reconcile.cjs +184 -0
- package/dist/reconcile.d.cts +26 -0
- package/dist/reconcile.d.cts.map +1 -0
- package/dist/reconcile.d.mts +26 -0
- package/dist/reconcile.d.mts.map +1 -0
- package/dist/reconcile.mjs +184 -0
- package/dist/reconcile.mjs.map +1 -0
- package/dist/reference-check.cjs +49 -0
- package/dist/reference-check.d.cts +10 -0
- package/dist/reference-check.d.cts.map +1 -0
- package/dist/reference-check.d.mts +10 -0
- package/dist/reference-check.d.mts.map +1 -0
- package/dist/reference-check.mjs +48 -0
- package/dist/reference-check.mjs.map +1 -0
- package/dist/remote-afs.cjs +681 -0
- package/dist/remote-afs.d.cts +236 -0
- package/dist/remote-afs.d.cts.map +1 -0
- package/dist/remote-afs.d.mts +236 -0
- package/dist/remote-afs.d.mts.map +1 -0
- package/dist/remote-afs.mjs +676 -0
- package/dist/remote-afs.mjs.map +1 -0
- package/dist/rename-guard.cjs +18 -0
- package/dist/rename-guard.mjs +18 -0
- package/dist/rename-guard.mjs.map +1 -0
- package/dist/resolve.cjs +260 -0
- package/dist/resolve.d.cts +113 -0
- package/dist/resolve.d.cts.map +1 -0
- package/dist/resolve.d.mts +113 -0
- package/dist/resolve.d.mts.map +1 -0
- package/dist/resolve.mjs +260 -0
- package/dist/resolve.mjs.map +1 -0
- package/dist/rsync-content.cjs +33 -0
- package/dist/rsync-content.mjs +33 -0
- package/dist/rsync-content.mjs.map +1 -0
- package/dist/rsync.cjs +400 -0
- package/dist/rsync.d.cts +157 -0
- package/dist/rsync.d.cts.map +1 -0
- package/dist/rsync.d.mts +157 -0
- package/dist/rsync.d.mts.map +1 -0
- package/dist/rsync.mjs +397 -0
- package/dist/rsync.mjs.map +1 -0
- package/dist/run-gc.cjs +57 -0
- package/dist/run-gc.d.cts +40 -0
- package/dist/run-gc.d.cts.map +1 -0
- package/dist/run-gc.d.mts +41 -0
- package/dist/run-gc.d.mts.map +1 -0
- package/dist/run-gc.mjs +57 -0
- package/dist/run-gc.mjs.map +1 -0
- package/dist/scope-id.cjs +70 -0
- package/dist/scope-id.d.cts +35 -0
- package/dist/scope-id.d.cts.map +1 -0
- package/dist/scope-id.d.mts +35 -0
- package/dist/scope-id.d.mts.map +1 -0
- package/dist/scope-id.mjs +68 -0
- package/dist/scope-id.mjs.map +1 -0
- package/dist/scope-migration-node.cjs +172 -0
- package/dist/scope-migration-node.d.cts +44 -0
- package/dist/scope-migration-node.d.cts.map +1 -0
- package/dist/scope-migration-node.d.mts +44 -0
- package/dist/scope-migration-node.d.mts.map +1 -0
- package/dist/scope-migration-node.mjs +171 -0
- package/dist/scope-migration-node.mjs.map +1 -0
- package/dist/scope-migration.cjs +155 -0
- package/dist/scope-migration.d.cts +65 -0
- package/dist/scope-migration.d.cts.map +1 -0
- package/dist/scope-migration.d.mts +65 -0
- package/dist/scope-migration.d.mts.map +1 -0
- package/dist/scope-migration.mjs +154 -0
- package/dist/scope-migration.mjs.map +1 -0
- package/dist/share-declaration.cjs +225 -0
- package/dist/share-declaration.d.cts +30 -0
- package/dist/share-declaration.d.cts.map +1 -0
- package/dist/share-declaration.d.mts +32 -0
- package/dist/share-declaration.d.mts.map +1 -0
- package/dist/share-declaration.mjs +221 -0
- package/dist/share-declaration.mjs.map +1 -0
- package/dist/share-hook.cjs +63 -0
- package/dist/share-hook.mjs +62 -0
- package/dist/share-hook.mjs.map +1 -0
- package/dist/share-resolve.cjs +176 -0
- package/dist/share-resolve.d.cts +36 -0
- package/dist/share-resolve.d.cts.map +1 -0
- package/dist/share-resolve.d.mts +36 -0
- package/dist/share-resolve.d.mts.map +1 -0
- package/dist/share-resolve.mjs +177 -0
- package/dist/share-resolve.mjs.map +1 -0
- package/dist/share-serve.cjs +145 -0
- package/dist/share-serve.d.cts +30 -0
- package/dist/share-serve.d.cts.map +1 -0
- package/dist/share-serve.d.mts +30 -0
- package/dist/share-serve.d.mts.map +1 -0
- package/dist/share-serve.mjs +144 -0
- package/dist/share-serve.mjs.map +1 -0
- package/dist/share-store.cjs +25 -0
- package/dist/share-store.d.cts +65 -0
- package/dist/share-store.d.cts.map +1 -0
- package/dist/share-store.d.mts +65 -0
- package/dist/share-store.d.mts.map +1 -0
- package/dist/share-store.mjs +25 -0
- package/dist/share-store.mjs.map +1 -0
- package/dist/share-virtual-path.cjs +102 -0
- package/dist/share-virtual-path.mjs +101 -0
- package/dist/share-virtual-path.mjs.map +1 -0
- package/dist/space-resolver.cjs +54 -0
- package/dist/space-resolver.d.cts +34 -0
- package/dist/space-resolver.d.cts.map +1 -0
- package/dist/space-resolver.d.mts +34 -0
- package/dist/space-resolver.d.mts.map +1 -0
- package/dist/space-resolver.mjs +55 -0
- package/dist/space-resolver.mjs.map +1 -0
- package/dist/sqlite-gc-queue.cjs +51 -0
- package/dist/sqlite-gc-queue.d.cts +22 -0
- package/dist/sqlite-gc-queue.d.cts.map +1 -0
- package/dist/sqlite-gc-queue.d.mts +22 -0
- package/dist/sqlite-gc-queue.d.mts.map +1 -0
- package/dist/sqlite-gc-queue.mjs +52 -0
- package/dist/sqlite-gc-queue.mjs.map +1 -0
- package/dist/sqlite-resolve-store.cjs +163 -0
- package/dist/sqlite-resolve-store.d.cts +50 -0
- package/dist/sqlite-resolve-store.d.cts.map +1 -0
- package/dist/sqlite-resolve-store.d.mts +50 -0
- package/dist/sqlite-resolve-store.d.mts.map +1 -0
- package/dist/sqlite-resolve-store.mjs +164 -0
- package/dist/sqlite-resolve-store.mjs.map +1 -0
- package/dist/sqlite-share-store.cjs +157 -0
- package/dist/sqlite-share-store.d.cts +31 -0
- package/dist/sqlite-share-store.d.cts.map +1 -0
- package/dist/sqlite-share-store.d.mts +31 -0
- package/dist/sqlite-share-store.d.mts.map +1 -0
- package/dist/sqlite-share-store.mjs +157 -0
- package/dist/sqlite-share-store.mjs.map +1 -0
- package/dist/sqlite-tree-index.cjs +901 -0
- package/dist/sqlite-tree-index.d.cts +107 -0
- package/dist/sqlite-tree-index.d.cts.map +1 -0
- package/dist/sqlite-tree-index.d.mts +107 -0
- package/dist/sqlite-tree-index.d.mts.map +1 -0
- package/dist/sqlite-tree-index.mjs +902 -0
- package/dist/sqlite-tree-index.mjs.map +1 -0
- package/dist/sync-engine.cjs +1324 -0
- package/dist/sync-engine.d.cts +473 -0
- package/dist/sync-engine.d.cts.map +1 -0
- package/dist/sync-engine.d.mts +473 -0
- package/dist/sync-engine.d.mts.map +1 -0
- package/dist/sync-engine.mjs +1315 -0
- package/dist/sync-engine.mjs.map +1 -0
- package/dist/sync-state.cjs +303 -0
- package/dist/sync-state.d.cts +224 -0
- package/dist/sync-state.d.cts.map +1 -0
- package/dist/sync-state.d.mts +224 -0
- package/dist/sync-state.d.mts.map +1 -0
- package/dist/sync-state.mjs +297 -0
- package/dist/sync-state.mjs.map +1 -0
- package/dist/timestamp.cjs +92 -0
- package/dist/timestamp.mjs +91 -0
- package/dist/timestamp.mjs.map +1 -0
- package/dist/transports/http.cjs +54 -0
- package/dist/transports/http.d.cts +26 -0
- package/dist/transports/http.d.cts.map +1 -0
- package/dist/transports/http.d.mts +26 -0
- package/dist/transports/http.d.mts.map +1 -0
- package/dist/transports/http.mjs +54 -0
- package/dist/transports/http.mjs.map +1 -0
- package/dist/transports/static.cjs +55 -0
- package/dist/transports/static.d.cts +28 -0
- package/dist/transports/static.d.cts.map +1 -0
- package/dist/transports/static.d.mts +28 -0
- package/dist/transports/static.d.mts.map +1 -0
- package/dist/transports/static.mjs +55 -0
- package/dist/transports/static.mjs.map +1 -0
- package/dist/transports/types.cjs +13 -0
- package/dist/transports/types.d.cts +72 -0
- package/dist/transports/types.d.cts.map +1 -0
- package/dist/transports/types.d.mts +72 -0
- package/dist/transports/types.d.mts.map +1 -0
- package/dist/transports/types.mjs +13 -0
- package/dist/transports/types.mjs.map +1 -0
- package/dist/transports/ws.cjs +207 -0
- package/dist/transports/ws.d.cts +47 -0
- package/dist/transports/ws.d.cts.map +1 -0
- package/dist/transports/ws.d.mts +47 -0
- package/dist/transports/ws.d.mts.map +1 -0
- package/dist/transports/ws.mjs +206 -0
- package/dist/transports/ws.mjs.map +1 -0
- package/dist/tree-index.cjs +333 -0
- package/dist/tree-index.d.cts +524 -0
- package/dist/tree-index.d.cts.map +1 -0
- package/dist/tree-index.d.mts +524 -0
- package/dist/tree-index.d.mts.map +1 -0
- package/dist/tree-index.mjs +322 -0
- package/dist/tree-index.mjs.map +1 -0
- package/dist/tree-query.cjs +655 -0
- package/dist/tree-query.d.cts +70 -0
- package/dist/tree-query.d.cts.map +1 -0
- package/dist/tree-query.d.mts +70 -0
- package/dist/tree-query.d.mts.map +1 -0
- package/dist/tree-query.mjs +648 -0
- package/dist/tree-query.mjs.map +1 -0
- package/dist/upload-actions.cjs +97 -0
- package/dist/upload-actions.d.cts +19 -0
- package/dist/upload-actions.d.cts.map +1 -0
- package/dist/upload-actions.d.mts +21 -0
- package/dist/upload-actions.d.mts.map +1 -0
- package/dist/upload-actions.mjs +90 -0
- package/dist/upload-actions.mjs.map +1 -0
- package/dist/upload-session-store.cjs +185 -0
- package/dist/upload-session-store.d.cts +99 -0
- package/dist/upload-session-store.d.cts.map +1 -0
- package/dist/upload-session-store.d.mts +99 -0
- package/dist/upload-session-store.d.mts.map +1 -0
- package/dist/upload-session-store.mjs +184 -0
- package/dist/upload-session-store.mjs.map +1 -0
- package/dist/url-normalize.cjs +61 -0
- package/dist/url-normalize.d.cts +33 -0
- package/dist/url-normalize.d.cts.map +1 -0
- package/dist/url-normalize.d.mts +33 -0
- package/dist/url-normalize.d.mts.map +1 -0
- package/dist/url-normalize.mjs +61 -0
- package/dist/url-normalize.mjs.map +1 -0
- package/dist/versioned-content-backends.cjs +155 -0
- package/dist/versioned-content-backends.d.cts +49 -0
- package/dist/versioned-content-backends.d.cts.map +1 -0
- package/dist/versioned-content-backends.d.mts +49 -0
- package/dist/versioned-content-backends.d.mts.map +1 -0
- package/dist/versioned-content-backends.mjs +154 -0
- package/dist/versioned-content-backends.mjs.map +1 -0
- package/dist/versioned-content-index.cjs +102 -0
- package/dist/versioned-content-index.d.cts +46 -0
- package/dist/versioned-content-index.d.cts.map +1 -0
- package/dist/versioned-content-index.d.mts +46 -0
- package/dist/versioned-content-index.d.mts.map +1 -0
- package/dist/versioned-content-index.mjs +103 -0
- package/dist/versioned-content-index.mjs.map +1 -0
- package/dist/versioned-content-store.cjs +121 -0
- package/dist/versioned-content-store.d.cts +90 -0
- package/dist/versioned-content-store.d.cts.map +1 -0
- package/dist/versioned-content-store.d.mts +90 -0
- package/dist/versioned-content-store.d.mts.map +1 -0
- package/dist/versioned-content-store.mjs +122 -0
- package/dist/versioned-content-store.mjs.map +1 -0
- package/dist/versioned-scope.cjs +39 -0
- package/dist/versioned-scope.d.cts +57 -0
- package/dist/versioned-scope.d.cts.map +1 -0
- package/dist/versioned-scope.d.mts +57 -0
- package/dist/versioned-scope.d.mts.map +1 -0
- package/dist/versioned-scope.mjs +37 -0
- package/dist/versioned-scope.mjs.map +1 -0
- package/migrations/0001_tree_entries.sql +17 -0
- package/migrations/0002_resolve_entries.sql +7 -0
- package/migrations/0003_entries_v2.sql +38 -0
- package/migrations/0004_gc_queue.sql +5 -0
- package/migrations/0006_resolve_enhance.sql +14 -0
- package/migrations/0007_resolve_created_at.sql +10 -0
- package/migrations/0008_changelog.sql +34 -0
- package/migrations/0009_device_sync_state.sql +29 -0
- package/migrations/0010_scope_meta.sql +33 -0
- package/migrations/0011_shares.sql +25 -0
- package/migrations/0012_scope_meta_file_count.sql +41 -0
- package/migrations/0013_scope_meta_total_bytes.sql +47 -0
- package/migrations/0014_rename_tables.sql +62 -0
- package/package.json +102 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-engine.mjs","names":["remaining"],"sources":["../src/sync-engine.ts"],"sourcesContent":["/**\n * SyncEngine — Phase 3 Task 3.6 (G8)\n *\n * The local-side push + pull + GC orchestrator for the DID Space sync\n * protocol. Embedded in the mobile DIDSpaceProvider (Swift + Kotlin) on\n * production builds; exposed as a standalone TypeScript class here for\n * test harness use and future desktop integration.\n *\n * ──────────────────────────────────────────────────────────────────────\n * MVP Scope (Decision 1A, round 5): the TypeScript SyncEngine is NOT\n * wired into `local.ts` DIDSpaceProvider. Desktop users continue to use\n * `afs space export/import` CLI for manual sync. This class exists so\n * tests + future desktop integration have a reference implementation to\n * build on; callers construct it with their own `RemoteUserSpaceAFS`\n * instance and a provider that implements the narrow\n * {@link SyncEngineProviderHandle} interface.\n * ──────────────────────────────────────────────────────────────────────\n *\n * sync() top-level semantics:\n *\n * 1. Acquire the per-instance mutex (concurrent sync() calls serialise).\n * 2. Read {localSeq, cloudSeq} from SyncStateStore.\n * 3. If both are 0 → fullResync() (first-sync path).\n * Otherwise → push() → pull().\n * 4. After successful legs → localChangelog.gc(), mark status=active.\n * 5. If abortSignal fires mid-way → status=idle, graceful return.\n * 6. On any other error → status=error, persist flushed cursor, re-throw.\n *\n * Echo-loop prevention has TWO layers:\n *\n * Layer I: pull() filters `.changelog` rows with `deviceId === self` so\n * the device does not re-apply its own changes as if they were\n * foreign.\n * Layer II: apply calls `provider.internalWrite/Delete` with the ORIGIN\n * device's deviceId (from the change row), NOT self. The row\n * written to local changelog records origin — so a THIRD\n * device pulling next sees a valid non-self row, not a\n * duplicated self row. Without Layer II, changes would\n * multiply through the cluster (A→B→A→B→…).\n *\n * Invariant I1 (fullResync pending paths): fullResync() MUST collect\n * `pendingPaths = set of unpushed change paths` BEFORE fetching the\n * manifest. If it collected after the manifest fetch, a local delete\n * written between pendingPaths collection and apply would get\n * REVIVED by the manifest restoration.\n */\n\nimport type {\n AFSBatchDeleteEntry,\n AFSBatchWriteEntry,\n AFSDeleteOptions,\n AFSDeleteResult,\n AFSListOptions,\n AFSListResult,\n AFSReadOptions,\n AFSReadResult,\n AFSWriteEntryPayload,\n AFSWriteOptions,\n AFSWriteResult,\n} from \"@aigne/afs\";\nimport { AFSCompactedError, AFSConflictError, makeNsLog } from \"@aigne/afs\";\nimport { firstNonEmpty } from \"@aigne/afs/utils/env\";\nimport { computeCID } from \"./cid.js\";\nimport { DOWNLOAD_MAX_ENTRIES } from \"./download-actions.js\";\nimport { DEFAULT_INLINE_THRESHOLD, decodeInlineText } from \"./inline-content.js\";\nimport type { LocalChangelog } from \"./local-changelog.js\";\nimport { detectMimeType } from \"./mime-utils.js\";\nimport {\n BatchOverBudgetError,\n BatchUnsupportedError,\n type CommitUploadEntryInput,\n DownloadForbiddenError,\n type DownloadGrant,\n type PrepareDownloadEntryInput,\n type PrepareDownloadResult,\n type PrepareUploadEntryInput,\n type PrepareUploadResult,\n type RpcBatchEntryResult,\n type RpcBatchResult,\n type UploadGrant,\n} from \"./remote-afs.js\";\nimport type { SyncStateStore } from \"./sync-state.js\";\nimport type { ChangelogEntry } from \"./tree-index.js\";\n\nconst log = makeNsLog(\"provider:did-space\");\n\n/**\n * Narrow AFS surface consumed by SyncEngine. Production wires this to a\n * full `AFS` instance (which structurally satisfies the four methods);\n * tests wire it to {@link InMemoryCloudProvider} so the engine never\n * speaks to a real HTTP transport.\n *\n * Restricting the type to four methods keeps the test surface tractable\n * (the AFS root has ~20 methods, most irrelevant to sync) and keeps the\n * \"this is the only I/O surface SyncEngine touches\" contract visible\n * at the type level.\n */\nexport interface SyncCloudAFS {\n list(path: string, options?: AFSListOptions): Promise<AFSListResult>;\n read(path: string, options?: AFSReadOptions): Promise<AFSReadResult>;\n write(\n path: string,\n payload: AFSWriteEntryPayload,\n options?: AFSWriteOptions,\n ): Promise<AFSWriteResult>;\n delete(path: string, options?: AFSDeleteOptions): Promise<AFSDeleteResult>;\n\n // ── optional batch surface (afs-rpc-batch P4) ──────────────────────────\n // Same names/shapes as the core batch methods (G7). Adapters without them\n // (or servers answering \"Unsupported op 'batch…'\", which the adapter turns\n // into BatchUnsupportedError) keep the engine on the single-op legs —\n // bit-for-bit equivalent semantics, just one round trip per row.\n batchWrite?(entries: AFSBatchWriteEntry[]): Promise<RpcBatchResult>;\n batchDelete?(entries: AFSBatchDeleteEntry[]): Promise<RpcBatchResult>;\n batchRead?(entries: Array<{ path: string; optional?: boolean }>): Promise<RpcBatchResult>;\n\n // ── optional upload plane (DID Space upload plane, design §6.1/§7.2) ─────\n // Present on adapters that speak the provider's prepare/commit/abort-upload\n // actions. The engine routes LARGE put batches through this (content out of\n // the worker) only when `uploadGrantCapability()` resolves true AND the\n // threshold is met; otherwise it stays on batchWrite (fallback, §6.1).\n uploadGrantCapability?(): Promise<boolean>;\n prepareUpload?(entries: PrepareUploadEntryInput[]): Promise<PrepareUploadResult>;\n commitUpload?(sessionId: string, entries: CommitUploadEntryInput[]): Promise<RpcBatchResult>;\n /** Out-of-band PUT of object bytes via a grant; returns the HTTP status. */\n uploadObject?(grant: UploadGrant, bytes: Uint8Array): Promise<number>;\n\n // ── optional download plane (DID Space download plane, design §6.3/§7.2) ──\n // Symmetric to the upload block. The engine routes LARGE object reads (size\n // ≥ DOWNLOAD_PLANE_MIN_BYTES) through these (content out of the worker, no\n // base64) only when `downloadGrantCapability()` resolves true; otherwise it\n // stays on batchRead (fallback, §6.3). A grant rejected 401/403 at fetch time\n // flips the whole session back to batchRead (runtime fallback-on-403, §7.3).\n downloadGrantCapability?(): Promise<boolean>;\n prepareDownload?(entries: PrepareDownloadEntryInput[]): Promise<PrepareDownloadResult>;\n /** Out-of-band GET of object bytes via a grant; returns the raw bytes. */\n downloadObject?(grant: DownloadGrant): Promise<Uint8Array>;\n}\n\n/** Client-side batch packing constants (wire-contract §9 CF tier — packing\n * to the CF table is slightly conservative against Node but always valid;\n * the client never needs to know the server runtime, design §7.3). */\nexport const BATCH_PUT_MAX_ENTRIES = 20;\nexport const BATCH_DELETE_MAX_ENTRIES = 40;\nexport const BATCH_PUT_MAX_BYTES = 1024 * 1024;\nexport const BATCH_READ_MAX_ENTRIES = 20;\nexport const BATCH_READ_MAX_BYTES = 2 * 1024 * 1024;\n\n/**\n * Push batch size. Each round, push() fetches up to this many unpushed\n * changelog rows in a single SQLite query and uploads them one by one.\n * Lower than PULL_BATCH_LIMIT because each row is a separate PUT/DELETE\n * round-trip (vs. PULL which fetches many change rows in a single call).\n */\nexport const PUSH_BATCH_LIMIT = 100;\n\n/**\n * Client-side upload-plane ACTIVATION threshold (design §6.1 / §15 decision 4).\n * The upload plane (prepare → out-of-band PUT → commit) is taken ONLY for large\n * backlogs — the Aside pain point of a ~2000-row small-record first full sync.\n * Small/common writes stay on the proven `batchWrite` path. A push uses the\n * upload plane only when its put backlog is ≥ MIN_ENTRIES (peeked) AND the\n * server advertises grant capability.\n *\n * Value 200 is deliberate, not a placeholder: lowering it does nothing for the\n * 2000 case (2000 ≥ 200 already activates) and would only pull the 100–200 band\n * onto the upload plane — MORE round-trips than batchWrite's ceil(N/20) POSTs.\n * Only lower with a measured 100–200 wall-clock crossover.\n */\nexport const AFS_DID_SPACE_UPLOAD_MIN_ENTRIES = 200;\nexport const AFS_DID_SPACE_UPLOAD_MIN_BYTES = 8 * 1024 * 1024;\n\n/**\n * Browser upload-plane SINGLE-FILE size threshold (upload-browser §4.5 / T4.2).\n * Distinct from the native-sync count/bytes batch gates above: the browser\n * orchestrator triages by individual file size — a file LARGER than this goes\n * the grant plane (bytes direct to R2, out of the worker); smaller files fall to\n * the legacy WS batchWrite. The browser orchestrator mirrors this value in its\n * inlined page JS (it cannot import this const). Conservative default — only\n * clearly-large files grant; measurement-gated, calibrated in P6.\n */\nexport const AFS_DID_SPACE_UPLOAD_MIN_SIZE = 4 * 1024 * 1024;\n\n/**\n * Upload-plane put-CHUNK caps (the load-bearing fix for Aside 2000 small writes).\n * Once the upload plane is active, each prepare/commit round packs up to this\n * many entries — DECOUPLED from `BATCH_PUT_MAX_ENTRIES` (20). Reusing the batch\n * cap would turn a 2000-entry backlog into ~100 prepare/commit rounds; at 100 it\n * is ~20. The effective chunk is `min(CHUNK_MAX_ENTRIES, PUSH_BATCH_LIMIT)`, and\n * `PUSH_BATCH_LIMIT` is also 100, so a full window is one chunk → 20 rounds for\n * 2000. The Cloudflare commit-sim probe (100 entries ≈ 425ms wall) justifies 100\n * as a safe client default; raising it past PUSH_BATCH_LIMIT is inert.\n */\nexport const AFS_DID_SPACE_UPLOAD_CHUNK_MAX_ENTRIES = 100;\nexport const AFS_DID_SPACE_UPLOAD_CHUNK_MAX_BYTES = 8 * 1024 * 1024;\n\n/**\n * Out-of-band PUT fan-out for the upload plane (independent tuning, design §3).\n * Each presigned PUT leaves the worker, so the client can run several in flight;\n * 8 is the deterministic default. Adaptive 8→4 backoff under R2 5xx/429 is a\n * follow-up, intentionally NOT in the first patch.\n */\nexport const AFS_DID_SPACE_UPLOAD_PUT_CONCURRENCY = 8;\n\n/**\n * Flush local_seq to SQLite every N successful pushes. Trade-off: lower\n * = less progress lost on crash, higher = fewer DB writes. 10 is the\n * round-trip frequency Task 3.6 spec mandates.\n */\nexport const PUSH_PROGRESS_FLUSH_INTERVAL = 10;\n\n/**\n * Pull batch size. Each `.changelog` cursor list call fetches up to this\n * many rows. Matches the server's MAX_LIMIT hard cap\n * (`services/arc-computer/src/handlers/space-api.ts` — .changelog list\n * branch inside `_createSpaceHandler`).\n */\nexport const PULL_BATCH_LIMIT = 1000;\n\n/**\n * Concurrency cap for `.objects/{cid}` reads inside pull(). Higher = faster\n * under large pulls, but risks server-side rate limits and memory\n * pressure on the client. 4 is the spec default.\n */\nexport const PULL_FETCH_CONCURRENCY = 4;\n\n/**\n * Download-plane size gate (design §6.3 / T3.5) — CALIBRATED by P4/T4.4 staging\n * measurement (2026-06-21, aside.afsd.io, logs/staging-download-plane-*.log).\n * An object whose size is ≥ this routes to the download plane (prepare-download\n * → direct R2 GET → computeCID → internalWrite), keeping its bytes out of the\n * worker; smaller objects / text / inline-cache hits stay on `batchRead` (D1\n * inline serves small text with ZERO R2 GETs — faster there). A null/missing\n * size is treated as small (never route into the plane on missing size).\n *\n * MEASURED breaking point: a single in-worker `.objects/{cid}` read (arrayBuffer\n * + base64-over-JSON) is fine at 5 MiB (~930ms) but HARD-FAILS with HTTP 503\n * (Worker exceeded resource limits) at 8 / 12 / 18 MiB — the download plane\n * served all of those correctly (8 MiB ~0.8s warm). So the worker simply CANNOT\n * read a single object ≳8 MiB; 4 MiB sits safely below that failure zone (and\n * covers the reported 5.26 MiB pain case) while keeping the common small/medium\n * objects on the locally-fast worker path.\n */\nexport const DOWNLOAD_PLANE_MIN_BYTES = 4 * 1024 * 1024; // 4 MiB (T4.4-calibrated)\n\n/**\n * Download-plane batch-count gate (design §6.3 / T3.5). Reserved placeholder for\n * the \"large batch\" route, calibrated by P4/T4.4. v1's OPERATIVE gate is the\n * per-object size gate above — a cold-start fullResync with many large files is\n * already handled by routing each big object individually; this count threshold\n * exists so P4 can tune the crossover where the plane's parallel GETs beat\n * batchRead even for medium objects, without another code change.\n */\nexport const DOWNLOAD_PLANE_MIN_ENTRIES = 64;\n\n/**\n * Out-of-band GET fan-out for the download plane (design §6.3). The client reads\n * directly from R2 and is NOT bound by the Workers 6-simultaneous-connection cap,\n * so the default is higher than the in-worker batchRead. Default 8; TS/Node may\n * override via `AFS_DID_SPACE_DOWNLOAD_GET_CONCURRENCY` (a single-point int parse\n * — NOT parseBool; the `AFS_DID_SPACE_DOWNLOAD_*` family, mirror of the shipped\n * `AFS_DID_SPACE_UPLOAD_*`). Swift/Kotlin clients use their own platform constant.\n */\nexport const DOWNLOAD_GET_CONCURRENCY_DEFAULT = 8;\n\nexport function resolveDownloadGetConcurrency(): number {\n // Only TS/Node reads env (process is absent in Workers/browser → default).\n const raw =\n typeof process !== \"undefined\" && process.env\n ? firstNonEmpty(process.env.AFS_DID_SPACE_DOWNLOAD_GET_CONCURRENCY)\n : undefined;\n const n = raw !== undefined ? Number.parseInt(raw, 10) : Number.NaN;\n return Number.isInteger(n) && n > 0 ? n : DOWNLOAD_GET_CONCURRENCY_DEFAULT;\n}\n\n/**\n * Wall-clock budget for a single sync() call. Protects against runaway\n * pulls on wildly out-of-date clients. pull() checks Date.now() against\n * the deadline at the top of every batch loop iteration.\n */\nexport const SYNC_WALL_CLOCK_BUDGET_MS = 25_000;\n\n/**\n * Narrow provider interface consumed by SyncEngine.\n *\n * The production mobile runtimes implement this directly on\n * DIDSpaceProvider; tests use a mock. Only `internalWrite`,\n * `internalDelete`, and `readBytes` are exercised — SyncEngine does\n * NOT need list/stat/search.\n *\n * `readBytes` is the push-side counterpart: SyncEngine needs the raw\n * bytes of a locally-stored cid so it can upload them via\n * `cloudAfs.write`. Tests wire this to an in-memory map.\n */\nexport interface SyncEngineProviderHandle {\n internalWrite(\n path: string,\n opts: {\n content: Uint8Array;\n deviceId: string;\n mtime?: number;\n mimeType?: string;\n /**\n * Application metadata (e.g. `createdAt`) the SyncEngine derived from the\n * bytes via `readApplicationMetadata` BEFORE calling internalWrite\n * (feed-sort §5.1). The provider stamps the local-copy ctime from\n * `metadata.createdAt` (Rule R1/R2) and persists it — it NEVER parses the\n * bytes itself. Absent ⇒ ctime falls back to the write time.\n */\n metadata?: Record<string, unknown>;\n },\n ): Promise<void>;\n internalDelete(path: string, opts: { deviceId: string; mtime?: number }): Promise<void>;\n /**\n * Fetch the raw bytes for a locally stored path. Used by push() to\n * upload the file content referenced by a `put` changelog row.\n *\n * Returning `null` signals \"the file is no longer on disk\" (e.g. the\n * user deleted it after the changelog row was written but before the\n * sync picked it up). push() treats null as \"skip this row,\n * advance cursor\" because the DELETE row that will follow is what\n * the cloud needs to see.\n */\n readBytes(path: string): Promise<Uint8Array | null>;\n /**\n * List all currently-stored file paths for the scope. Only used by\n * fullResync() to diff the cloud manifest against local state. Paths\n * returned here are NOT slash-prefixed (they match the internal\n * entries table shape); SyncEngine normalises before calling\n * internalDelete.\n */\n listAllPaths(): Promise<string[]>;\n /**\n * Derive application metadata (e.g. Aside's `createdAt`) from a file's own\n * bytes (did-space-metadata G3). The engine is format-agnostic, so the\n * provider decides how to extract it (parse the record JSON). Attached to the\n * push wire — top-level `meta` on the batchWrite leg (afs-rpc `wireMeta` lifts\n * it onto `payload.meta`), `metadata` on the upload-plane commit leg.\n *\n * OPTIONAL: providers that expose no application metadata omit it, and the\n * engine emits no metadata — preserving the pre-G3 wire contract byte-for-byte.\n * Mirrors Swift's `readApplicationMetadata` + Kotlin's equivalent (P1 lockstep).\n */\n readApplicationMetadata?(path: string, bytes: Uint8Array): Promise<Record<string, unknown>>;\n}\n\n/** Outcome of a {@link SyncEngine.sync} call. */\nexport interface SyncResult {\n /** Whether the sync completed all three legs (push, pull, gc) normally. */\n ok: boolean;\n /** Whether abortSignal fired before the engine reached its happy path. */\n aborted: boolean;\n /** Number of rows successfully pushed in the push leg. */\n pushed: number;\n /** Number of rows successfully applied in the pull leg. */\n pulled: number;\n /** Number of changelog rows GC'd at the end of the run. */\n gcDeleted: number;\n /** True when fullResync was taken (first-sync or 409 Compacted). */\n fullResynced: boolean;\n /** When the run ended, Date.now() in millis. */\n endedAt: number;\n}\n\n/**\n * Thrown by {@link SyncEngine.resolveConflict} when the server's 409\n * conflict response is missing required fields (notably `currentMtime`).\n * The LWW decision needs both timestamps, so a malformed response is\n * treated as fatal to the in-flight push leg.\n *\n * Phase 5 Task 5.1 (G11): replaced the Task 3.6 `ConflictTODOError`\n * stub with the real LWW resolver. The resolver handles every well-formed\n * conflict shape inline; this error class only fires on protocol breakage.\n */\nexport class ConflictResolutionError extends Error {\n readonly change: ChangelogEntry;\n readonly reason: string;\n constructor(change: ChangelogEntry, reason: string) {\n super(`resolveConflict on ${change.path}: ${reason}`);\n this.name = \"ConflictResolutionError\";\n this.change = change;\n this.reason = reason;\n }\n}\n\n/**\n * Thrown by {@link SyncEngine.fullResync} when the cloud-side manifest\n * snapshot exceeds the size budget the server is willing to materialise\n * in one shot. The server signals this via `result.meta.truncated === true`\n * on the `cloudAfs.list(\"\", { maxDepth: -1 })` response.\n *\n * Recovery: the caller (typically a higher-level scheduler) is expected\n * to either narrow the scope (sub-tree manifest, future feature) or\n * back off and retry once the cloud has compacted the manifest. There\n * is no automatic recovery inside SyncEngine — applying a partial\n * manifest would silently lose data.\n */\nexport class ManifestTooLargeError extends Error {\n constructor(message = \"fullResync: cloud manifest is too large to materialise in one shot\") {\n super(message);\n this.name = \"ManifestTooLargeError\";\n }\n}\n\n/** Where a current-cid 404 surfaced. */\nexport type CurrentObjectSource = \"manifest\" | \"latest-put\" | \"conflict\";\n\n/**\n * Thrown when an object read returns 404 for a cid that is the **current**\n * state of a path — design §4.7.2, the GC-go-live safety contract. Enabling\n * object GC makes a current-cid 404 possible (a GC bug or the residual\n * re-reference race); it MUST be a loud, hard error — NEVER silently treated as\n * convergence. The cursor must stay pinned (do not advance past the failing\n * change) so the next sync revisits it once the object is restored/re-pushed.\n *\n * Distinct from a **superseded**-cid 404 (an old changelog row a later same-path\n * row supersedes), which is benign — skip + advance (the Path-Y offline\n * tradeoff, §0.2). Classification: see {@link classifyMissingObject}.\n */\nexport class CurrentObjectMissingError extends Error {\n readonly path: string;\n readonly cid: string;\n readonly source: CurrentObjectSource;\n constructor(path: string, cid: string, source: CurrentObjectSource) {\n super(\n `SyncEngine: current object missing (${source}) for path ${path} cid ${cid} — ` +\n `object GC may have reclaimed a live cid; cursor left pinned for retry`,\n );\n this.name = \"CurrentObjectMissingError\";\n this.path = path;\n this.cid = cid;\n this.source = source;\n }\n}\n\n/**\n * Classify an object-read 404 during incremental apply (design §4.7.2).\n *\n * `lastCidForPath` maps each path in the batch to the cid of its LAST row\n * (max seq) — `null` if that last row is a delete. Fail-closed:\n * - cid === the path's last-row cid ⇒ \"current\" (this cid is the path's\n * final state in the batch) ⇒ hard error.\n * - path absent from the map (unknown cid, not in this batch) ⇒ \"current\"\n * (fail closed — never benign-skip something we can't prove superseded).\n * - otherwise a later same-path row supersedes this cid ⇒ \"superseded\" ⇒ skip.\n */\nexport function classifyMissingObject(\n path: string,\n cid: string,\n lastCidForPath: Map<string, string | null>,\n): \"current\" | \"superseded\" {\n if (!lastCidForPath.has(path)) return \"current\";\n return lastCidForPath.get(path) === cid ? \"current\" : \"superseded\";\n}\n\n/**\n * Conflict response shape consumed by {@link SyncEngine.resolveConflict}.\n * Mirrors the 409 body returned by Task 3.3's PUT/DELETE handlers.\n *\n * `currentCid === null` AND `currentMtime !== null` means \"the cloud has\n * deleted this path at currentMtime\"; both null means the server returned\n * a malformed response and the resolver throws ConflictResolutionError.\n */\nexport interface ConflictResponse {\n /** Server's current CID for the path (null if cloud has deleted it). */\n currentCid: string | null;\n /** Server's current mtime for the path (required for LWW comparison). */\n currentMtime: number | null;\n /** Origin device id for the cloud entry. Used for echo-loop Layer II. */\n currentDeviceId: string | null;\n}\n\n/**\n * Simple promise-based mutex used to serialise concurrent sync() calls.\n * Callers `await acquire()` to claim the lock and MUST `release()` in a\n * finally block. FIFO ordering is not guaranteed — the spec only\n * requires mutual exclusion.\n */\nclass AsyncMutex {\n private chain: Promise<void> = Promise.resolve();\n\n async acquire(): Promise<() => void> {\n let release: () => void;\n const next = new Promise<void>((resolve) => {\n release = resolve;\n });\n const prev = this.chain;\n this.chain = this.chain.then(() => next);\n await prev;\n // @ts-expect-error — release is assigned synchronously inside the executor above\n return release;\n }\n}\n\nexport interface SyncEngineOptions {\n localChangelog: LocalChangelog;\n syncStateStore: SyncStateStore;\n cloudAfs: SyncCloudAFS;\n provider: SyncEngineProviderHandle;\n /** Self deviceId: written into changelog rows (options.meta.deviceId) and used\n /// as the self filter. */\n deviceId: string;\n /**\n * Override wall-clock budget in tests. Falls back to the module-level\n * constant when not provided.\n */\n wallClockBudgetMs?: number;\n}\n\n/**\n * Local push + pull + GC orchestrator. One instance per DIDSpaceProvider /\n * per userDid. Instances are NOT thread-safe at the data level — the\n * internal mutex only serialises `sync()` calls; callers that mutate\n * local state while a sync is in flight are expected to do so via\n * `provider.internalWrite/Delete` so the changelog row is visible to the\n * next push batch.\n */\nexport class SyncEngine {\n private readonly localChangelog: LocalChangelog;\n private readonly syncStateStore: SyncStateStore;\n private readonly cloudAfs: SyncCloudAFS;\n private readonly provider: SyncEngineProviderHandle;\n private readonly selfDeviceId: string;\n private readonly wallClockBudgetMs: number;\n private readonly mutex = new AsyncMutex();\n /**\n * Per-INSTANCE capability-fallback memory (design §7.1): set the first\n * time the server answers a batch envelope with the exact\n * \"Unsupported op 'batch…'\" 400. Never set by any other 400 (e.g.\n * BATCH_OVER_BUDGET halves and retries instead).\n */\n private batchFallback = false;\n /**\n * Per-INSTANCE upload-plane capability memory (design §7.1, mirrors\n * batchFallback). null = not yet probed; set on the first `push()` that\n * reaches the threshold. `false` (Node / old server / signer-less) keeps the\n * engine on batchWrite forever for this instance.\n */\n private uploadPlaneCapable: boolean | null = null;\n /** Set per-push() when this push routes large put-chunks through the upload plane. */\n private uploadPlaneActive = false;\n /**\n * Per-INSTANCE download-plane capability memory (design §7.3, mirrors\n * uploadPlaneCapable). null = not yet probed. `false` (Node / old server /\n * signer-less) keeps the engine on batchRead forever for this instance — it is\n * NOT reset per session (unlike {@link downloadFallback}): a server that never\n * advertised the capability won't start mid-process, so re-probing every sync\n * would just waste a `/.actions` list. A server UPGRADE needs a client restart.\n */\n private downloadPlaneCapable: boolean | null = null;\n /**\n * Runtime fallback-on-403 latch (design §7.3): set when a presigned GET is\n * rejected 401/403 (token lacks GetObject / bad signature). Once set, the whole\n * sync SESSION reverts to batchRead and never re-tries the plane — so a misconfig\n * costs at most ONE 403 per object before the first hit flips it, then nothing.\n * Reset at the start of each sync() so a fixed token recovers without restart.\n */\n private downloadFallback = false;\n /** Resolved once per instance — the out-of-band GET fan-out (env-overridable). */\n private readonly downloadGetConcurrency = resolveDownloadGetConcurrency();\n\n constructor(opts: SyncEngineOptions) {\n if (!opts.deviceId) throw new Error(\"SyncEngine: deviceId is required\");\n this.localChangelog = opts.localChangelog;\n this.syncStateStore = opts.syncStateStore;\n this.cloudAfs = opts.cloudAfs;\n this.provider = opts.provider;\n this.selfDeviceId = opts.deviceId;\n this.wallClockBudgetMs = opts.wallClockBudgetMs ?? SYNC_WALL_CLOCK_BUDGET_MS;\n }\n\n /**\n * Run a single sync round — push → pull → gc. Concurrent sync() calls\n * on the same instance are serialised by the internal mutex. Always\n * returns a {@link SyncResult}; throws only on unhandled errors from\n * the underlying store/network layers.\n */\n async sync(opts: { signal?: AbortSignal } = {}): Promise<SyncResult> {\n const release = await this.mutex.acquire();\n const deadline = Date.now() + this.wallClockBudgetMs;\n // Runtime fallback-on-403 is per-SESSION (§7.3): clear it each sync round so\n // a token fixed by ops recovers the download plane WITHOUT a client restart\n // (it re-probes, hits at most one more 403 if still broken, then re-latches).\n this.downloadFallback = false;\n const result: SyncResult = {\n ok: false,\n aborted: false,\n pushed: 0,\n pulled: 0,\n gcDeleted: 0,\n fullResynced: false,\n endedAt: 0,\n };\n try {\n if (opts.signal?.aborted) {\n result.aborted = true;\n result.endedAt = Date.now();\n await this.syncStateStore.save({ syncStatus: { kind: \"idle\" } });\n return result;\n }\n await this.syncStateStore.save({ syncStatus: { kind: \"syncing\" } });\n try {\n const state = await this.syncStateStore.load();\n // First-sync path: lastSyncAt === 0 means this engine has never\n // completed a sync round before. Using lastSyncAt as the flag\n // (rather than cloudSeq/localSeq watermarks) lets tests simulate\n // a \"second sync\" after a successful first run without having\n // to stuff watermarks that would change push/pull starting\n // positions. On a real install, lastSyncAt=0 → do a fullResync\n // to seed the local store from the cloud manifest; on subsequent\n // runs, push+pull incrementally.\n if (state.lastSyncAt === 0) {\n const r = await this.fullResync({ signal: opts.signal, deadline });\n result.pulled = r.pulled;\n result.fullResynced = true;\n } else {\n const pushResult = await this.push({ signal: opts.signal, deadline });\n result.pushed = pushResult.pushed;\n const pullResult = await this.pull({ signal: opts.signal, deadline });\n result.pulled = pullResult.pulled;\n if (pullResult.fullResynced) result.fullResynced = true;\n }\n const gc = await this.localChangelog.gc();\n result.gcDeleted = gc.deleted;\n await this.syncStateStore.save({\n syncStatus: { kind: \"active\" },\n lastSyncAt: Date.now(),\n });\n result.ok = true;\n } catch (e: unknown) {\n if (opts.signal?.aborted) {\n // Graceful abort — propagate the abort state and leave the\n // engine in `idle`. The cursor in SQLite is whatever has been\n // flushed so far; the next sync() resumes from there.\n result.aborted = true;\n await this.syncStateStore.save({ syncStatus: { kind: \"idle\" } });\n return result;\n }\n const message = e instanceof Error ? e.message : String(e);\n await this.syncStateStore.save({ syncStatus: { kind: \"error\", message } });\n throw e;\n }\n } finally {\n result.endedAt = Date.now();\n release();\n }\n return result;\n }\n\n // ── push ─────────────────────────────────────────────────────────────\n\n /**\n * Upload unpushed local changes to the cloud, advancing local_seq in\n * SQLite every `PUSH_PROGRESS_FLUSH_INTERVAL` rows. On any error\n * mid-batch, flush the already-advanced cursor before re-throwing so\n * the next sync() resumes where this one left off.\n *\n * Per-row semantics:\n * - `put` row → `cloudAfs.write(path, content, {...})`\n * - `delete` row → `cloudAfs.delete(path, {...})`\n * - `put` row whose file has been deleted locally since the row was\n * written → skip (cursor advances; the following delete row will\n * tell the cloud what happened).\n * - 409 conflict → delegate to {@link SyncEngine.resolveConflict}\n * (LWW + tiebreak — see G11 in native-sync-design.md).\n */\n async push(opts: { signal?: AbortSignal; deadline: number }): Promise<{ pushed: number }> {\n let cursor = (await this.syncStateStore.load()).localSeq;\n let pushed = 0;\n let processedSinceFlush = 0;\n\n // Upload-plane decision (design §6.1 / decision 4): per-push, route large\n // put backlogs through prepare→PUT→commit (content out of the worker). The\n // decision is made ONCE here; `sendPushChunk` consults `uploadPlaneActive`.\n // Below-threshold / no-capability pushes keep the proven batchWrite path.\n this.uploadPlaneActive = await this.shouldUseUploadPlane(cursor);\n\n // Batch leg gate (P4): the adapter has the batch surface AND has not been\n // remembered as batch-unsupported. The two paths are MUTUALLY EXCLUSIVE\n // within (and across) push() loops — once the fallback trips, the whole\n // instance stays on pushOne so the cursor has a single advancing owner.\n const batchCapable = () =>\n !this.batchFallback &&\n typeof this.cloudAfs.batchWrite === \"function\" &&\n typeof this.cloudAfs.batchDelete === \"function\";\n\n try {\n while (true) {\n if (opts.signal?.aborted) return { pushed };\n if (Date.now() >= opts.deadline) return { pushed };\n\n const batch = await this.localChangelog.getUnpushed(cursor, PUSH_BATCH_LIMIT);\n if (batch.length === 0) break;\n\n if (batchCapable()) {\n const r = await this.pushBatchLeg(batch, cursor, opts);\n cursor = r.cursor;\n pushed += r.pushed;\n await this.syncStateStore.save({ localSeq: cursor });\n if (r.stop) return { pushed };\n continue; // re-fetch from cursor (single-op leg takes over if the fallback tripped)\n }\n\n for (const change of batch) {\n if (opts.signal?.aborted) return { pushed };\n if (Date.now() >= opts.deadline) return { pushed };\n\n await this.pushOne(change);\n cursor = change.seq;\n pushed += 1;\n processedSinceFlush += 1;\n\n if (processedSinceFlush >= PUSH_PROGRESS_FLUSH_INTERVAL) {\n await this.syncStateStore.save({ localSeq: cursor });\n processedSinceFlush = 0;\n }\n }\n }\n } finally {\n // Final flush — always write the last in-memory cursor, even on\n // success (the loop's per-N flush may have left a tail un-flushed).\n if (cursor > 0) {\n await this.syncStateStore.save({ localSeq: cursor });\n }\n }\n return { pushed };\n }\n\n /**\n * Batch push leg (P4, design §7.2): split the changelog window into\n * MAXIMAL same-op runs in seq order (op type never mixes inside a run —\n * put(a)→delete(a) is two runs, so same-path cross-op order rides the\n * sequential awaits); pack each run by maxEntries + Σ-decoded-bytes +\n * UNIQUE-path-per-chunk (a duplicate path cuts the chunk so the server's\n * one-shot preread stays serial-equivalent — and the SECOND chunk's\n * ifMatch sees the first chunk's result); consume results strictly in\n * order. Returns the advanced cursor; `stop` = abort/deadline/tail-skip.\n */\n private async pushBatchLeg(\n batch: ChangelogEntry[],\n cursor: number,\n opts: { signal?: AbortSignal; deadline: number },\n ): Promise<{ cursor: number; pushed: number; stop: boolean }> {\n let pushed = 0;\n\n // Maximal same-op contiguous runs in seq order.\n const runs: ChangelogEntry[][] = [];\n for (const change of batch) {\n const last = runs[runs.length - 1];\n if (last && last[0]!.op === change.op) last.push(change);\n else runs.push([change]);\n }\n\n for (const run of runs) {\n let index = 0;\n while (index < run.length) {\n if (opts.signal?.aborted || Date.now() >= opts.deadline) {\n return { cursor, pushed, stop: true };\n }\n\n // Pack one chunk: entry cap + byte cap + unique paths. Put runs routed\n // through the upload plane use the DECOUPLED chunk caps (100 / 8MB) so a\n // 2000-entry backlog packs into ~20 prepare/commit rounds instead of\n // ~100 batch-sized ones; everything else keeps the batchWrite caps.\n const putUploadPlane = run[0]!.op === \"put\" && this.uploadPlaneActive;\n const maxEntries =\n run[0]!.op !== \"put\"\n ? BATCH_DELETE_MAX_ENTRIES\n : putUploadPlane\n ? AFS_DID_SPACE_UPLOAD_CHUNK_MAX_ENTRIES\n : BATCH_PUT_MAX_ENTRIES;\n const maxBytes = putUploadPlane\n ? AFS_DID_SPACE_UPLOAD_CHUNK_MAX_BYTES\n : BATCH_PUT_MAX_BYTES;\n const chunk: Array<{ change: ChangelogEntry; bytes: Uint8Array | null }> = [];\n const seen = new Set<string>();\n let chunkBytes = 0;\n while (index < run.length && chunk.length < maxEntries) {\n const change = run[index]!;\n if (seen.has(change.path)) break; // duplicate path → cut chunk\n if (change.op === \"put\") {\n const bytes = await this.provider.readBytes(change.path);\n if (bytes === null) {\n // File vanished — same as pushOne: skip, advance cursor.\n cursor = change.seq;\n index++;\n continue;\n }\n if (chunk.length > 0 && chunkBytes + bytes.length > maxBytes) break;\n chunkBytes += bytes.length;\n chunk.push({ change, bytes });\n } else {\n chunk.push({ change, bytes: null });\n }\n seen.add(change.path);\n index++;\n }\n if (chunk.length === 0) continue;\n\n const outcome = await this.sendPushChunk(chunk);\n if (outcome.fallback) {\n // Capability fallback: NOTHING from this chunk executed (envelope\n // rejected). The caller re-fetches from `cursor` and the single-op\n // leg owns everything from here on.\n this.batchFallback = true;\n return { cursor, pushed, stop: false };\n }\n for (const consumed of outcome.consumed) {\n cursor = consumed.seq;\n if (consumed.counted) pushed += 1;\n }\n // Per-chunk durability: a throw in a LATER chunk/envelope must not\n // lose this chunk's progress (the single-op leg flushes every N rows;\n // the batch leg flushes every chunk — including the consumed prefix\n // of a chunk whose tail failed, BEFORE the error surfaces).\n if (outcome.consumed.length > 0 || outcome.error) {\n await this.syncStateStore.save({ localSeq: cursor });\n }\n if (outcome.error) throw outcome.error;\n if (outcome.tailSkipped) {\n // Clean-prefix semantics: the unconsumed tail stays unpushed; the\n // NEXT push window re-fetches from the advanced cursor.\n return { cursor, pushed, stop: false };\n }\n }\n }\n return { cursor, pushed, stop: false };\n }\n\n /**\n * Send one packed chunk, halving on envelope over-budget (down to 1 entry\n * = pushOne, the literal old behavior — bounded recursion by construction:\n * each level halves a strictly smaller chunk).\n */\n private async sendPushChunk(\n chunk: Array<{ change: ChangelogEntry; bytes: Uint8Array | null }>,\n ): Promise<{\n consumed: Array<{ seq: number; counted: boolean }>;\n tailSkipped: boolean;\n fallback: boolean;\n /** Terminal per-entry failure — rethrown by the caller AFTER the\n * consumed prefix's cursor advance is applied (no progress lost). */\n error?: Error;\n }> {\n const op = chunk[0]!.change.op;\n let result: RpcBatchResult;\n try {\n if (op === \"put\") {\n // Upload plane (decision 2/4): prepare → out-of-band PUT (commit-after-\n // 200) → commit, keeping content out of the worker. Active only for\n // large backlogs with grant capability (see push/shouldUseUploadPlane);\n // otherwise the proven batchWrite path. Returns the SAME RpcBatchResult\n // shape so the in-order consumption + conflict handling below is reused.\n result = this.uploadPlaneActive\n ? await this.uploadPlaneWriteChunk(chunk)\n : await this.cloudAfs.batchWrite!(\n await Promise.all(\n chunk.map(async ({ change, bytes }) => {\n // Application metadata (did-space-metadata G3): rides the wire\n // entry's top-level `meta`, which afs-rpc `wireMeta` lifts onto\n // `payload.meta` server-side (G1 closes the loop). Provenance\n // (deviceId/mtime) stays separate in `options.meta`.\n const appMeta = await this.readAppMeta(change.path, bytes!);\n return {\n path: normaliseWirePath(change.path),\n content: appMeta ? { content: bytes!, meta: appMeta } : { content: bytes! },\n options: {\n ...(change.old_cid ? { ifMatch: change.old_cid } : {}),\n meta: { deviceId: change.device_id, mtime: change.mtime },\n } as never,\n };\n }),\n ),\n );\n } else {\n result = await this.cloudAfs.batchDelete!(\n chunk.map(({ change }) => ({\n path: normaliseWirePath(change.path),\n options: {\n meta: {\n deviceId: change.device_id,\n mtime: change.mtime,\n ...(change.old_cid ? { ifMatch: change.old_cid } : {}),\n },\n } as never,\n })),\n );\n }\n } catch (e: unknown) {\n if (e instanceof BatchUnsupportedError) {\n return { consumed: [], tailSkipped: false, fallback: true };\n }\n if (e instanceof BatchOverBudgetError) {\n if (chunk.length === 1) {\n // Down to one entry = old behavior (single-op path for this row).\n await this.pushOne(chunk[0]!.change);\n return {\n consumed: [{ seq: chunk[0]!.change.seq, counted: true }],\n tailSkipped: false,\n fallback: false,\n };\n }\n const mid = Math.ceil(chunk.length / 2);\n const left = await this.sendPushChunk(chunk.slice(0, mid));\n if (left.fallback || left.tailSkipped || left.error) return left;\n const right = await this.sendPushChunk(chunk.slice(mid));\n return {\n consumed: [...left.consumed, ...right.consumed],\n tailSkipped: right.tailSkipped,\n fallback: right.fallback,\n error: right.error,\n };\n }\n throw e;\n }\n\n // Strictly in-order consumption (results[i] ↔ chunk[i]). The Rpc\n // transport already rejects misaligned arrays; this guard covers any\n // OTHER adapter — a misaligned response is a protocol violation that\n // must never be consumed (entries would map to the wrong outcomes).\n if (!Array.isArray(result.results) || result.results.length !== chunk.length) {\n throw new Error(\n `SyncEngine.push: batch results length ${result.results?.length ?? \"?\"} != chunk length ${chunk.length} (protocol violation)`,\n );\n }\n const consumed: Array<{ seq: number; counted: boolean }> = [];\n for (let i = 0; i < result.results.length; i++) {\n const r = result.results[i]!;\n const change = chunk[i]!.change;\n if (r.success) {\n consumed.push({ seq: change.seq, counted: true });\n continue;\n }\n if (r.code === \"BATCH_TAIL_SKIPPED\") {\n // Entries from here on were NOT executed — leave them to the next\n // window (cursor stays before them).\n return { consumed, tailSkipped: true, fallback: false };\n }\n if (r.conflict) {\n // resolveConflict handles the row (LWW force-push or local adopt);\n // single-op parity: a conflict-RESOLVED row counts as pushed.\n await this.resolveConflict(change, r.conflict);\n consumed.push({ seq: change.seq, counted: true });\n continue;\n }\n // Any other per-entry failure: stop at the last consumed row — the\n // cursor pins here and the next sync retries from this change. The\n // error is RETURNED (not thrown) so the consumed prefix still advances.\n return {\n consumed,\n tailSkipped: false,\n fallback: false,\n error: new Error(\n `SyncEngine.push: batch entry failed at seq=${change.seq} path=${change.path}: ${r.error ?? r.code ?? r.status}`,\n ),\n };\n }\n return { consumed, tailSkipped: false, fallback: false };\n }\n\n /**\n * Decide whether THIS push routes large put-chunks through the upload plane\n * (design §6.1 / decision 4). True only when (a) the adapter has the\n * prepare/commit/uploadObject surface, (b) the server advertises grant\n * capability (memoized per instance — Node/old-server/signer-less → false →\n * never), and (c) the put backlog meets the conservative threshold\n * (≥ MIN_ENTRIES puts, or ≥ MIN_BYTES) via a cheap bounded peek.\n */\n private async shouldUseUploadPlane(cursor: number): Promise<boolean> {\n if (\n typeof this.cloudAfs.prepareUpload !== \"function\" ||\n typeof this.cloudAfs.commitUpload !== \"function\" ||\n typeof this.cloudAfs.uploadObject !== \"function\"\n ) {\n return false;\n }\n if (this.uploadPlaneCapable === null) {\n try {\n this.uploadPlaneCapable =\n typeof this.cloudAfs.uploadGrantCapability === \"function\"\n ? await this.cloudAfs.uploadGrantCapability()\n : false;\n } catch {\n this.uploadPlaneCapable = false; // probe failure → stay on batchWrite\n }\n }\n if (!this.uploadPlaneCapable) return false;\n // Cheap bounded peek: MIN_ENTRIES+1 rows is enough to decide both gates.\n const peek = await this.localChangelog.getUnpushed(\n cursor,\n AFS_DID_SPACE_UPLOAD_MIN_ENTRIES + 1,\n );\n const puts = peek.filter((c) => c.op === \"put\");\n if (puts.length >= AFS_DID_SPACE_UPLOAD_MIN_ENTRIES) return true;\n const bytes = puts.reduce((sum, c) => sum + (c.size ?? 0), 0);\n return bytes >= AFS_DID_SPACE_UPLOAD_MIN_BYTES;\n }\n\n /**\n * Upload-plane transport for one put chunk (design §6.1 / §8). Mirrors a\n * `batchWrite` call but moves content OUT of the worker:\n * 1. prepare-upload (reuses each row's local `cid` — never re-hashed);\n * 2. out-of-band PUT each grant (bounded concurrency);\n * 3. commit-upload ONLY the entries whose PUT returned 200 (commit-after-\n * 200, decision 2 — the sole dangling defense). PUT-failed entries get a\n * per-entry failure so the cursor pins there for retry next round.\n * Returns the SAME `RpcBatchResult` shape `sendPushChunk` consumes. If a grant\n * is unexpectedly absent (capability was advertised but a row got none), bail\n * via `BatchUnsupportedError` → the engine reverts to the legacy path.\n */\n /**\n * Application metadata for a push entry, derived from its own bytes via the\n * optional provider hook (did-space-metadata G3). Returns undefined when the\n * provider exposes none — so the engine emits no metadata and the wire stays\n * pre-G3 byte-identical.\n */\n private async readAppMeta(\n path: string,\n bytes: Uint8Array,\n ): Promise<Record<string, unknown> | undefined> {\n const read = this.provider.readApplicationMetadata;\n if (!read) return undefined;\n try {\n // Fail-soft (mirror Swift's `try?`): a provider hook that throws on an\n // unparseable / binary file must NOT crash the whole push — just emit no\n // metadata for this entry. The provider SHOULD return {} itself, but a\n // buggy one can't take down the sync leg.\n const meta = await read.call(this.provider, path, bytes);\n return meta && Object.keys(meta).length > 0 ? meta : undefined;\n } catch {\n return undefined;\n }\n }\n\n private async uploadPlaneWriteChunk(\n chunk: Array<{ change: ChangelogEntry; bytes: Uint8Array | null }>,\n ): Promise<RpcBatchResult> {\n // Recompute the cid from the bytes we are ACTUALLY uploading — NOT the\n // changelog row's recorded `cid`. The recorded cid can be stale: if a file\n // is rewritten after its changelog row was recorded (e.g. an\n // attachment-manifest repair that bypasses the changelog), the row keeps the\n // pre-rewrite cid while `readBytes` returns the post-rewrite bytes.\n // prepare-upload pins `x-amz-checksum-sha256` to whatever cid we send here;\n // a stale one makes R2 reject the out-of-band PUT with `BadDigest`, and\n // because the stale row carries the smallest seq it is retried first and\n // never succeeds — permanently wedging the push. Recomputing over the real\n // bytes mirrors the server's batchWrite leg (which runs computeCID over the\n // received bytes) and is self-healing for already-wedged clients. INVARIANT:\n // every upload-plane (put) chunk entry has non-null bytes — the chunk\n // builder skips a put whose readBytes returned null — so `bytes!` is sound.\n const cids = await Promise.all(chunk.map(({ bytes }) => computeCID(bytes!)));\n\n const toEntry = (\n change: ChangelogEntry,\n size: number,\n cid: string,\n ): PrepareUploadEntryInput => {\n const wirePath = normaliseWirePath(change.path);\n // The changelog carries no mime, so path-detect it on the PREPARE entry\n // (inline-content-commit §5.3): the session persists `mime_type`, the\n // commit lands it on `treeEntry.mimeType`, and it gates the inline copy.\n const mimeType = detectMimeType(wirePath);\n return {\n path: wirePath,\n cid,\n size,\n ...(mimeType ? { mimeType } : {}),\n ...(change.old_cid ? { ifMatch: change.old_cid } : {}),\n deviceId: change.device_id,\n mtime: change.mtime,\n };\n };\n\n // Inline-text candidate for the COMMIT leg (inline-content-commit §5.3): the\n // ≤2KB client bound (== server DEFAULT_INLINE_THRESHOLD, §5.4) AND text-mime\n // AND strict-UTF-8 (decodeInlineText fails closed on a lossy decode). The\n // bytes are already local from the chunk — no re-read. >2KB / binary /\n // invalid-UTF-8 → no content (server reads/queries from the object instead).\n const inlineCandidate = (\n bytes: Uint8Array,\n mimeType: string | undefined,\n ): string | undefined => {\n if (bytes.length > DEFAULT_INLINE_THRESHOLD) return undefined;\n return decodeInlineText(bytes, mimeType) ?? undefined;\n };\n\n const prep = await this.cloudAfs.prepareUpload!(\n chunk.map(({ change, bytes }, i) => toEntry(change, bytes!.length, cids[i]!)),\n );\n if (prep.entries.length !== chunk.length || prep.entries.some((e) => !e.grant)) {\n // Server advertised capability but didn't issue a grant per entry — bail\n // to the legacy path rather than risk a dangling commit.\n throw new BatchUnsupportedError();\n }\n\n // (2) out-of-band PUT, bounded concurrency. commit-after-200.\n const putOk = new Array<boolean>(chunk.length).fill(false);\n await this.boundedAll(chunk.length, AFS_DID_SPACE_UPLOAD_PUT_CONCURRENCY, async (i) => {\n const status = await this.cloudAfs.uploadObject!(prep.entries[i]!.grant!, chunk[i]!.bytes!);\n putOk[i] = status === 200;\n });\n\n // (3) commit ONLY the 200'd entries, preserving chunk order. `commitIndex`\n // maps a chunk index → its position in the commit result array (O(1) lookup).\n const commitEntries: CommitUploadEntryInput[] = [];\n const commitIndex = new Map<number, number>();\n for (const [i, { change, bytes }] of chunk.entries()) {\n if (!putOk[i]) continue;\n commitIndex.set(i, commitEntries.length);\n const base = toEntry(change, bytes!.length, cids[i]!);\n const content = inlineCandidate(bytes!, base.mimeType);\n // Application metadata (did-space-metadata G3): the commit leg carries it\n // explicitly (exec args are forwarded verbatim — no wireMeta lift here).\n const metadata = await this.readAppMeta(change.path, bytes!);\n commitEntries.push({\n ...base,\n ...(content !== undefined ? { content } : {}),\n ...(metadata ? { metadata } : {}),\n });\n }\n const commit =\n commitEntries.length > 0\n ? await this.cloudAfs.commitUpload!(prep.sessionId, commitEntries)\n : ({ results: [], succeeded: 0, failed: 0 } as RpcBatchResult);\n\n // Align results to chunk order: committed entries take their commit result;\n // PUT-failed entries get a failure (NOT committed → retried next window).\n const results: RpcBatchEntryResult[] = chunk.map(({ change }, i) => {\n const ci = commitIndex.get(i);\n if (ci !== undefined) {\n return (\n commit.results[ci] ?? {\n path: normaliseWirePath(change.path),\n success: false,\n status: 500,\n error: \"missing commit result\",\n }\n );\n }\n return {\n path: normaliseWirePath(change.path),\n success: false,\n status: 503,\n code: \"UPLOAD_PUT_FAILED\",\n error: \"presigned PUT did not return 200\",\n };\n });\n let succeeded = 0;\n for (const r of results) if (r.success) succeeded++;\n return { results, succeeded, failed: results.length - succeeded };\n }\n\n /** Run `fn(0..n-1)` with at most `limit` in flight (push-side PUT fan-out). */\n private async boundedAll(\n n: number,\n limit: number,\n fn: (i: number) => Promise<void>,\n ): Promise<void> {\n let next = 0;\n const workers = Array.from({ length: Math.min(limit, n) }, async () => {\n while (next < n) {\n const i = next++;\n await fn(i);\n }\n });\n await Promise.all(workers);\n }\n\n /**\n * Upload a single change row. Split from push() so both the normal\n * loop and future retry paths can call it.\n *\n * On 409 conflict, delegates to {@link resolveConflict}, which decides\n * by LWW + tiebreak whether to force-overwrite the cloud or adopt the\n * cloud's version locally. Either way, after resolveConflict returns,\n * the row is considered \"handled\" and the push cursor advances past it.\n */\n private async pushOne(change: ChangelogEntry): Promise<void> {\n const wirePath = normaliseWirePath(change.path);\n if (change.op === \"put\") {\n if (change.cid === null) {\n throw new Error(\n `SyncEngine.push: put row with null cid at seq=${change.seq} path=${change.path}`,\n );\n }\n const bytes = await this.provider.readBytes(change.path);\n if (bytes === null) {\n // File vanished between the changelog row and the push — the\n // next `delete` row will communicate the vanishment to the\n // cloud. Skip with cursor advance.\n return;\n }\n // feed-sort §5.1: single-op push (the chunk==1-over-budget / 409-fallback\n // edge path that bypasses batchWrite) carries app createdAt so the cloud\n // ctime matches. RpcUserSpaceAFS.write lifts these app fields onto the\n // wire body top-level `meta`; `extra` stays sync provenance.\n const appMeta = await this.readAppMeta(change.path, bytes);\n try {\n await this.cloudAfs.write(wirePath, {\n content: bytes,\n meta: {\n ...appMeta,\n extra: {\n contentCid: change.cid,\n deviceId: change.device_id,\n mtime: change.mtime,\n ifMatch: change.old_cid ?? undefined,\n },\n },\n });\n } catch (e: unknown) {\n if (e instanceof AFSConflictError) {\n await this.resolveConflict(change, {\n currentCid: e.currentCid,\n currentMtime: e.currentMtime,\n currentDeviceId: e.currentDeviceId,\n });\n return;\n }\n throw e;\n }\n } else if (change.op === \"delete\") {\n try {\n await this.cloudAfs.delete(wirePath, {\n meta: {\n deviceId: change.device_id,\n mtime: change.mtime,\n ifMatch: change.old_cid ?? undefined,\n },\n });\n } catch (e: unknown) {\n if (e instanceof AFSConflictError) {\n await this.resolveConflict(change, {\n currentCid: e.currentCid,\n currentMtime: e.currentMtime,\n currentDeviceId: e.currentDeviceId,\n });\n return;\n }\n throw e;\n }\n } else {\n throw new Error(`SyncEngine.push: unexpected op=${change.op} at seq=${change.seq}`);\n }\n }\n\n // ── resolveConflict (G11 LWW) ────────────────────────────────────────\n\n /**\n * Resolve a push-side 409 conflict via Last-Write-Wins (LWW).\n *\n * Decision matrix (see `native-sync-design.md` G11 for the full 7-row\n * matrix and rationale):\n *\n * 1. `localMtime > cloudMtime` → **local wins**: force-overwrite the\n * cloud (resend without `If-Match`). The server still validates\n * `X-Content-CID`, so a corrupted upload still fails.\n *\n * 2. `localMtime < cloudMtime` → **cloud wins**: adopt the cloud's\n * version locally via `provider.internalWrite/Delete` with the\n * ORIGIN deviceId from the conflict response. Echo-loop Layer II.\n *\n * 3. `localMtime === cloudMtime` → **tiebreak by CID lexicographic\n * order** (round 7 decision). The larger cid wins. delete ops\n * treat their cid as the empty string, so a `put` with non-empty\n * cid always beats a `delete` at the same mtime.\n *\n * Special cases handled here:\n * - delete vs delete (both null cid) → idempotent no-op (defensive;\n * in practice the server's delete handler turns DELETE 404 into\n * success and the conflict never reaches here).\n * - same mtime + same cid → idempotent natural convergence (the\n * local-wins path is taken but the resend produces the same cid\n * the cloud already has).\n *\n * mtime skew defense: G5.c (Task 3.3) rejects writes with\n * `mtime > serverNow + 5min` at the header layer, so a single device\n * with a wildly bad clock cannot poison the LWW decision globally.\n */\n async resolveConflict(change: ChangelogEntry, resp: ConflictResponse): Promise<void> {\n if (resp.currentMtime === null) {\n throw new ConflictResolutionError(change, \"missing currentMtime in conflict response\");\n }\n const localMtime = change.mtime;\n const cloudMtime = resp.currentMtime;\n\n // Defensive: delete vs delete + same mtime → idempotent. The server\n // delete handler should already short-circuit this (404 → success),\n // but resolveConflict guards against a misbehaving handler.\n if (change.op === \"delete\" && resp.currentCid === null) {\n return;\n }\n\n // Tiebreak: cid lexicographic order. delete ops contribute the empty\n // string for their cid, so non-empty put cid > \"\" → put wins ties.\n const localCid = change.cid ?? \"\";\n const cloudCid = resp.currentCid ?? \"\";\n const localWins = localMtime > cloudMtime || (localMtime === cloudMtime && localCid > cloudCid);\n\n if (localWins) {\n // Local wins: force-overwrite the cloud. Resend without If-Match\n // (unconditional) — the server still validates X-Content-CID, so\n // a corrupted upload still fails.\n const wirePath = normaliseWirePath(change.path);\n if (change.op === \"put\") {\n const bytes = await this.provider.readBytes(change.path);\n if (bytes === null) {\n // File vanished between the changelog row and the resolve —\n // the next delete row will tell the cloud what happened.\n return;\n }\n // feed-sort §5.1: conflict-resend (local-wins force-overwrite) also\n // carries app createdAt so the cloud ctime is preserved on overwrite.\n const appMeta = await this.readAppMeta(change.path, bytes);\n await this.cloudAfs.write(wirePath, {\n content: bytes,\n meta: {\n ...appMeta,\n extra: {\n contentCid: change.cid!,\n deviceId: change.device_id,\n mtime: change.mtime,\n // ifMatch omitted → unconditional overwrite\n },\n },\n });\n } else {\n await this.cloudAfs.delete(wirePath, {\n meta: {\n deviceId: change.device_id,\n mtime: change.mtime,\n // ifMatch omitted → unconditional delete\n },\n });\n }\n } else {\n // Cloud wins: adopt the cloud's version locally. Use ORIGIN\n // deviceId so the local changelog row this write creates carries\n // foreign provenance (Layer II echo prevention). When a third\n // device later pulls, it sees the cloud's row, not ours.\n const wirePath = normaliseWirePath(change.path);\n const originDeviceId = resp.currentDeviceId ?? \"unknown\";\n if (resp.currentCid !== null) {\n // Cloud has a current put (could be a fresh write or a put that\n // beats our local delete — resurrect semantics). `currentCid` is the\n // cloud's CURRENT state, so a 404 here is a current-object loss\n // (§4.7.2) — surface as a hard error rather than writing empty bytes.\n let bytes: Uint8Array | undefined;\n try {\n const readResult = await this.cloudAfs.read(`/.objects/${resp.currentCid}`);\n bytes = readResult.data?.content as Uint8Array | undefined;\n } catch (e: unknown) {\n if (e instanceof Error && e.name === \"AbortError\") throw e;\n throw new CurrentObjectMissingError(wirePath, resp.currentCid, \"conflict\");\n }\n if (bytes === undefined) {\n throw new CurrentObjectMissingError(wirePath, resp.currentCid, \"conflict\");\n }\n await this.provider.internalWrite(wirePath, {\n content: bytes,\n deviceId: originDeviceId,\n mtime: cloudMtime,\n // feed-sort §5.1: derive app createdAt from the bytes so the local\n // copy's ctime matches the origin (else it'd fall back to now).\n metadata: await this.readAppMeta(wirePath, bytes),\n });\n } else {\n // Cloud has deleted this path. (cloud delete vs local put case —\n // local delete vs cloud delete is short-circuited above.)\n await this.provider.internalDelete(wirePath, {\n deviceId: originDeviceId,\n mtime: cloudMtime,\n });\n }\n }\n }\n\n // ── pull ─────────────────────────────────────────────────────────────\n\n /**\n * Fetch and apply incoming changes from the cloud. Loops until either\n * the server reports `hasMore === false`, the deadline passes, or the\n * abort signal fires. On 409 Compacted, delegates to fullResync().\n *\n * Apply order: sequential in seq order within a batch (SQLite\n * integrity + single origin-deviceId per row). Content fetching is\n * parallel up to `PULL_FETCH_CONCURRENCY`.\n */\n async pull(opts: {\n signal?: AbortSignal;\n deadline: number;\n }): Promise<{ pulled: number; fullResynced: boolean }> {\n // Cursor is opaque to SyncEngine: load the persisted numeric value\n // from sync_state and convert to string for the AFS boundary, then\n // round-trip whatever the cloud sends back unchanged. The persistence\n // layer parses the cursor as a number for monotonic comparison, but\n // SyncEngine itself never inspects the structure — that's the §0.5\n // Decision #3 opaque-cursor contract.\n const initialCloudSeq = (await this.syncStateStore.load()).cloudSeq;\n let cloudCursor: string | undefined = initialCloudSeq > 0 ? String(initialCloudSeq) : undefined;\n let pulled = 0;\n\n try {\n while (true) {\n if (opts.signal?.aborted) return { pulled, fullResynced: false };\n if (Date.now() >= opts.deadline) return { pulled, fullResynced: false };\n\n let listResult: AFSListResult;\n try {\n listResult = await this.cloudAfs.list(\".changelog\", {\n cursor: cloudCursor ?? \"0\",\n limit: PULL_BATCH_LIMIT,\n });\n } catch (e: unknown) {\n if (e instanceof AFSCompactedError) {\n // Compaction watermark moved past our cursor — fall back to\n // full resync. The outer try/finally still writes cloudSeq\n // based on what was applied here, but fullResync() overwrites\n // it with the manifestSeq.\n const r = await this.fullResync({ signal: opts.signal, deadline: opts.deadline });\n return { pulled: pulled + r.pulled, fullResynced: true };\n }\n throw e;\n }\n\n // Parse changelog entries from the AFS list result. Each entry's\n // protocol-level fields live under `meta.extra` (matching the\n // server's `?op=list` `.changelog` response convention — see\n // tasks.md §2.2.1 line 2.14 for the canonical key list).\n type RemoteChange = {\n seq: number;\n originalPath: string;\n op: string;\n cid: string | null;\n oldCid: string | null;\n size: number | null;\n mtime: number;\n deviceId: string;\n };\n const allChanges: RemoteChange[] = listResult.data.map((entry) => {\n const extra = (entry.meta?.extra ?? {}) as Record<string, unknown>;\n return {\n seq: Number(extra.seq ?? 0),\n // T2.3: canonical `extra.originalPath` only — the legacy `extra.path`\n // fallback is dropped now the server emits originalPath (T1.1).\n originalPath: String(extra.originalPath ?? \"\"),\n op: String(extra.op ?? \"\"),\n cid: typeof extra.cid === \"string\" ? extra.cid : null,\n oldCid: typeof extra.oldCid === \"string\" ? extra.oldCid : null,\n size: typeof extra.size === \"number\" ? extra.size : null,\n mtime: Number(extra.mtime ?? 0),\n deviceId: String(extra.deviceId ?? \"\"),\n };\n });\n const hasMore = listResult.meta?.hasMore ?? false;\n // Defense in depth: the server already sorts ORDER BY seq ASC\n // (.changelog list branch), but the apply loop advances per-row,\n // so a non-monotone batch would corrupt the cursor. Reject\n // out-of-order responses proactively.\n for (let i = 1; i < allChanges.length; i++) {\n if (allChanges[i]!.seq <= allChanges[i - 1]!.seq) {\n throw new Error(\"SyncEngine.pull: cloud returned out-of-order changes\");\n }\n }\n\n // Layer I echo-loop prevention: drop rows this device produced.\n const foreignChanges = allChanges.filter((c) => c.deviceId !== this.selfDeviceId);\n\n // Fetch content for all `put` rows in parallel (bounded by\n // concurrency). Delete rows need no content. We fetch into a\n // map so the serial apply below does zero network I/O.\n const putRows = foreignChanges.filter((c) => c.op === \"put\" && c.cid !== null);\n const cidMap = new Map<string, Uint8Array>();\n await this.fetchContent(putRows, cidMap, opts);\n\n // §4.7.2 classification input: per-path cid of the LAST row (max seq)\n // in this batch. foreignChanges are already seq-ascending (asserted\n // above), so a straight assignment leaves the final write per path.\n const lastCidForPath = new Map<string, string | null>();\n for (const c of foreignChanges) {\n lastCidForPath.set(normaliseAfsPath(c.originalPath), c.op === \"put\" ? c.cid : null);\n }\n\n // Serial apply in seq order — order matters for two rows on the\n // same path (write then delete must apply in that order).\n for (const change of foreignChanges) {\n if (opts.signal?.aborted) return { pulled, fullResynced: false };\n\n const wirePath = normaliseAfsPath(change.originalPath);\n if (change.op === \"put\") {\n if (change.cid === null) continue;\n const bytes = cidMap.get(change.cid);\n if (bytes === undefined) {\n // `.objects/{cid}` read 404 (object GC may have reclaimed it).\n // Classify (§4.7.2): a CURRENT-cid 404 is a hard error — the\n // cursor MUST stay pinned (we throw BEFORE advancing it) so the\n // next sync revisits this change once the object is restored.\n // A SUPERSEDED-cid 404 is benign — a later same-path row replaces\n // it (Path-Y offline tradeoff) → skip + advance.\n if (classifyMissingObject(wirePath, change.cid, lastCidForPath) === \"current\") {\n throw new CurrentObjectMissingError(wirePath, change.cid, \"latest-put\");\n }\n log.warn(\n `SyncEngine.pull: skipping superseded seq=${change.seq} path=${wirePath} (cid ${change.cid} reclaimed)`,\n );\n cloudCursor = String(change.seq);\n continue;\n }\n // Layer II echo-loop prevention: deviceId is ORIGIN (not\n // self), so the local changelog row this write creates\n // carries the origin provenance. The next pull from a\n // third device sees a valid non-self row, not a\n // duplicated self row.\n await this.provider.internalWrite(wirePath, {\n content: bytes,\n deviceId: change.deviceId,\n mtime: change.mtime,\n // feed-sort §5.1: local-copy ctime tracks the origin createdAt.\n metadata: await this.readAppMeta(wirePath, bytes),\n });\n } else if (change.op === \"delete\") {\n await this.provider.internalDelete(wirePath, {\n deviceId: change.deviceId,\n mtime: change.mtime,\n });\n }\n cloudCursor = String(change.seq);\n pulled += 1;\n }\n\n // Empty foreign batch but server has more rows → advance cursor\n // to the server-reported high water mark so we don't re-poll the\n // same self rows forever. The cursor stays opaque — we just trust\n // whatever the server returned.\n if (foreignChanges.length === 0 && allChanges.length > 0) {\n cloudCursor = listResult.meta?.cursor ?? cloudCursor;\n }\n\n if (!hasMore) break;\n }\n } finally {\n // Persist the opaque cursor as a number into sync_state. Phase 2\n // keeps sync_state.cloud_seq as Int64-shaped storage so the\n // monotonic-write semantics are preserved; the InMemoryCloudProvider\n // and the production server both encode cursors as decimal-string\n // sequence numbers. If a future server emits a non-numeric cursor,\n // sync_state will need an opaque-string column too — see §0.5 D3.\n if (cloudCursor !== undefined) {\n const numericCursor = Number(cloudCursor);\n if (Number.isFinite(numericCursor) && numericCursor > 0) {\n await this.syncStateStore.save({ cloudSeq: numericCursor });\n }\n }\n }\n return { pulled, fullResynced: false };\n }\n\n /**\n * Fetch content bytes for a batch of `put` rows in parallel, bounded\n * by {@link PULL_FETCH_CONCURRENCY}. Results land in `cidMap`;\n * individual fetch failures (404, cid mismatch) are logged and the\n * offending cid is omitted so the caller's serial apply pass can\n * detect the gap and skip.\n */\n /**\n * Content fetch dispatcher (P4): chunked `batchRead(optional:true)` when\n * the adapter has it (≤20 cids and ≤2MB ESTIMATED bytes per chunk — size\n * from the changelog rows), single concurrent reads otherwise. Tail-\n * skipped cids are simply re-queued for the next chunk (they were never\n * read); `missing:true` cids stay out of the map so the apply loop's\n * §4.7.2 current/superseded classification is untouched.\n */\n private async fetchContent(\n rows: Array<{ cid: string | null; size: number | null }>,\n cidMap: Map<string, Uint8Array>,\n opts: { signal?: AbortSignal },\n ): Promise<void> {\n // Download plane FIRST: route big objects (≥ MIN_BYTES) out of the worker.\n // Everything else — small objects, Node grant:null, post-403 fallback —\n // flows to batchRead below (small text is cheapest there, 0 R2 GETs §6.3).\n const planeSatisfied = await this.fillBytesViaDownloadPlane(\n rows.filter((r): r is { cid: string; size: number | null } => r.cid !== null),\n cidMap,\n opts,\n );\n const remaining =\n planeSatisfied.size === 0\n ? rows\n : rows.filter((r) => r.cid === null || !planeSatisfied.has(r.cid));\n\n if (this.batchFallback || typeof this.cloudAfs.batchRead !== \"function\") {\n return this.fetchContentConcurrent(remaining, cidMap, opts);\n }\n\n // Unique cids with a size estimate (first row wins — same cid = same size).\n const sizeByCid = new Map<string, number>();\n for (const r of remaining) {\n if (r.cid !== null && !sizeByCid.has(r.cid)) sizeByCid.set(r.cid, r.size ?? 0);\n }\n const queue = Array.from(sizeByCid.keys());\n\n while (queue.length > 0) {\n if (opts.signal?.aborted) return;\n\n const chunk: string[] = [];\n let estBytes = 0;\n while (queue.length > 0 && chunk.length < BATCH_READ_MAX_ENTRIES) {\n const cid = queue[0]!;\n const est = sizeByCid.get(cid) ?? 0;\n if (chunk.length > 0 && estBytes + est > BATCH_READ_MAX_BYTES) break;\n chunk.push(cid);\n estBytes += est;\n queue.shift();\n }\n\n let result: RpcBatchResult;\n try {\n result = await this.cloudAfs.batchRead(\n chunk.map((cid) => ({ path: `/.objects/${cid}`, optional: true })),\n );\n } catch (e: unknown) {\n if (e instanceof BatchUnsupportedError) {\n this.batchFallback = true;\n const remaining = [...chunk, ...queue].map((cid) => ({\n cid,\n size: sizeByCid.get(cid) ?? null,\n }));\n return this.fetchContentConcurrent(remaining, cidMap, opts);\n }\n if (e instanceof BatchOverBudgetError && chunk.length > 1) {\n // Read the first half singly; the outer loop re-packs the rest.\n const half = Math.ceil(chunk.length / 2);\n for (const cid of chunk.slice(0, half)) {\n await this.fetchOneObject(cid, cidMap, opts);\n }\n queue.unshift(...chunk.slice(half));\n continue;\n }\n throw e;\n }\n\n let skippedTail = false;\n for (let i = 0; i < result.results.length; i++) {\n const r = result.results[i]!;\n const cid = chunk[i]!;\n if (r.success) {\n const content = r.data?.content;\n const bytes =\n content instanceof Uint8Array\n ? content\n : typeof content === \"string\"\n ? new TextEncoder().encode(content)\n : undefined;\n if (bytes) {\n const actualCid = await computeCID(bytes);\n if (actualCid !== cid) {\n log.warn(`SyncEngine.pull: cid mismatch for ${cid}: got ${actualCid}`);\n } else {\n cidMap.set(cid, bytes);\n }\n }\n continue;\n }\n if (r.code === \"BATCH_TAIL_SKIPPED\") {\n // Never read — retry in the next chunk. Guard: if the FIRST entry\n // of a chunk is tail-skipped the server made no progress; fall\n // back to single reads for the remainder to avoid a tight loop.\n if (i === 0) {\n for (const c of chunk.slice(i)) await this.fetchOneObject(c, cidMap, opts);\n } else {\n queue.unshift(...chunk.slice(i));\n }\n skippedTail = true;\n break;\n }\n // missing:true / other failures → leave out of the map (apply loop\n // classifies current vs superseded — §4.7.2 unchanged).\n }\n if (skippedTail) continue;\n }\n }\n\n /** Single `.objects/{cid}` read with the same soft-failure semantics. */\n private async fetchOneObject(\n cid: string,\n cidMap: Map<string, Uint8Array>,\n opts: { signal?: AbortSignal },\n ): Promise<void> {\n if (opts.signal?.aborted) return;\n try {\n const readResult = await this.cloudAfs.read(`/.objects/${cid}`);\n const bytes = readResult.data?.content as Uint8Array | undefined;\n if (bytes) {\n const actualCid = await computeCID(bytes);\n if (actualCid === cid) cidMap.set(cid, bytes);\n else log.warn(`SyncEngine.pull: cid mismatch for ${cid}: got ${actualCid}`);\n }\n } catch (e: unknown) {\n if (e instanceof Error && e.name === \"AbortError\") throw e;\n log.warn(\n `SyncEngine.pull: read .objects/${cid} failed: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n }\n\n private async fetchContentConcurrent(\n rows: Array<{ cid: string | null }>,\n cidMap: Map<string, Uint8Array>,\n opts: { signal?: AbortSignal },\n ): Promise<void> {\n const uniqueCids = new Set<string>();\n for (const r of rows) {\n if (r.cid !== null) uniqueCids.add(r.cid);\n }\n const cids = Array.from(uniqueCids);\n let index = 0;\n const worker = async (): Promise<void> => {\n while (index < cids.length) {\n if (opts.signal?.aborted) return;\n const myIdx = index++;\n const cid = cids[myIdx]!;\n try {\n const readResult = await this.cloudAfs.read(`/.objects/${cid}`);\n const bytes = readResult.data?.content as Uint8Array | undefined;\n if (bytes) {\n const actualCid = await computeCID(bytes);\n if (actualCid !== cid) {\n log.warn(`SyncEngine.pull: cid mismatch for ${cid}: got ${actualCid}`);\n } else {\n cidMap.set(cid, bytes);\n }\n }\n } catch (e: unknown) {\n // Skip the cid on any per-fetch failure — the serial apply\n // loop will detect the absence and log a warning. Abort\n // errors must propagate, though.\n if (e instanceof Error && e.name === \"AbortError\") throw e;\n log.warn(\n `SyncEngine.pull: read .objects/${cid} failed: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n }\n };\n const concurrency = Math.max(1, Math.min(PULL_FETCH_CONCURRENCY, cids.length));\n const workers: Array<Promise<void>> = [];\n for (let i = 0; i < concurrency; i++) workers.push(worker());\n await Promise.all(workers);\n }\n\n // ── download plane (design §6.3 / §7.2 / §7.3) ──────────────────────────\n\n /**\n * Whether the download plane can be used RIGHT NOW: the adapter speaks the\n * methods, the server advertised the INDEPENDENT capability (memoized per\n * instance, §7.3), and this session hasn't latched a 401/403 fallback.\n */\n private async downloadPlaneAvailable(): Promise<boolean> {\n if (this.downloadFallback) return false;\n if (\n typeof this.cloudAfs.prepareDownload !== \"function\" ||\n typeof this.cloudAfs.downloadObject !== \"function\"\n ) {\n return false;\n }\n if (this.downloadPlaneCapable === null) {\n try {\n this.downloadPlaneCapable =\n typeof this.cloudAfs.downloadGrantCapability === \"function\"\n ? await this.cloudAfs.downloadGrantCapability()\n : false;\n } catch {\n this.downloadPlaneCapable = false; // probe failure → stay on batchRead\n }\n }\n return this.downloadPlaneCapable === true;\n }\n\n /**\n * Fetch LARGE objects (size ≥ {@link DOWNLOAD_PLANE_MIN_BYTES}) via the\n * download plane — prepare-download → bounded-concurrency direct R2 GET →\n * computeCID → `bytesMap`. Bytes leave R2 directly (never through the worker /\n * base64). Returns the SET of cids it satisfied; the caller routes the rest\n * (small objects, Node `grant:null`, forbidden, 404, post-fallback) through\n * batchRead — so a true miss still raises the existing fail-closed path.\n *\n * fail-closed: a byte string whose `computeCID` ≠ the requested cid (R2 served\n * tampered/other bytes) is DROPPED, never written (§8.1). A 401/403 latches\n * `downloadFallback` for the whole session — at most one wall-bang (§7.3).\n */\n private async fillBytesViaDownloadPlane(\n entries: Array<{ cid: string; size: number | null }>,\n bytesMap: Map<string, Uint8Array>,\n opts: { signal?: AbortSignal },\n ): Promise<Set<string>> {\n const satisfied = new Set<string>();\n if (!(await this.downloadPlaneAvailable())) return satisfied;\n\n // Size gate (§6.3): only BIG objects go to the plane; dedupe by cid. A\n // null/0 size (missing wire field, empty object) is < MIN_BYTES → stays on\n // batchRead (which serves small/inline objects cheaper, with 0 R2 GETs).\n const bigCids: string[] = [];\n const seen = new Set<string>();\n for (const e of entries) {\n if ((e.size ?? 0) < DOWNLOAD_PLANE_MIN_BYTES) continue;\n if (seen.has(e.cid)) continue;\n seen.add(e.cid);\n bigCids.push(e.cid);\n }\n if (bigCids.length === 0) return satisfied;\n\n for (let i = 0; i < bigCids.length; i += DOWNLOAD_MAX_ENTRIES) {\n if (opts.signal?.aborted || this.downloadFallback) break;\n const batch = bigCids.slice(i, i + DOWNLOAD_MAX_ENTRIES);\n let prep: PrepareDownloadResult;\n try {\n prep = await this.cloudAfs.prepareDownload!(batch.map((cid) => ({ cid })));\n } catch (e: unknown) {\n if (e instanceof BatchUnsupportedError) {\n // Old server lost the action mid-session → no capability; bail to batchRead.\n this.downloadPlaneCapable = false;\n return satisfied;\n }\n throw e;\n }\n // Index grants by cid (output is input-aligned, but be robust). Only\n // status:\"ok\" entries with a grant are signable; grant:null (Node) /\n // forbidden / invalid are left unsatisfied → fall to batchRead.\n const grantByCid = new Map<string, DownloadGrant>();\n for (const pe of prep.entries) {\n if (pe.status === \"ok\" && pe.grant) grantByCid.set(pe.cid, pe.grant);\n }\n const grantedCids = batch.filter((c) => grantByCid.has(c));\n\n await this.boundedAll(grantedCids.length, this.downloadGetConcurrency, async (j) => {\n if (opts.signal?.aborted || this.downloadFallback) return;\n const cid = grantedCids[j]!;\n try {\n const bytes = await this.cloudAfs.downloadObject!(grantByCid.get(cid)!);\n const actualCid = await computeCID(bytes);\n if (actualCid === cid) {\n bytesMap.set(cid, bytes);\n satisfied.add(cid);\n } else {\n log.warn(`SyncEngine.pull: download-plane cid mismatch for ${cid}: got ${actualCid}`);\n }\n } catch (e: unknown) {\n if (e instanceof DownloadForbiddenError) {\n // Latch the whole session to batchRead (§7.3) — no repeated banging.\n this.downloadFallback = true;\n return;\n }\n if (e instanceof Error && e.name === \"AbortError\") throw e;\n // 404 (dangling) / network → leave unsatisfied; the caller's missing-\n // object path (soft skip for incremental, CurrentObjectMissingError for\n // fullResync) takes over.\n }\n });\n }\n return satisfied;\n }\n\n // ── fullResync ───────────────────────────────────────────────────────\n\n /**\n * First-sync / post-compaction recovery path. Fetches the full manifest\n * and reconciles the local store against it, skipping paths the local\n * changelog still has unpushed changes for (invariant I1).\n *\n * ORDERING IS CRITICAL — `pendingPaths` MUST be collected BEFORE the\n * manifest fetch so a delete written between pendingPaths collection\n * and apply does not get reverted by the manifest restoration.\n */\n async fullResync(opts: { signal?: AbortSignal; deadline: number }): Promise<{ pulled: number }> {\n // I1: collect pendingPaths FIRST. Anything in this set is \"the\n // client has a locally-written row that has not yet been pushed to\n // the cloud\" — we must not let the cloud's stale view clobber it.\n const unpushed = await this.localChangelog.getUnpushed(0, Number.MAX_SAFE_INTEGER);\n const pendingPaths = new Set(unpushed.map((c) => c.path));\n\n if (opts.signal?.aborted) return { pulled: 0 };\n\n // Snapshot the cloud manifest at the aside fragment ROOT. Phase 5\n // (mobile-didspace-transport): the cloud is the aside `/user` fragment —\n // the production cloudAfs adapter addresses it via `/user`, so the manifest\n // is just the fragment root (\"\"), not the old whole-space subtree // (no /files, no aside/ wrapper). `/files` + the `aside/` wrapper are both gone (files-prefix-removal).\n const manifestResult = await this.cloudAfs.list(\"\", { maxDepth: -1 });\n if (manifestResult.meta?.truncated === true) {\n throw new ManifestTooLargeError();\n }\n // Each manifest entry exposes the canonical {path, cid, mtime} via\n // top-level fields (path, meta.cid) plus sync-specific extras under\n // meta.extra. mtime lives under extra so it stays consistent with\n // the .changelog wire shape.\n const manifestEntries = manifestResult.data.map((entry) => {\n const extra = (entry.meta?.extra ?? {}) as Record<string, unknown>;\n // T2.3: read the canonical `extra.cid` (all-runtime position), falling\n // back to top-level `meta.cid` for forward/backward safety while the\n // additive top-level cid is still emitted (T1.2). Non-breaking cleanup —\n // the server sends both, so this flip works against old and new wire.\n const cid =\n typeof extra.cid === \"string\"\n ? extra.cid\n : typeof entry.meta?.cid === \"string\"\n ? (entry.meta.cid as string)\n : \"\";\n return {\n path: entry.path,\n cid,\n mtime: typeof extra.mtime === \"number\" ? extra.mtime : 0,\n // T3.4 (Finding 3): the server ALREADY emits size — but at TOP-LEVEL\n // `meta.size` (serializeManifestEntry), NOT under `extra` (which only\n // carries {cid, mtime}). This parser had dropped it, defeating the size\n // gate. Read `entry.meta.size`; a missing/non-number size → 0 → the gate\n // conservatively keeps the object on batchRead.\n size: typeof entry.meta?.size === \"number\" ? entry.meta.size : 0,\n };\n });\n const manifestSeq = Number(manifestResult.meta?.cursor ?? 0);\n\n // Build a set of manifest paths (in the LOCAL tree-path form — no\n // leading slash) so we can compute the local-only set for deletion.\n const manifestPathSet = new Set<string>();\n for (const e of manifestEntries) {\n manifestPathSet.add(normaliseToLocalPath(e.path));\n }\n\n // Apply manifest entries serially, skipping pendingPaths. Object reads\n // are CHUNKED through batchRead when the adapter has it (P4 — fullResync\n // was the worst serial offender: 1 + N round trips), falling back to the\n // original per-entry read otherwise. A missing manifest cid is ALWAYS a\n // hard error (§4.7.2) — `optional:true` only softens the wire envelope,\n // and the miss is re-raised as CurrentObjectMissingError here.\n let pulled = 0;\n const applicable = manifestEntries.filter(\n (entry) => !pendingPaths.has(normaliseToLocalPath(entry.path)),\n );\n\n // Download plane pre-pass (T3.5): fetch big objects (≥ MIN_BYTES) out of the\n // worker — THE cold-start痛点 (5.26MB → 1102/44.7s). Un-satisfied big objects\n // (404 / grant:null / post-403 fallback) flow into the batchRead loop below,\n // where a true manifest miss raises CurrentObjectMissingError (fail-closed).\n const planeBytes = new Map<string, Uint8Array>();\n const planeSatisfied = await this.fillBytesViaDownloadPlane(\n applicable.map((e) => ({ cid: e.cid, size: e.size })),\n planeBytes,\n opts,\n );\n for (const entry of applicable) {\n if (opts.signal?.aborted) return { pulled };\n if (Date.now() >= opts.deadline) return { pulled };\n const bytes = planeBytes.get(entry.cid);\n if (!bytes) continue;\n await this.provider.internalWrite(normaliseAfsPath(entry.path), {\n content: bytes,\n deviceId: this.selfDeviceId,\n mtime: entry.mtime,\n // feed-sort §5.1: full-resync (download-plane leg) — local-copy ctime\n // from the origin createdAt, else big items sort by reset time.\n metadata: await this.readAppMeta(normaliseAfsPath(entry.path), bytes),\n });\n pulled += 1;\n }\n // The rest go through batchRead (small objects + any plane-unsatisfied bigs).\n const viaBatch =\n planeSatisfied.size === 0 ? applicable : applicable.filter((e) => !planeSatisfied.has(e.cid));\n\n // Recomputed EVERY iteration: a BatchUnsupportedError mid-loop flips\n // this.batchFallback, and a stale snapshot here would re-enter the batch\n // branch forever (caught by the old-server proxy E2E — 4.4M requests).\n const useBatchRead = () => !this.batchFallback && typeof this.cloudAfs.batchRead === \"function\";\n\n for (let offset = 0; offset < viaBatch.length; ) {\n if (opts.signal?.aborted) return { pulled };\n if (Date.now() >= opts.deadline) return { pulled };\n\n // Pack the next chunk — manifest rows carry no size estimate, so the\n // entry cap alone bounds it (response overshoot stays bounded by the\n // server's own encoded-byte tail truncation).\n const chunk: typeof viaBatch = [];\n while (\n offset < viaBatch.length &&\n chunk.length < (useBatchRead() ? BATCH_READ_MAX_ENTRIES : 1)\n ) {\n chunk.push(viaBatch[offset]!);\n offset++;\n }\n\n const bytesByCid = new Map<string, Uint8Array>();\n if (useBatchRead() && chunk.length > 0) {\n let result: RpcBatchResult;\n try {\n result = await this.cloudAfs.batchRead!(\n chunk.map((entry) => ({ path: `/.objects/${entry.cid}`, optional: true })),\n );\n } catch (e: unknown) {\n if (e instanceof BatchUnsupportedError) {\n this.batchFallback = true;\n offset -= chunk.length; // re-process this chunk on the single path\n continue;\n }\n throw e;\n }\n for (let i = 0; i < result.results.length; i++) {\n const r = result.results[i]!;\n const entry = chunk[i]!;\n if (r.code === \"BATCH_TAIL_SKIPPED\") {\n // Not read — re-process from this entry in the next chunk.\n offset -= chunk.length - i;\n break;\n }\n if (!r.success) {\n throw new CurrentObjectMissingError(entry.path, entry.cid, \"manifest\");\n }\n const content = r.data?.content;\n const bytes =\n content instanceof Uint8Array\n ? content\n : typeof content === \"string\"\n ? new TextEncoder().encode(content)\n : undefined;\n if (!bytes) throw new CurrentObjectMissingError(entry.path, entry.cid, \"manifest\");\n bytesByCid.set(entry.cid, bytes);\n }\n } else {\n for (const entry of chunk) {\n try {\n const readResult = await this.cloudAfs.read(`/.objects/${entry.cid}`);\n const content = readResult.data?.content as Uint8Array | undefined;\n if (!content) throw new CurrentObjectMissingError(entry.path, entry.cid, \"manifest\");\n bytesByCid.set(entry.cid, content);\n } catch (e: unknown) {\n if (e instanceof CurrentObjectMissingError) throw e;\n if (e instanceof Error && e.name === \"AbortError\") throw e;\n // A 404 (or any read failure) on a manifest cid is a current-object loss.\n throw new CurrentObjectMissingError(entry.path, entry.cid, \"manifest\");\n }\n }\n }\n\n for (const entry of chunk) {\n if (opts.signal?.aborted) return { pulled };\n const bytes = bytesByCid.get(entry.cid);\n if (!bytes) continue; // tail-skipped — re-processed next round\n const wirePath = normaliseAfsPath(entry.path);\n await this.provider.internalWrite(wirePath, {\n content: bytes,\n // fullResync has no per-row deviceId (manifest entries don't\n // carry one). Use the self id with the understanding that the\n // devicing is about provenance for future pulls — since this\n // is a reset, a self-provenance row is fine: subsequent pulls\n // filter self rows out anyway.\n deviceId: this.selfDeviceId,\n mtime: entry.mtime,\n // feed-sort §5.1: full-resync (batch leg) — the large-collection path\n // (count-gate ≥200); without this the local ctime falls back to reset\n // time. Local-copy ctime tracks the origin createdAt.\n metadata: await this.readAppMeta(wirePath, bytes),\n });\n pulled += 1;\n }\n }\n\n // Delete local entries that are NOT in the manifest AND NOT in\n // pendingPaths. Two-pass diff: list local → subtract manifest →\n // subtract pending → delete.\n if (!opts.signal?.aborted) {\n const localPaths = await this.provider.listAllPaths();\n for (const p of localPaths) {\n if (opts.signal?.aborted) break;\n if (manifestPathSet.has(p)) continue;\n if (pendingPaths.has(p)) continue;\n await this.provider.internalDelete(normaliseAfsPath(p), {\n deviceId: this.selfDeviceId,\n });\n }\n }\n\n // Persist the new cloud watermark. Use non-monotonic save? — no,\n // save() is monotonic for cloudSeq, and manifestSeq is guaranteed\n // ≥ whatever we had before (manifests are taken from the same\n // changelog snapshot). If manifestSeq is somehow lower (weird\n // server state), the save is a no-op, which is safe.\n if (manifestSeq > 0) {\n await this.syncStateStore.save({ cloudSeq: manifestSeq });\n }\n\n return { pulled };\n }\n}\n\n// ── helpers ────────────────────────────────────────────────────────────\n\n/**\n * Normalise an internal changelog path (no leading slash) to the AFS\n * wire path used by PUT/DELETE endpoints (always starts with `/`).\n */\nfunction normaliseWirePath(path: string): string {\n return path.startsWith(\"/\") ? path : `/${path}`;\n}\n\n/**\n * Same as {@link normaliseWirePath} but applied to paths that may come\n * from either the manifest (no leading slash) or a `.changelog` row's\n * `meta.extra.originalPath` (no leading slash either — the server stores\n * without leading slash). Always returns a `/`-prefixed path suitable\n * for `provider.internalWrite/Delete`.\n */\nfunction normaliseAfsPath(path: string): string {\n return path.startsWith(\"/\") ? path : `/${path}`;\n}\n\n/**\n * Inverse of {@link normaliseAfsPath}: strip the leading slash so the\n * path matches the `entries.path` / `changelog.path` column shape\n * (used by pendingPaths lookups).\n */\nfunction normaliseToLocalPath(path: string): string {\n return path.startsWith(\"/\") ? path.slice(1) : path;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
const require_changelog_schema = require('./changelog-schema.cjs');
|
|
2
|
+
|
|
3
|
+
//#region src/sync-state.ts
|
|
4
|
+
/**
|
|
5
|
+
* SyncStateStore — Phase 3 Task 3.5 (G7)
|
|
6
|
+
*
|
|
7
|
+
* Typed key-value façade over the `sync_state` table introduced by
|
|
8
|
+
* Task 3.4. Where {@link LocalChangelog} owns the bare minimum of the
|
|
9
|
+
* `{scope}:local_seq` and `{scope}:cloud_seq` keys for changelog GC,
|
|
10
|
+
* SyncStateStore exposes the FULL set of progress fields the local
|
|
11
|
+
* SyncEngine needs:
|
|
12
|
+
*
|
|
13
|
+
* - device_id (per-user, NOT scope-prefixed)
|
|
14
|
+
* - {scope}:local_seq
|
|
15
|
+
* - {scope}:cloud_seq
|
|
16
|
+
* - {scope}:last_sync_at
|
|
17
|
+
* - {scope}:sync_status
|
|
18
|
+
*
|
|
19
|
+
* The class is intentionally a sibling of LocalChangelog rather than a
|
|
20
|
+
* refactor of it. Both operate on the same SQLite `sync_state` table and
|
|
21
|
+
* share monotonic semantics for `local_seq` / `cloud_seq`; cross-class
|
|
22
|
+
* parity is asserted by tests so the SyncEngine can mix the two without
|
|
23
|
+
* surprises. Refactoring LocalChangelog into a thin wrapper around
|
|
24
|
+
* SyncStateStore is a future optimisation that Task 3.6 (SyncEngine) can
|
|
25
|
+
* decide to make if it becomes ergonomic.
|
|
26
|
+
*
|
|
27
|
+
* Platform notes:
|
|
28
|
+
* - TypeScript / desktop: device_id is stored ONLY in this SQLite table.
|
|
29
|
+
* There is no Keychain equivalent on desktop, so the file IS the SoT.
|
|
30
|
+
* - Swift / iOS: Keychain is the SoT; this SQLite cache is rebuilt
|
|
31
|
+
* on startup.
|
|
32
|
+
* - Kotlin / Android: AndroidKeystore + EncryptedSharedPreferences is
|
|
33
|
+
* the SoT; SQLite is again the cache.
|
|
34
|
+
*
|
|
35
|
+
* The two mobile runtimes implement their own SyncStateStore + DeviceIDStore
|
|
36
|
+
* pair under `swift/Sources/AFSDIDSpace/` and
|
|
37
|
+
* `kotlin/afs-didspace/.../io/aigne/afs/didspace/`. The TypeScript class
|
|
38
|
+
* here matches their public surface as closely as the language allows.
|
|
39
|
+
*/
|
|
40
|
+
/** Default `AppState` returned by load() when nothing is persisted yet. */
|
|
41
|
+
function defaultAppState() {
|
|
42
|
+
return {
|
|
43
|
+
deviceId: "",
|
|
44
|
+
cloudSeq: 0,
|
|
45
|
+
localSeq: 0,
|
|
46
|
+
lastSyncAt: 0,
|
|
47
|
+
syncStatus: { kind: "idle" }
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Registry of the raw `sync_state` keys this class manages. Exported so
|
|
52
|
+
* tests and the eventual SyncEngine can introspect the wire format without
|
|
53
|
+
* hard-coding strings.
|
|
54
|
+
*
|
|
55
|
+
* Keys marked `scoped` are written as `{scope}:{name}`. The `device_id`
|
|
56
|
+
* key is unique among the registry — it is per-user and intentionally NOT
|
|
57
|
+
* scope-prefixed so a single device sharing two DID Space scopes still
|
|
58
|
+
* presents the same identity to both.
|
|
59
|
+
*/
|
|
60
|
+
const SYNC_STATE_KEYS = {
|
|
61
|
+
deviceId: {
|
|
62
|
+
name: "device_id",
|
|
63
|
+
scoped: false
|
|
64
|
+
},
|
|
65
|
+
localSeq: {
|
|
66
|
+
name: "local_seq",
|
|
67
|
+
scoped: true
|
|
68
|
+
},
|
|
69
|
+
cloudSeq: {
|
|
70
|
+
name: "cloud_seq",
|
|
71
|
+
scoped: true
|
|
72
|
+
},
|
|
73
|
+
lastSyncAt: {
|
|
74
|
+
name: "last_sync_at",
|
|
75
|
+
scoped: true
|
|
76
|
+
},
|
|
77
|
+
syncStatus: {
|
|
78
|
+
name: "sync_status",
|
|
79
|
+
scoped: true
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Serialise a {@link SyncStatus} to its on-disk string form.
|
|
84
|
+
*
|
|
85
|
+
* Format (matches the spec acceptance criteria):
|
|
86
|
+
* - `idle` → `"idle"`
|
|
87
|
+
* - `syncing` → `"syncing"`
|
|
88
|
+
* - `active` → `"active"`
|
|
89
|
+
* - `error:{message}` → `"error:{message}"`
|
|
90
|
+
*
|
|
91
|
+
* The `error` branch preserves the full message verbatim. Newlines and
|
|
92
|
+
* colons inside the message are tolerated; parsing is greedy on the prefix
|
|
93
|
+
* `error:` so the remainder of the string is the message regardless of
|
|
94
|
+
* what punctuation it contains.
|
|
95
|
+
*/
|
|
96
|
+
function serializeSyncStatus(status) {
|
|
97
|
+
switch (status.kind) {
|
|
98
|
+
case "idle": return "idle";
|
|
99
|
+
case "syncing": return "syncing";
|
|
100
|
+
case "active": return "active";
|
|
101
|
+
case "error": return `error:${status.message}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Parse a `sync_status` string back into a {@link SyncStatus}. Throws on
|
|
106
|
+
* unrecognised forms — this matches LocalChangelog's "corrupt value =
|
|
107
|
+
* surface the bug" stance from Task 3.4.
|
|
108
|
+
*/
|
|
109
|
+
function parseSyncStatus(raw) {
|
|
110
|
+
if (raw === "idle") return { kind: "idle" };
|
|
111
|
+
if (raw === "syncing") return { kind: "syncing" };
|
|
112
|
+
if (raw === "active") return { kind: "active" };
|
|
113
|
+
if (raw.startsWith("error:")) return {
|
|
114
|
+
kind: "error",
|
|
115
|
+
message: raw.slice(6)
|
|
116
|
+
};
|
|
117
|
+
throw new Error(`SyncStateStore: corrupt sync_status value: ${raw}`);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Strict device_id format regex. Must match the server-side validator in
|
|
121
|
+
* `services/arc-computer/src/handlers/validate-headers.ts` so locally
|
|
122
|
+
* generated identifiers round-trip through the cloud sync endpoints
|
|
123
|
+
* unchanged. Format: `<class>:<platform>:<suffix>`.
|
|
124
|
+
*
|
|
125
|
+
* Duplicated locally so this module has zero cross-package imports — the
|
|
126
|
+
* source of truth lives in `validate-headers.ts` and any change there must
|
|
127
|
+
* be mirrored here AND in the Swift/Kotlin equivalents.
|
|
128
|
+
*/
|
|
129
|
+
const DEVICE_ID_FORMAT_RE = /^(aside|desktop|server):[a-z0-9-]+:[a-z0-9-]+$/;
|
|
130
|
+
/** Validate a device_id string. Returns true on match. */
|
|
131
|
+
function isValidDeviceId(deviceId) {
|
|
132
|
+
return DEVICE_ID_FORMAT_RE.test(deviceId);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Generate a fresh device_id for the current TypeScript desktop runtime.
|
|
136
|
+
* Format: `desktop:ts:<uuid v4>`. Tests can pass an injected UUID to
|
|
137
|
+
* keep generation deterministic.
|
|
138
|
+
*/
|
|
139
|
+
function generateDesktopDeviceId(uuid) {
|
|
140
|
+
return `desktop:ts:${uuid ?? crypto.randomUUID()}`;
|
|
141
|
+
}
|
|
142
|
+
var SyncStateStore = class {
|
|
143
|
+
client;
|
|
144
|
+
scope;
|
|
145
|
+
constructor(opts) {
|
|
146
|
+
if (typeof opts.scope !== "string" || opts.scope.length === 0) throw new Error("SyncStateStore: scope must be a non-empty string");
|
|
147
|
+
this.client = opts.client;
|
|
148
|
+
this.scope = opts.scope;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Idempotently create the `sync_state` table. MetadataDB.open() and the
|
|
152
|
+
* Swift/Kotlin SQLiteTreeIndex.migrate() already create it; this static
|
|
153
|
+
* helper exists for test fixtures that build a SyncStateStore on a raw
|
|
154
|
+
* libsql client. Reuses the canonical DDL from `changelog-schema.ts` so
|
|
155
|
+
* three-runtime parity is preserved.
|
|
156
|
+
*/
|
|
157
|
+
static async ensureTable(client) {
|
|
158
|
+
await client.execute(require_changelog_schema.SYNC_STATE_TABLE_SQL);
|
|
159
|
+
for (const sql of require_changelog_schema.SYNC_STATE_INDEX_SQL) await client.execute(sql);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Build the on-disk key for a given AppState field. Per-scope fields
|
|
163
|
+
* are prefixed with `{scope}:`; the per-user `device_id` field is the
|
|
164
|
+
* raw bare name. Exposed as a private helper (rather than inlined) so
|
|
165
|
+
* the parity tests can introspect the rule via SYNC_STATE_KEYS.
|
|
166
|
+
*/
|
|
167
|
+
keyFor(field) {
|
|
168
|
+
const meta = SYNC_STATE_KEYS[field];
|
|
169
|
+
return meta.scoped ? `${this.scope}:${meta.name}` : meta.name;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Read the full {@link AppState} for this scope. Missing fields fall
|
|
173
|
+
* back to the defaults from {@link defaultAppState}.
|
|
174
|
+
*
|
|
175
|
+
* If `device_id` is absent, this method generates a fresh one using
|
|
176
|
+
* {@link generateDesktopDeviceId} and persists it back to the same
|
|
177
|
+
* SQLite cache before returning. Subsequent loads will see the same
|
|
178
|
+
* value (this is the "TS desktop has no Keychain — file IS the SoT"
|
|
179
|
+
* branch from the spec).
|
|
180
|
+
*
|
|
181
|
+
* Corrupt values throw — corrupt `cloud_seq` / `local_seq` / `last_sync_at`
|
|
182
|
+
* non-numeric, corrupt `sync_status` not in the recognised set, corrupt
|
|
183
|
+
* `device_id` failing the strict regex.
|
|
184
|
+
*/
|
|
185
|
+
async load() {
|
|
186
|
+
const state = defaultAppState();
|
|
187
|
+
const result = await this.client.execute({
|
|
188
|
+
sql: `SELECT key, value FROM ds_sync_state WHERE key IN (?, ?, ?, ?, ?)`,
|
|
189
|
+
args: [
|
|
190
|
+
this.keyFor("deviceId"),
|
|
191
|
+
this.keyFor("cloudSeq"),
|
|
192
|
+
this.keyFor("localSeq"),
|
|
193
|
+
this.keyFor("lastSyncAt"),
|
|
194
|
+
this.keyFor("syncStatus")
|
|
195
|
+
]
|
|
196
|
+
});
|
|
197
|
+
const rows = /* @__PURE__ */ new Map();
|
|
198
|
+
for (const row of result.rows) {
|
|
199
|
+
const k = row[0];
|
|
200
|
+
const v = row[1];
|
|
201
|
+
if (typeof k === "string" && (typeof v === "string" || typeof v === "number")) rows.set(k, String(v));
|
|
202
|
+
}
|
|
203
|
+
const rawDeviceId = rows.get(this.keyFor("deviceId"));
|
|
204
|
+
if (rawDeviceId !== void 0) {
|
|
205
|
+
if (!isValidDeviceId(rawDeviceId)) throw new Error(`SyncStateStore: corrupt device_id value: ${rawDeviceId}`);
|
|
206
|
+
state.deviceId = rawDeviceId;
|
|
207
|
+
} else {
|
|
208
|
+
const fresh = generateDesktopDeviceId();
|
|
209
|
+
await this.writeKey(this.keyFor("deviceId"), fresh);
|
|
210
|
+
state.deviceId = fresh;
|
|
211
|
+
}
|
|
212
|
+
state.cloudSeq = parseSeqValue(rows.get(this.keyFor("cloudSeq")), "cloud_seq");
|
|
213
|
+
state.localSeq = parseSeqValue(rows.get(this.keyFor("localSeq")), "local_seq");
|
|
214
|
+
state.lastSyncAt = parseSeqValue(rows.get(this.keyFor("lastSyncAt")), "last_sync_at");
|
|
215
|
+
const rawStatus = rows.get(this.keyFor("syncStatus"));
|
|
216
|
+
if (rawStatus !== void 0) state.syncStatus = parseSyncStatus(rawStatus);
|
|
217
|
+
return state;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Persist a partial update to the {@link AppState}. Each field is
|
|
221
|
+
* written via INSERT OR REPLACE on its individual key; concurrent
|
|
222
|
+
* save() calls touching disjoint fields cannot lose data.
|
|
223
|
+
*
|
|
224
|
+
* Monotonic fields:
|
|
225
|
+
* - `cloudSeq` and `localSeq` use the same CASE expression as
|
|
226
|
+
* {@link LocalChangelog.markPushed}, so a save() with a lower value
|
|
227
|
+
* than the current watermark is silently ignored. This keeps a
|
|
228
|
+
* SyncEngine + LocalChangelog mixed-write path safe.
|
|
229
|
+
*
|
|
230
|
+
* Non-monotonic fields:
|
|
231
|
+
* - `lastSyncAt`, `syncStatus`, `deviceId` are written as-is. The
|
|
232
|
+
* state machine (`idle → syncing → active → idle` etc.) and the
|
|
233
|
+
* `last_sync_at` clock have valid backwards transitions in some
|
|
234
|
+
* edge cases (test reset, manual rollback) and we must not block
|
|
235
|
+
* them.
|
|
236
|
+
*
|
|
237
|
+
* Unknown fields are caught at compile time by `Partial<AppState>`.
|
|
238
|
+
* Defense-in-depth: at runtime we iterate the explicit list of fields
|
|
239
|
+
* we know how to serialise rather than `Object.keys(update)`.
|
|
240
|
+
*/
|
|
241
|
+
async save(update) {
|
|
242
|
+
if (update.deviceId !== void 0) {
|
|
243
|
+
if (!isValidDeviceId(update.deviceId)) throw new Error(`SyncStateStore: refusing to save invalid device_id: ${update.deviceId}`);
|
|
244
|
+
await this.writeKey(this.keyFor("deviceId"), update.deviceId);
|
|
245
|
+
}
|
|
246
|
+
if (update.cloudSeq !== void 0) await this.writeKeyMonotonic(this.keyFor("cloudSeq"), update.cloudSeq);
|
|
247
|
+
if (update.localSeq !== void 0) await this.writeKeyMonotonic(this.keyFor("localSeq"), update.localSeq);
|
|
248
|
+
if (update.lastSyncAt !== void 0) {
|
|
249
|
+
const safe = Math.max(0, Math.floor(Number.isFinite(update.lastSyncAt) ? update.lastSyncAt : 0));
|
|
250
|
+
await this.writeKey(this.keyFor("lastSyncAt"), String(safe));
|
|
251
|
+
}
|
|
252
|
+
if (update.syncStatus !== void 0) await this.writeKey(this.keyFor("syncStatus"), serializeSyncStatus(update.syncStatus));
|
|
253
|
+
}
|
|
254
|
+
/** Plain INSERT OR REPLACE for non-monotonic keys. */
|
|
255
|
+
async writeKey(key, value) {
|
|
256
|
+
await this.client.execute({
|
|
257
|
+
sql: `INSERT INTO ds_sync_state (key, value) VALUES (?, ?)
|
|
258
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
259
|
+
args: [key, value]
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Monotonic INSERT OR REPLACE for integer keys. Mirror of the helper in
|
|
264
|
+
* {@link LocalChangelog.writeSeqKeyMonotonic} so the two classes converge
|
|
265
|
+
* on the same on-disk representation regardless of which one performs
|
|
266
|
+
* the write.
|
|
267
|
+
*/
|
|
268
|
+
async writeKeyMonotonic(key, seq) {
|
|
269
|
+
const safe = Math.max(0, Math.floor(Number.isFinite(seq) ? seq : 0));
|
|
270
|
+
await this.client.execute({
|
|
271
|
+
sql: `INSERT INTO ds_sync_state (key, value) VALUES (?, ?)
|
|
272
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
273
|
+
value = CASE
|
|
274
|
+
WHEN CAST(excluded.value AS INTEGER) > CAST(ds_sync_state.value AS INTEGER)
|
|
275
|
+
THEN excluded.value
|
|
276
|
+
ELSE ds_sync_state.value
|
|
277
|
+
END`,
|
|
278
|
+
args: [key, String(safe)]
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* Parse a stored numeric value (cloud_seq / local_seq / last_sync_at).
|
|
284
|
+
*
|
|
285
|
+
* Missing → 0 (the "nothing recorded yet" sentinel, matches LocalChangelog).
|
|
286
|
+
* Non-integer / non-numeric → throws to surface corruption to the caller.
|
|
287
|
+
*/
|
|
288
|
+
function parseSeqValue(raw, label) {
|
|
289
|
+
if (raw === void 0) return 0;
|
|
290
|
+
const parsed = Number(raw);
|
|
291
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) throw new Error(`SyncStateStore: corrupt ${label} value: ${raw}`);
|
|
292
|
+
return parsed;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
exports.DEVICE_ID_FORMAT_RE = DEVICE_ID_FORMAT_RE;
|
|
297
|
+
exports.SYNC_STATE_KEYS = SYNC_STATE_KEYS;
|
|
298
|
+
exports.SyncStateStore = SyncStateStore;
|
|
299
|
+
exports.defaultAppState = defaultAppState;
|
|
300
|
+
exports.generateDesktopDeviceId = generateDesktopDeviceId;
|
|
301
|
+
exports.isValidDeviceId = isValidDeviceId;
|
|
302
|
+
exports.parseSyncStatus = parseSyncStatus;
|
|
303
|
+
exports.serializeSyncStatus = serializeSyncStatus;
|