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