@syncular/server 0.0.6-95 → 0.1.0

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 (601) hide show
  1. package/README.md +1 -2
  2. package/dist/auth-leases.d.ts +85 -0
  3. package/dist/auth-leases.d.ts.map +1 -0
  4. package/dist/auth-leases.js +368 -0
  5. package/dist/auth-leases.js.map +1 -0
  6. package/dist/better-sqlite3.d.ts +22 -0
  7. package/dist/better-sqlite3.d.ts.map +1 -0
  8. package/dist/better-sqlite3.js +16 -0
  9. package/dist/better-sqlite3.js.map +1 -0
  10. package/dist/blobs/access.d.ts +35 -0
  11. package/dist/blobs/access.d.ts.map +1 -0
  12. package/dist/blobs/access.js +149 -0
  13. package/dist/blobs/access.js.map +1 -0
  14. package/dist/blobs/adapters/database.d.ts +26 -9
  15. package/dist/blobs/adapters/database.d.ts.map +1 -1
  16. package/dist/blobs/adapters/database.js +68 -23
  17. package/dist/blobs/adapters/database.js.map +1 -1
  18. package/dist/blobs/index.d.ts +1 -0
  19. package/dist/blobs/index.d.ts.map +1 -1
  20. package/dist/blobs/index.js +5 -4
  21. package/dist/blobs/index.js.map +1 -1
  22. package/dist/blobs/manager.d.ts +69 -44
  23. package/dist/blobs/manager.d.ts.map +1 -1
  24. package/dist/blobs/manager.js +250 -65
  25. package/dist/blobs/manager.js.map +1 -1
  26. package/dist/blobs/migrate.d.ts.map +1 -1
  27. package/dist/blobs/migrate.js +16 -8
  28. package/dist/blobs/migrate.js.map +1 -1
  29. package/dist/blobs/types.d.ts +4 -0
  30. package/dist/blobs/types.d.ts.map +1 -1
  31. package/dist/bun-sqlite.d.ts +19 -0
  32. package/dist/bun-sqlite.d.ts.map +1 -0
  33. package/dist/bun-sqlite.js +19 -0
  34. package/dist/bun-sqlite.js.map +1 -0
  35. package/dist/clients.d.ts +1 -0
  36. package/dist/clients.d.ts.map +1 -1
  37. package/dist/clients.js.map +1 -1
  38. package/dist/cloudflare/durable-object.d.ts +93 -0
  39. package/dist/cloudflare/durable-object.d.ts.map +1 -0
  40. package/dist/cloudflare/durable-object.js +210 -0
  41. package/dist/cloudflare/durable-object.js.map +1 -0
  42. package/dist/cloudflare/index.d.ts +22 -0
  43. package/dist/cloudflare/index.d.ts.map +1 -0
  44. package/dist/cloudflare/index.js +22 -0
  45. package/dist/cloudflare/index.js.map +1 -0
  46. package/dist/cloudflare/r2.d.ts +180 -0
  47. package/dist/cloudflare/r2.d.ts.map +1 -0
  48. package/dist/cloudflare/r2.js +258 -0
  49. package/dist/cloudflare/r2.js.map +1 -0
  50. package/dist/cloudflare/scope-cache.d.ts +54 -0
  51. package/dist/cloudflare/scope-cache.d.ts.map +1 -0
  52. package/dist/cloudflare/scope-cache.js +223 -0
  53. package/dist/cloudflare/scope-cache.js.map +1 -0
  54. package/dist/cloudflare/sentry.d.ts +47 -0
  55. package/dist/cloudflare/sentry.d.ts.map +1 -0
  56. package/dist/cloudflare/sentry.js +163 -0
  57. package/dist/cloudflare/sentry.js.map +1 -0
  58. package/dist/cloudflare/worker.d.ts +46 -0
  59. package/dist/cloudflare/worker.d.ts.map +1 -0
  60. package/dist/cloudflare/worker.js +63 -0
  61. package/dist/cloudflare/worker.js.map +1 -0
  62. package/dist/commit-integrity.d.ts +38 -0
  63. package/dist/commit-integrity.d.ts.map +1 -0
  64. package/dist/commit-integrity.js +260 -0
  65. package/dist/commit-integrity.js.map +1 -0
  66. package/dist/compaction.js.map +1 -1
  67. package/dist/crdt-yjs/index.d.ts +99 -0
  68. package/dist/crdt-yjs/index.d.ts.map +1 -0
  69. package/dist/crdt-yjs/index.js +629 -0
  70. package/dist/crdt-yjs/index.js.map +1 -0
  71. package/dist/d1.d.ts +13 -0
  72. package/dist/d1.d.ts.map +1 -0
  73. package/dist/d1.js +14 -0
  74. package/dist/d1.js.map +1 -0
  75. package/dist/dialect/base.d.ts +11 -1
  76. package/dist/dialect/base.d.ts.map +1 -1
  77. package/dist/dialect/base.js +43 -6
  78. package/dist/dialect/base.js.map +1 -1
  79. package/dist/dialect/helpers.d.ts +6 -0
  80. package/dist/dialect/helpers.d.ts.map +1 -1
  81. package/dist/dialect/helpers.js +24 -0
  82. package/dist/dialect/helpers.js.map +1 -1
  83. package/dist/dialect/index.js +3 -3
  84. package/dist/dialect/types.d.ts +40 -0
  85. package/dist/dialect/types.d.ts.map +1 -1
  86. package/dist/encrypted-crdt.d.ts +67 -0
  87. package/dist/encrypted-crdt.d.ts.map +1 -0
  88. package/dist/encrypted-crdt.js +426 -0
  89. package/dist/encrypted-crdt.js.map +1 -0
  90. package/dist/errors.d.ts +10 -0
  91. package/dist/errors.d.ts.map +1 -0
  92. package/dist/errors.js +13 -0
  93. package/dist/errors.js.map +1 -0
  94. package/dist/filesystem/index.d.ts +44 -0
  95. package/dist/filesystem/index.d.ts.map +1 -0
  96. package/dist/filesystem/index.js +164 -0
  97. package/dist/filesystem/index.js.map +1 -0
  98. package/dist/handlers/collection.d.ts +11 -3
  99. package/dist/handlers/collection.d.ts.map +1 -1
  100. package/dist/handlers/collection.js +34 -56
  101. package/dist/handlers/collection.js.map +1 -1
  102. package/dist/handlers/create-handler.d.ts +31 -7
  103. package/dist/handlers/create-handler.d.ts.map +1 -1
  104. package/dist/handlers/create-handler.js +332 -115
  105. package/dist/handlers/create-handler.js.map +1 -1
  106. package/dist/handlers/index.js +3 -3
  107. package/dist/handlers/types.d.ts +86 -1
  108. package/dist/handlers/types.d.ts.map +1 -1
  109. package/dist/helpers/conflict.js.map +1 -1
  110. package/dist/helpers/emitted-change.js.map +1 -1
  111. package/dist/helpers/index.d.ts +2 -0
  112. package/dist/helpers/index.d.ts.map +1 -1
  113. package/dist/helpers/index.js +6 -4
  114. package/dist/helpers/index.js.map +1 -1
  115. package/dist/helpers/paginate.js.map +1 -1
  116. package/dist/helpers/scope-authorization.d.ts +7 -0
  117. package/dist/helpers/scope-authorization.d.ts.map +1 -0
  118. package/dist/helpers/scope-authorization.js +19 -0
  119. package/dist/helpers/scope-authorization.js.map +1 -0
  120. package/dist/helpers/scope-commit-index.d.ts +12 -0
  121. package/dist/helpers/scope-commit-index.d.ts.map +1 -0
  122. package/dist/helpers/scope-commit-index.js +35 -0
  123. package/dist/helpers/scope-commit-index.js.map +1 -0
  124. package/dist/helpers/scope-strings.js.map +1 -1
  125. package/dist/hono/api-key-auth.d.ts +49 -0
  126. package/dist/hono/api-key-auth.d.ts.map +1 -0
  127. package/dist/hono/api-key-auth.js +108 -0
  128. package/dist/hono/api-key-auth.js.map +1 -0
  129. package/dist/hono/audit-redaction.d.ts +20 -0
  130. package/dist/hono/audit-redaction.d.ts.map +1 -0
  131. package/dist/hono/audit-redaction.js +85 -0
  132. package/dist/hono/audit-redaction.js.map +1 -0
  133. package/dist/hono/blobs.d.ts +75 -0
  134. package/dist/hono/blobs.d.ts.map +1 -0
  135. package/dist/hono/blobs.js +586 -0
  136. package/dist/hono/blobs.js.map +1 -0
  137. package/dist/hono/console/gateway.d.ts +42 -0
  138. package/dist/hono/console/gateway.d.ts.map +1 -0
  139. package/dist/hono/console/gateway.js +2158 -0
  140. package/dist/hono/console/gateway.js.map +1 -0
  141. package/dist/hono/console/live-auth.d.ts +7 -0
  142. package/dist/hono/console/live-auth.d.ts.map +1 -0
  143. package/dist/hono/console/live-auth.js +39 -0
  144. package/dist/hono/console/live-auth.js.map +1 -0
  145. package/dist/hono/console/route-descriptor.d.ts +6 -0
  146. package/dist/hono/console/route-descriptor.d.ts.map +1 -0
  147. package/dist/hono/console/route-descriptor.js +16 -0
  148. package/dist/hono/console/route-descriptor.js.map +1 -0
  149. package/dist/hono/console/routes/api-keys.d.ts +8 -0
  150. package/dist/hono/console/routes/api-keys.d.ts.map +1 -0
  151. package/dist/hono/console/routes/api-keys.js +575 -0
  152. package/dist/hono/console/routes/api-keys.js.map +1 -0
  153. package/dist/hono/console/routes/clients.d.ts +8 -0
  154. package/dist/hono/console/routes/clients.d.ts.map +1 -0
  155. package/dist/hono/console/routes/clients.js +322 -0
  156. package/dist/hono/console/routes/clients.js.map +1 -0
  157. package/dist/hono/console/routes/commits.d.ts +8 -0
  158. package/dist/hono/console/routes/commits.d.ts.map +1 -0
  159. package/dist/hono/console/routes/commits.js +882 -0
  160. package/dist/hono/console/routes/commits.js.map +1 -0
  161. package/dist/hono/console/routes/context.d.ts +188 -0
  162. package/dist/hono/console/routes/context.d.ts.map +1 -0
  163. package/dist/hono/console/routes/context.js +633 -0
  164. package/dist/hono/console/routes/context.js.map +1 -0
  165. package/dist/hono/console/routes/events.d.ts +8 -0
  166. package/dist/hono/console/routes/events.d.ts.map +1 -0
  167. package/dist/hono/console/routes/events.js +501 -0
  168. package/dist/hono/console/routes/events.js.map +1 -0
  169. package/dist/hono/console/routes/maintenance.d.ts +8 -0
  170. package/dist/hono/console/routes/maintenance.d.ts.map +1 -0
  171. package/dist/hono/console/routes/maintenance.js +341 -0
  172. package/dist/hono/console/routes/maintenance.js.map +1 -0
  173. package/dist/hono/console/routes/shared.d.ts +174 -0
  174. package/dist/hono/console/routes/shared.d.ts.map +1 -0
  175. package/dist/hono/console/routes/shared.js +583 -0
  176. package/dist/hono/console/routes/shared.js.map +1 -0
  177. package/dist/hono/console/routes/stats.d.ts +8 -0
  178. package/dist/hono/console/routes/stats.d.ts.map +1 -0
  179. package/dist/hono/console/routes/stats.js +295 -0
  180. package/dist/hono/console/routes/stats.js.map +1 -0
  181. package/dist/hono/console/routes/storage.d.ts +8 -0
  182. package/dist/hono/console/routes/storage.d.ts.map +1 -0
  183. package/dist/hono/console/routes/storage.js +121 -0
  184. package/dist/hono/console/routes/storage.js.map +1 -0
  185. package/dist/hono/console/routes.d.ts +36 -0
  186. package/dist/hono/console/routes.d.ts.map +1 -0
  187. package/dist/hono/console/routes.js +112 -0
  188. package/dist/hono/console/routes.js.map +1 -0
  189. package/dist/hono/console/schema-errors.d.ts +2 -0
  190. package/dist/hono/console/schema-errors.d.ts.map +1 -0
  191. package/dist/hono/console/schema-errors.js +17 -0
  192. package/dist/hono/console/schema-errors.js.map +1 -0
  193. package/dist/hono/console/schemas.d.ts +1515 -0
  194. package/dist/hono/console/schemas.d.ts.map +1 -0
  195. package/dist/hono/console/schemas.js +661 -0
  196. package/dist/hono/console/schemas.js.map +1 -0
  197. package/dist/hono/console/types.d.ts +213 -0
  198. package/dist/hono/console/types.d.ts.map +1 -0
  199. package/dist/hono/console/types.js +2 -0
  200. package/dist/hono/console/types.js.map +1 -0
  201. package/dist/hono/console/ui.d.ts +38 -0
  202. package/dist/hono/console/ui.d.ts.map +1 -0
  203. package/dist/hono/console/ui.js +43 -0
  204. package/dist/hono/console/ui.js.map +1 -0
  205. package/dist/hono/create-server.d.ts +71 -0
  206. package/dist/hono/create-server.d.ts.map +1 -0
  207. package/dist/hono/create-server.js +121 -0
  208. package/dist/hono/create-server.js.map +1 -0
  209. package/dist/hono/errors.d.ts +13 -0
  210. package/dist/hono/errors.d.ts.map +1 -0
  211. package/dist/hono/errors.js +21 -0
  212. package/dist/hono/errors.js.map +1 -0
  213. package/dist/hono/index.d.ts +20 -0
  214. package/dist/hono/index.d.ts.map +1 -0
  215. package/dist/hono/index.js +31 -0
  216. package/dist/hono/index.js.map +1 -0
  217. package/dist/hono/openapi.d.ts +72 -0
  218. package/dist/hono/openapi.d.ts.map +1 -0
  219. package/dist/hono/openapi.js +99 -0
  220. package/dist/hono/openapi.js.map +1 -0
  221. package/dist/hono/proxy/connection-manager.d.ts +78 -0
  222. package/dist/hono/proxy/connection-manager.d.ts.map +1 -0
  223. package/dist/hono/proxy/connection-manager.js +251 -0
  224. package/dist/hono/proxy/connection-manager.js.map +1 -0
  225. package/dist/hono/proxy/index.d.ts +8 -0
  226. package/dist/hono/proxy/index.d.ts.map +1 -0
  227. package/dist/hono/proxy/index.js +8 -0
  228. package/dist/hono/proxy/index.js.map +1 -0
  229. package/dist/hono/proxy/routes.d.ts +86 -0
  230. package/dist/hono/proxy/routes.d.ts.map +1 -0
  231. package/dist/hono/proxy/routes.js +183 -0
  232. package/dist/hono/proxy/routes.js.map +1 -0
  233. package/dist/hono/rate-limit.d.ts +101 -0
  234. package/dist/hono/rate-limit.d.ts.map +1 -0
  235. package/dist/hono/rate-limit.js +184 -0
  236. package/dist/hono/rate-limit.js.map +1 -0
  237. package/dist/hono/realtime-sync-packs.d.ts +43 -0
  238. package/dist/hono/realtime-sync-packs.d.ts.map +1 -0
  239. package/dist/hono/realtime-sync-packs.js +219 -0
  240. package/dist/hono/realtime-sync-packs.js.map +1 -0
  241. package/dist/hono/routes/audit.d.ts +12 -0
  242. package/dist/hono/routes/audit.d.ts.map +1 -0
  243. package/dist/hono/routes/audit.js +385 -0
  244. package/dist/hono/routes/audit.js.map +1 -0
  245. package/dist/hono/routes/auth-leases.d.ts +8 -0
  246. package/dist/hono/routes/auth-leases.d.ts.map +1 -0
  247. package/dist/hono/routes/auth-leases.js +88 -0
  248. package/dist/hono/routes/auth-leases.js.map +1 -0
  249. package/dist/hono/routes/combined.d.ts +8 -0
  250. package/dist/hono/routes/combined.d.ts.map +1 -0
  251. package/dist/hono/routes/combined.js +392 -0
  252. package/dist/hono/routes/combined.js.map +1 -0
  253. package/dist/hono/routes/context.d.ts +314 -0
  254. package/dist/hono/routes/context.d.ts.map +1 -0
  255. package/dist/hono/routes/context.js +1147 -0
  256. package/dist/hono/routes/context.js.map +1 -0
  257. package/dist/hono/routes/health.d.ts +8 -0
  258. package/dist/hono/routes/health.d.ts.map +1 -0
  259. package/dist/hono/routes/health.js +16 -0
  260. package/dist/hono/routes/health.js.map +1 -0
  261. package/dist/hono/routes/realtime.d.ts +9 -0
  262. package/dist/hono/routes/realtime.d.ts.map +1 -0
  263. package/dist/hono/routes/realtime.js +626 -0
  264. package/dist/hono/routes/realtime.js.map +1 -0
  265. package/dist/hono/routes/shared.d.ts +689 -0
  266. package/dist/hono/routes/shared.d.ts.map +1 -0
  267. package/dist/hono/routes/shared.js +972 -0
  268. package/dist/hono/routes/shared.js.map +1 -0
  269. package/dist/hono/routes/snapshots.d.ts +10 -0
  270. package/dist/hono/routes/snapshots.d.ts.map +1 -0
  271. package/dist/hono/routes/snapshots.js +281 -0
  272. package/dist/hono/routes/snapshots.js.map +1 -0
  273. package/dist/hono/routes.d.ts +19 -0
  274. package/dist/hono/routes.d.ts.map +1 -0
  275. package/dist/hono/routes.js +37 -0
  276. package/dist/hono/routes.js.map +1 -0
  277. package/dist/hono/validation.d.ts +5 -0
  278. package/dist/hono/validation.d.ts.map +1 -0
  279. package/dist/hono/validation.js +47 -0
  280. package/dist/hono/validation.js.map +1 -0
  281. package/dist/hono/websocket-origin.d.ts +9 -0
  282. package/dist/hono/websocket-origin.d.ts.map +1 -0
  283. package/dist/hono/websocket-origin.js +96 -0
  284. package/dist/hono/websocket-origin.js.map +1 -0
  285. package/dist/hono/ws.d.ts +341 -0
  286. package/dist/hono/ws.d.ts.map +1 -0
  287. package/dist/hono/ws.js +711 -0
  288. package/dist/hono/ws.js.map +1 -0
  289. package/dist/index.d.ts +6 -0
  290. package/dist/index.d.ts.map +1 -1
  291. package/dist/index.js +24 -18
  292. package/dist/index.js.map +1 -1
  293. package/dist/libsql.d.ts +29 -0
  294. package/dist/libsql.d.ts.map +1 -0
  295. package/dist/libsql.js +25 -0
  296. package/dist/libsql.js.map +1 -0
  297. package/dist/migrate.js.map +1 -1
  298. package/dist/neon.d.ts +21 -0
  299. package/dist/neon.d.ts.map +1 -0
  300. package/dist/neon.js +22 -0
  301. package/dist/neon.js.map +1 -0
  302. package/dist/notify.d.ts +30 -0
  303. package/dist/notify.d.ts.map +1 -1
  304. package/dist/notify.js +160 -41
  305. package/dist/notify.js.map +1 -1
  306. package/dist/pglite.d.ts +37 -0
  307. package/dist/pglite.d.ts.map +1 -0
  308. package/dist/pglite.js +37 -0
  309. package/dist/pglite.js.map +1 -0
  310. package/dist/plugins/index.d.ts +2 -0
  311. package/dist/plugins/index.d.ts.map +1 -0
  312. package/dist/plugins/index.js +2 -0
  313. package/dist/plugins/index.js.map +1 -0
  314. package/dist/plugins/types.d.ts +73 -0
  315. package/dist/plugins/types.d.ts.map +1 -0
  316. package/dist/plugins/types.js +30 -0
  317. package/dist/plugins/types.js.map +1 -0
  318. package/dist/postgres/index.d.ts +68 -0
  319. package/dist/postgres/index.d.ts.map +1 -0
  320. package/dist/postgres/index.js +924 -0
  321. package/dist/postgres/index.js.map +1 -0
  322. package/dist/proxy/collection.d.ts +0 -2
  323. package/dist/proxy/collection.d.ts.map +1 -1
  324. package/dist/proxy/collection.js +2 -17
  325. package/dist/proxy/collection.js.map +1 -1
  326. package/dist/proxy/handler.d.ts +1 -1
  327. package/dist/proxy/handler.d.ts.map +1 -1
  328. package/dist/proxy/handler.js +3 -4
  329. package/dist/proxy/handler.js.map +1 -1
  330. package/dist/proxy/index.d.ts +1 -1
  331. package/dist/proxy/index.d.ts.map +1 -1
  332. package/dist/proxy/index.js +3 -3
  333. package/dist/proxy/index.js.map +1 -1
  334. package/dist/proxy/mutation-detector.js.map +1 -1
  335. package/dist/proxy/oplog.d.ts.map +1 -1
  336. package/dist/proxy/oplog.js +32 -8
  337. package/dist/proxy/oplog.js.map +1 -1
  338. package/dist/prune.d.ts +8 -1
  339. package/dist/prune.d.ts.map +1 -1
  340. package/dist/prune.js +68 -28
  341. package/dist/prune.js.map +1 -1
  342. package/dist/pull.d.ts +31 -0
  343. package/dist/pull.d.ts.map +1 -1
  344. package/dist/pull.js +1014 -474
  345. package/dist/pull.js.map +1 -1
  346. package/dist/push.d.ts +36 -2
  347. package/dist/push.d.ts.map +1 -1
  348. package/dist/push.js +603 -358
  349. package/dist/push.js.map +1 -1
  350. package/dist/realtime/in-memory.js.map +1 -1
  351. package/dist/realtime/index.d.ts +2 -1
  352. package/dist/realtime/index.d.ts.map +1 -1
  353. package/dist/realtime/index.js +2 -1
  354. package/dist/realtime/index.js.map +1 -1
  355. package/dist/realtime/types.d.ts +12 -0
  356. package/dist/realtime/types.d.ts.map +1 -1
  357. package/dist/realtime/types.js +13 -1
  358. package/dist/realtime/types.js.map +1 -1
  359. package/dist/relay/client-role/forward-engine.d.ts +63 -0
  360. package/dist/relay/client-role/forward-engine.d.ts.map +1 -0
  361. package/dist/relay/client-role/forward-engine.js +267 -0
  362. package/dist/relay/client-role/forward-engine.js.map +1 -0
  363. package/dist/relay/client-role/index.d.ts +9 -0
  364. package/dist/relay/client-role/index.d.ts.map +1 -0
  365. package/dist/relay/client-role/index.js +9 -0
  366. package/dist/relay/client-role/index.js.map +1 -0
  367. package/dist/relay/client-role/pull-engine.d.ts +72 -0
  368. package/dist/relay/client-role/pull-engine.d.ts.map +1 -0
  369. package/dist/relay/client-role/pull-engine.js +249 -0
  370. package/dist/relay/client-role/pull-engine.js.map +1 -0
  371. package/dist/relay/client-role/sequence-mapper.d.ts +65 -0
  372. package/dist/relay/client-role/sequence-mapper.d.ts.map +1 -0
  373. package/dist/relay/client-role/sequence-mapper.js +161 -0
  374. package/dist/relay/client-role/sequence-mapper.js.map +1 -0
  375. package/dist/relay/evaluation/relay-paths.d.ts +41 -0
  376. package/dist/relay/evaluation/relay-paths.d.ts.map +1 -0
  377. package/dist/relay/evaluation/relay-paths.js +504 -0
  378. package/dist/relay/evaluation/relay-paths.js.map +1 -0
  379. package/dist/relay/evaluation/rust-boundary.d.ts +47 -0
  380. package/dist/relay/evaluation/rust-boundary.d.ts.map +1 -0
  381. package/dist/relay/evaluation/rust-boundary.js +220 -0
  382. package/dist/relay/evaluation/rust-boundary.js.map +1 -0
  383. package/dist/relay/index.d.ts +37 -0
  384. package/dist/relay/index.d.ts.map +1 -0
  385. package/dist/relay/index.js +44 -0
  386. package/dist/relay/index.js.map +1 -0
  387. package/dist/relay/migrate.d.ts +18 -0
  388. package/dist/relay/migrate.d.ts.map +1 -0
  389. package/dist/relay/migrate.js +99 -0
  390. package/dist/relay/migrate.js.map +1 -0
  391. package/dist/relay/mode-manager.d.ts +60 -0
  392. package/dist/relay/mode-manager.d.ts.map +1 -0
  393. package/dist/relay/mode-manager.js +114 -0
  394. package/dist/relay/mode-manager.js.map +1 -0
  395. package/dist/relay/realtime.d.ts +90 -0
  396. package/dist/relay/realtime.d.ts.map +1 -0
  397. package/dist/relay/realtime.js +147 -0
  398. package/dist/relay/realtime.js.map +1 -0
  399. package/dist/relay/relay.d.ts +190 -0
  400. package/dist/relay/relay.d.ts.map +1 -0
  401. package/dist/relay/relay.js +320 -0
  402. package/dist/relay/relay.js.map +1 -0
  403. package/dist/relay/schema.d.ts +158 -0
  404. package/dist/relay/schema.d.ts.map +1 -0
  405. package/dist/relay/schema.js +7 -0
  406. package/dist/relay/schema.js.map +1 -0
  407. package/dist/relay/server-role/index.d.ts +56 -0
  408. package/dist/relay/server-role/index.d.ts.map +1 -0
  409. package/dist/relay/server-role/index.js +193 -0
  410. package/dist/relay/server-role/index.js.map +1 -0
  411. package/dist/relay/server-role/pull.d.ts +27 -0
  412. package/dist/relay/server-role/pull.d.ts.map +1 -0
  413. package/dist/relay/server-role/pull.js +24 -0
  414. package/dist/relay/server-role/pull.js.map +1 -0
  415. package/dist/relay/server-role/push.d.ts +29 -0
  416. package/dist/relay/server-role/push.d.ts.map +1 -0
  417. package/dist/relay/server-role/push.js +122 -0
  418. package/dist/relay/server-role/push.js.map +1 -0
  419. package/dist/s3/index.d.ts +83 -0
  420. package/dist/s3/index.d.ts.map +1 -0
  421. package/dist/s3/index.js +223 -0
  422. package/dist/s3/index.js.map +1 -0
  423. package/dist/schema.d.ts +137 -1
  424. package/dist/schema.d.ts.map +1 -1
  425. package/dist/schema.js +1 -0
  426. package/dist/schema.js.map +1 -1
  427. package/dist/service-worker/index.d.ts +114 -0
  428. package/dist/service-worker/index.d.ts.map +1 -0
  429. package/dist/service-worker/index.js +487 -0
  430. package/dist/service-worker/index.js.map +1 -0
  431. package/dist/snapshot-artifacts/sqlite-bun.d.ts +15 -0
  432. package/dist/snapshot-artifacts/sqlite-bun.d.ts.map +1 -0
  433. package/dist/snapshot-artifacts/sqlite-bun.js +128 -0
  434. package/dist/snapshot-artifacts/sqlite-bun.js.map +1 -0
  435. package/dist/snapshot-artifacts.d.ts +206 -0
  436. package/dist/snapshot-artifacts.d.ts.map +1 -0
  437. package/dist/snapshot-artifacts.js +545 -0
  438. package/dist/snapshot-artifacts.js.map +1 -0
  439. package/dist/snapshot-chunks/db-metadata.d.ts +18 -0
  440. package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
  441. package/dist/snapshot-chunks/db-metadata.js +91 -63
  442. package/dist/snapshot-chunks/db-metadata.js.map +1 -1
  443. package/dist/snapshot-chunks/index.js +2 -2
  444. package/dist/snapshot-chunks/types.d.ts +2 -0
  445. package/dist/snapshot-chunks/types.d.ts.map +1 -1
  446. package/dist/snapshot-chunks.d.ts +33 -3
  447. package/dist/snapshot-chunks.d.ts.map +1 -1
  448. package/dist/snapshot-chunks.js +136 -55
  449. package/dist/snapshot-chunks.js.map +1 -1
  450. package/dist/sqlite/index.d.ts +53 -0
  451. package/dist/sqlite/index.d.ts.map +1 -0
  452. package/dist/sqlite/index.js +795 -0
  453. package/dist/sqlite/index.js.map +1 -0
  454. package/dist/sqlite3.d.ts +22 -0
  455. package/dist/sqlite3.d.ts.map +1 -0
  456. package/dist/sqlite3.js +99 -0
  457. package/dist/sqlite3.js.map +1 -0
  458. package/dist/stats.d.ts +8 -0
  459. package/dist/stats.d.ts.map +1 -1
  460. package/dist/stats.js +45 -13
  461. package/dist/stats.js.map +1 -1
  462. package/dist/subscriptions/cache.d.ts +3 -0
  463. package/dist/subscriptions/cache.d.ts.map +1 -1
  464. package/dist/subscriptions/cache.js +44 -0
  465. package/dist/subscriptions/cache.js.map +1 -1
  466. package/dist/subscriptions/index.js +2 -2
  467. package/dist/subscriptions/resolve.d.ts +3 -1
  468. package/dist/subscriptions/resolve.d.ts.map +1 -1
  469. package/dist/subscriptions/resolve.js +91 -52
  470. package/dist/subscriptions/resolve.js.map +1 -1
  471. package/dist/sync.d.ts +3 -0
  472. package/dist/sync.d.ts.map +1 -1
  473. package/dist/sync.js +9 -5
  474. package/dist/sync.js.map +1 -1
  475. package/package.json +297 -8
  476. package/src/auth-leases.ts +649 -0
  477. package/src/better-sqlite3.ts +35 -0
  478. package/src/blobs/access.ts +244 -0
  479. package/src/blobs/adapters/database.ts +125 -43
  480. package/src/blobs/index.ts +1 -0
  481. package/src/blobs/manager.ts +366 -65
  482. package/src/blobs/migrate.ts +16 -8
  483. package/src/blobs/types.ts +4 -0
  484. package/src/bun-sqlite-ambient.d.ts +19 -0
  485. package/src/bun-sqlite.ts +27 -0
  486. package/src/clients.ts +1 -0
  487. package/src/cloudflare/durable-object.ts +289 -0
  488. package/src/cloudflare/index.ts +22 -0
  489. package/src/cloudflare/r2.ts +526 -0
  490. package/src/cloudflare/scope-cache.ts +341 -0
  491. package/src/cloudflare/sentry.ts +230 -0
  492. package/src/cloudflare/worker.ts +77 -0
  493. package/src/commit-integrity.ts +371 -0
  494. package/src/crdt-yjs/index.ts +931 -0
  495. package/src/d1.ts +16 -0
  496. package/src/dialect/base.ts +71 -5
  497. package/src/dialect/helpers.ts +31 -0
  498. package/src/dialect/types.ts +47 -0
  499. package/src/encrypted-crdt.ts +786 -0
  500. package/src/errors.ts +22 -0
  501. package/src/filesystem/index.ts +262 -0
  502. package/src/handlers/collection.ts +78 -78
  503. package/src/handlers/create-handler.ts +540 -147
  504. package/src/handlers/types.ts +116 -0
  505. package/src/helpers/index.ts +2 -0
  506. package/src/helpers/scope-authorization.ts +27 -0
  507. package/src/helpers/scope-commit-index.ts +52 -0
  508. package/src/hono/api-key-auth.ts +177 -0
  509. package/src/hono/audit-redaction.ts +135 -0
  510. package/src/hono/blobs.ts +851 -0
  511. package/src/hono/console/gateway.ts +3046 -0
  512. package/src/hono/console/live-auth.ts +46 -0
  513. package/src/hono/console/route-descriptor.ts +22 -0
  514. package/src/hono/console/routes/api-keys.ts +721 -0
  515. package/src/hono/console/routes/clients.ts +447 -0
  516. package/src/hono/console/routes/commits.ts +1137 -0
  517. package/src/hono/console/routes/context.ts +956 -0
  518. package/src/hono/console/routes/events.ts +669 -0
  519. package/src/hono/console/routes/maintenance.ts +461 -0
  520. package/src/hono/console/routes/shared.ts +816 -0
  521. package/src/hono/console/routes/stats.ts +392 -0
  522. package/src/hono/console/routes/storage.ts +159 -0
  523. package/src/hono/console/routes.ts +146 -0
  524. package/src/hono/console/schema-errors.ts +23 -0
  525. package/src/hono/console/schemas.ts +914 -0
  526. package/src/hono/console/types.ts +223 -0
  527. package/src/hono/console/ui.ts +100 -0
  528. package/src/hono/create-server.ts +230 -0
  529. package/src/hono/errors.ts +50 -0
  530. package/src/hono/index.ts +54 -0
  531. package/src/hono/openapi.ts +139 -0
  532. package/src/hono/proxy/connection-manager.ts +340 -0
  533. package/src/hono/proxy/index.ts +8 -0
  534. package/src/hono/proxy/routes.ts +272 -0
  535. package/src/hono/rate-limit.ts +319 -0
  536. package/src/hono/realtime-sync-packs.ts +354 -0
  537. package/src/hono/routes/audit.ts +499 -0
  538. package/src/hono/routes/auth-leases.ts +119 -0
  539. package/src/hono/routes/combined.ts +583 -0
  540. package/src/hono/routes/context.ts +1629 -0
  541. package/src/hono/routes/health.ts +26 -0
  542. package/src/hono/routes/realtime.ts +808 -0
  543. package/src/hono/routes/shared.ts +1626 -0
  544. package/src/hono/routes/snapshots.ts +345 -0
  545. package/src/hono/routes.ts +68 -0
  546. package/src/hono/validation.ts +81 -0
  547. package/src/hono/websocket-origin.ts +131 -0
  548. package/src/hono/ws.ts +1134 -0
  549. package/src/index.ts +6 -0
  550. package/src/libsql.ts +51 -0
  551. package/src/neon.ts +28 -0
  552. package/src/notify.ts +224 -44
  553. package/src/pglite.ts +68 -0
  554. package/src/plugins/index.ts +1 -0
  555. package/src/plugins/types.ts +144 -0
  556. package/src/postgres/index.ts +1291 -0
  557. package/src/proxy/collection.ts +5 -27
  558. package/src/proxy/handler.ts +2 -2
  559. package/src/proxy/index.ts +0 -2
  560. package/src/proxy/oplog.ts +41 -10
  561. package/src/prune.ts +137 -36
  562. package/src/pull.ts +1504 -591
  563. package/src/push.ts +881 -443
  564. package/src/realtime/index.ts +2 -0
  565. package/src/realtime/types.ts +35 -0
  566. package/src/relay/bun-types.d.ts +50 -0
  567. package/src/relay/client-role/forward-engine.ts +355 -0
  568. package/src/relay/client-role/index.ts +9 -0
  569. package/src/relay/client-role/pull-engine.ts +329 -0
  570. package/src/relay/client-role/sequence-mapper.ts +201 -0
  571. package/src/relay/evaluation/relay-paths.ts +699 -0
  572. package/src/relay/evaluation/rust-boundary.ts +464 -0
  573. package/src/relay/index.ts +50 -0
  574. package/src/relay/migrate.ts +113 -0
  575. package/src/relay/mode-manager.ts +142 -0
  576. package/src/relay/realtime.ts +207 -0
  577. package/src/relay/relay.ts +431 -0
  578. package/src/relay/schema.ts +171 -0
  579. package/src/relay/server-role/index.ts +338 -0
  580. package/src/relay/server-role/pull.ts +43 -0
  581. package/src/relay/server-role/push.ts +164 -0
  582. package/src/s3/index.ts +346 -0
  583. package/src/schema.ts +141 -1
  584. package/src/service-worker/index.ts +773 -0
  585. package/src/snapshot-artifacts/sqlite-bun.ts +168 -0
  586. package/src/snapshot-artifacts.ts +896 -0
  587. package/src/snapshot-chunks/db-metadata.ts +131 -72
  588. package/src/snapshot-chunks/types.ts +2 -0
  589. package/src/snapshot-chunks.ts +194 -70
  590. package/src/sqlite/index.ts +1064 -0
  591. package/src/sqlite3.ts +137 -0
  592. package/src/stats.ts +76 -13
  593. package/src/subscriptions/cache.ts +58 -0
  594. package/src/subscriptions/resolve.ts +99 -58
  595. package/src/sync.ts +16 -6
  596. package/src/blobs/adapters/database.test.ts +0 -67
  597. package/src/notify.test.ts +0 -520
  598. package/src/proxy/handler.test.ts +0 -126
  599. package/src/proxy/mutation-detector.test.ts +0 -71
  600. package/src/snapshot-chunks/db-metadata.test.ts +0 -172
  601. package/src/subscriptions/resolve.test.ts +0 -180
package/src/pull.ts CHANGED
@@ -1,38 +1,66 @@
1
1
  import {
2
+ type BinarySnapshotColumn,
3
+ type BinarySnapshotColumnType,
4
+ type BinarySnapshotRowsEncoder,
5
+ bytesToReadableStream,
2
6
  captureSyncException,
7
+ concatByteChunks,
3
8
  countSyncMetric,
9
+ createSnapshotManifest,
4
10
  distributionSyncMetric,
5
- encodeSnapshotRowFrames,
6
- encodeSnapshotRows,
11
+ encodeBinarySnapshotTable,
7
12
  gzipBytes,
8
- gzipBytesToStream,
9
13
  randomId,
10
14
  type ScopeValues,
15
+ SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1,
11
16
  SYNC_SNAPSHOT_CHUNK_COMPRESSION,
12
17
  SYNC_SNAPSHOT_CHUNK_ENCODING,
13
18
  type SyncBootstrapState,
14
19
  type SyncChange,
15
20
  type SyncCommit,
21
+ type SyncCrdtStateVectorHint,
16
22
  type SyncPullRequest,
17
23
  type SyncPullResponse,
18
24
  type SyncPullSubscriptionResponse,
25
+ type SyncScopedSnapshotArtifactKind,
19
26
  type SyncSnapshot,
27
+ type SyncSnapshotArtifactCompression,
28
+ type SyncSnapshotArtifactsRequest,
29
+ type SyncSnapshotChunkRef,
20
30
  sha256Hex,
31
+ snapshotScopeDigestFromCacheKey,
21
32
  startSyncSpan,
22
33
  } from '@syncular/core';
23
34
  import type { Kysely } from 'kysely';
24
- import type { DbExecutor, ServerSyncDialect } from './dialect/types';
35
+ import {
36
+ createWireSubscriptionIntegrity,
37
+ SYNCULAR_COMMIT_GENESIS_ROOT,
38
+ } from './commit-integrity';
39
+ import type {
40
+ DbExecutor,
41
+ IncrementalPullRow,
42
+ ServerSyncDialect,
43
+ } from './dialect/types';
25
44
  import {
26
45
  getServerBootstrapOrderFor,
27
- getServerHandlerOrThrow,
28
46
  type ServerHandlerCollection,
29
47
  } from './handlers/collection';
30
48
  import type { ServerTableHandler, SyncServerAuth } from './handlers/types';
31
49
  import { EXTERNAL_CLIENT_ID } from './notify';
50
+ import {
51
+ type SyncServerPullPlugin,
52
+ sortServerPullPlugins,
53
+ } from './plugins/types';
32
54
  import type { SyncCoreDb } from './schema';
33
55
  import {
56
+ createScopedSnapshotArtifactScopeCacheKey,
57
+ readBestScopedSnapshotArtifactRefForPageCapacity,
58
+ } from './snapshot-artifacts';
59
+ import {
60
+ createSnapshotChunkScopeCacheKey,
34
61
  insertSnapshotChunk,
35
62
  readSnapshotChunkRefByPageKey,
63
+ type SnapshotChunkRefWithContinuation,
36
64
  } from './snapshot-chunks';
37
65
  import type { SnapshotChunkStorage } from './snapshot-chunks/types';
38
66
  import {
@@ -42,24 +70,294 @@ import {
42
70
  import { resolveEffectiveScopesForSubscriptions } from './subscriptions/resolve';
43
71
 
44
72
  const defaultScopeCache = createMemoryScopeCache();
73
+ const DEFAULT_MAX_BINARY_SNAPSHOT_BUNDLE_ROWS = 50_000;
74
+ const DEFAULT_SNAPSHOT_CHUNK_GZIP_LEVEL = 1;
75
+ const MAX_PULL_TRANSACTION_RETRIES = 2;
76
+ const PULL_TRANSACTION_RETRY_DELAY_MS = 15;
77
+
78
+ interface PullBootstrapTimings {
79
+ snapshotQueryMs: number;
80
+ binaryEncodeMs: number;
81
+ chunkCacheLookupMs: number;
82
+ artifactCacheLookupMs: number;
83
+ chunkGzipMs: number;
84
+ chunkHashMs: number;
85
+ chunkPersistMs: number;
86
+ }
87
+
88
+ interface SnapshotChunkEncodeResult {
89
+ body: Uint8Array;
90
+ sha256: string;
91
+ gzipMs: number;
92
+ hashMs: number;
93
+ }
94
+
95
+ function toResponseChunkRef(ref: SyncSnapshotChunkRef): SyncSnapshotChunkRef {
96
+ return {
97
+ id: ref.id,
98
+ byteLength: ref.byteLength,
99
+ sha256: ref.sha256,
100
+ encoding: ref.encoding,
101
+ compression: ref.compression,
102
+ };
103
+ }
104
+
105
+ async function createChunkedSnapshotManifest(args: {
106
+ table: string;
107
+ asOfCommitSeq: number;
108
+ scopeKey: string;
109
+ rowCursor: string | null;
110
+ rowLimit: number;
111
+ nextRowCursor: string | null;
112
+ isFirstPage: boolean;
113
+ isLastPage: boolean;
114
+ chunks: readonly SyncSnapshotChunkRef[];
115
+ }): Promise<SyncSnapshot['manifest']> {
116
+ return createSnapshotManifest({
117
+ version: 1,
118
+ table: args.table,
119
+ asOfCommitSeq: args.asOfCommitSeq,
120
+ scopeDigest: snapshotScopeDigestFromCacheKey(args.scopeKey),
121
+ rowCursor: args.rowCursor,
122
+ rowLimit: args.rowLimit,
123
+ nextRowCursor: args.nextRowCursor,
124
+ isFirstPage: args.isFirstPage,
125
+ isLastPage: args.isLastPage,
126
+ chunks: args.chunks.map(toResponseChunkRef),
127
+ });
128
+ }
129
+
130
+ interface SnapshotArtifactSelection {
131
+ artifactKind: SyncScopedSnapshotArtifactKind;
132
+ compression: SyncSnapshotArtifactCompression;
133
+ schemaVersion: string;
134
+ featureSet: readonly string[];
135
+ }
45
136
 
46
- function concatByteChunks(chunks: readonly Uint8Array[]): Uint8Array {
47
- if (chunks.length === 1) {
48
- return chunks[0] ?? new Uint8Array();
137
+ function normalizeFeatureSet(
138
+ features: readonly string[] | undefined
139
+ ): string[] {
140
+ return Array.from(new Set(features ?? [])).sort();
141
+ }
142
+
143
+ function resolveSnapshotArtifactSelection(
144
+ request: SyncSnapshotArtifactsRequest | undefined,
145
+ clientSchemaVersion: number
146
+ ): SnapshotArtifactSelection | null {
147
+ if (!request) return null;
148
+ if (
149
+ !request.artifactKinds.includes(
150
+ SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1
151
+ )
152
+ ) {
153
+ return null;
49
154
  }
155
+ if (!request.compressions?.includes(SYNC_SNAPSHOT_CHUNK_COMPRESSION)) {
156
+ return null;
157
+ }
158
+ return {
159
+ artifactKind: SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1,
160
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
161
+ schemaVersion: String(clientSchemaVersion),
162
+ featureSet: normalizeFeatureSet(request.featureSet),
163
+ };
164
+ }
165
+
166
+ function resolveSnapshotBinaryColumns<
167
+ DB extends SyncCoreDb,
168
+ Auth extends SyncServerAuth,
169
+ >(
170
+ handler: ServerTableHandler<DB, Auth>,
171
+ schemaVersion: number
172
+ ): readonly BinarySnapshotColumn[] | undefined {
173
+ const versioned = handler.snapshotBinaryColumnsForVersion?.(schemaVersion);
174
+ return versioned === undefined
175
+ ? handler.snapshotBinaryColumns
176
+ : (versioned ?? undefined);
177
+ }
178
+
179
+ function resolveSnapshotBinaryEncoder<
180
+ DB extends SyncCoreDb,
181
+ Auth extends SyncServerAuth,
182
+ >(
183
+ handler: ServerTableHandler<DB, Auth>,
184
+ schemaVersion: number
185
+ ): BinarySnapshotRowsEncoder | undefined {
186
+ const versioned = handler.snapshotBinaryEncoderForVersion?.(schemaVersion);
187
+ return versioned === undefined
188
+ ? handler.snapshotBinaryEncoder
189
+ : (versioned ?? undefined);
190
+ }
50
191
 
51
- let total = 0;
52
- for (const chunk of chunks) {
53
- total += chunk.length;
192
+ function createPullBootstrapTimings(): PullBootstrapTimings {
193
+ return {
194
+ snapshotQueryMs: 0,
195
+ binaryEncodeMs: 0,
196
+ chunkCacheLookupMs: 0,
197
+ artifactCacheLookupMs: 0,
198
+ chunkGzipMs: 0,
199
+ chunkHashMs: 0,
200
+ chunkPersistMs: 0,
201
+ };
202
+ }
203
+
204
+ function resolveBinarySnapshotBundleRowLimit(args: {
205
+ limitSnapshotRows: number;
206
+ pagesRemaining: number;
207
+ }): number {
208
+ const pageSize = Math.max(1, args.limitSnapshotRows);
209
+ const pagesRemaining = Math.max(1, args.pagesRemaining);
210
+ const maxBundlePages = Math.max(
211
+ 1,
212
+ Math.ceil(DEFAULT_MAX_BINARY_SNAPSHOT_BUNDLE_ROWS / pageSize)
213
+ );
214
+ return pageSize * Math.min(pagesRemaining, maxBundlePages);
215
+ }
216
+
217
+ async function gzipByteChunks(
218
+ chunks: readonly Uint8Array[],
219
+ gzipLevel: number
220
+ ): Promise<Uint8Array> {
221
+ return gzipBytes(concatByteChunks(chunks), {
222
+ level: gzipLevel,
223
+ });
224
+ }
225
+
226
+ async function encodeCompressedSnapshotChunk(
227
+ chunks: readonly Uint8Array[],
228
+ gzipLevel: number
229
+ ): Promise<SnapshotChunkEncodeResult> {
230
+ const gzipStartedAt = Date.now();
231
+ const body = await gzipByteChunks(chunks, gzipLevel);
232
+ const gzipMs = Math.max(0, Date.now() - gzipStartedAt);
233
+ const hashStartedAt = Date.now();
234
+ const sha256 = await sha256Hex(body);
235
+ const hashMs = Math.max(0, Date.now() - hashStartedAt);
236
+ return { body, sha256, gzipMs, hashMs };
237
+ }
238
+
239
+ async function encodeCompressedSnapshotChunkToStream(
240
+ chunks: readonly Uint8Array[],
241
+ gzipLevel: number
242
+ ): Promise<{
243
+ stream: ReadableStream<Uint8Array>;
244
+ byteLength: number;
245
+ sha256: string;
246
+ gzipMs: number;
247
+ hashMs: number;
248
+ }> {
249
+ const encoded = await encodeCompressedSnapshotChunk(chunks, gzipLevel);
250
+ return {
251
+ stream: bytesToReadableStream(encoded.body),
252
+ byteLength: encoded.body.length,
253
+ sha256: encoded.sha256,
254
+ gzipMs: encoded.gzipMs,
255
+ hashMs: encoded.hashMs,
256
+ };
257
+ }
258
+
259
+ interface SnapshotColumnInference {
260
+ name: string;
261
+ type: BinarySnapshotColumnType | null;
262
+ nullable: boolean;
263
+ presentCount: number;
264
+ }
265
+
266
+ function encodeBinarySnapshotRows(
267
+ table: string,
268
+ rows: readonly unknown[],
269
+ columns?: readonly BinarySnapshotColumn[]
270
+ ): Uint8Array {
271
+ const recordRows = rows.map((row) => toSnapshotRecordRow(table, row));
272
+ return encodeBinarySnapshotTable({
273
+ table,
274
+ columns: columns ?? inferBinarySnapshotColumns(recordRows),
275
+ rows: recordRows,
276
+ });
277
+ }
278
+
279
+ function toSnapshotRecordRow(
280
+ table: string,
281
+ row: unknown
282
+ ): Record<string, unknown> {
283
+ if (
284
+ row == null ||
285
+ typeof row !== 'object' ||
286
+ Array.isArray(row) ||
287
+ row instanceof Uint8Array ||
288
+ row instanceof ArrayBuffer
289
+ ) {
290
+ throw new Error(
291
+ `Cannot encode binary snapshot for table ${table}: snapshot rows must be objects`
292
+ );
54
293
  }
294
+ return row as Record<string, unknown>;
295
+ }
55
296
 
56
- const merged = new Uint8Array(total);
57
- let offset = 0;
58
- for (const chunk of chunks) {
59
- merged.set(chunk, offset);
60
- offset += chunk.length;
297
+ function inferBinarySnapshotColumns(
298
+ rows: readonly Record<string, unknown>[]
299
+ ): BinarySnapshotColumn[] {
300
+ const columns: SnapshotColumnInference[] = [];
301
+ const columnsByName = new Map<string, SnapshotColumnInference>();
302
+
303
+ for (const row of rows) {
304
+ for (const name in row) {
305
+ if (!Object.hasOwn(row, name)) continue;
306
+ const value = row[name];
307
+ let column = columnsByName.get(name);
308
+ if (!column) {
309
+ column = { name, type: null, nullable: false, presentCount: 0 };
310
+ columnsByName.set(name, column);
311
+ columns.push(column);
312
+ }
313
+ column.presentCount += 1;
314
+ if (value == null) {
315
+ column.nullable = true;
316
+ continue;
317
+ }
318
+ column.type = mergeBinarySnapshotColumnTypes(
319
+ column.type,
320
+ inferBinarySnapshotColumnType(value)
321
+ );
322
+ }
61
323
  }
62
- return merged;
324
+
325
+ return columns.map((column) => ({
326
+ name: column.name,
327
+ type: column.type ?? 'json',
328
+ ...(column.nullable || column.presentCount < rows.length
329
+ ? { nullable: true }
330
+ : {}),
331
+ }));
332
+ }
333
+
334
+ function inferBinarySnapshotColumnType(
335
+ value: unknown
336
+ ): BinarySnapshotColumnType {
337
+ if (typeof value === 'string') return 'string';
338
+ if (typeof value === 'boolean') return 'boolean';
339
+ if (typeof value === 'bigint') return 'integer';
340
+ if (typeof value === 'number') {
341
+ return Number.isSafeInteger(value) ? 'integer' : 'float';
342
+ }
343
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
344
+ return 'bytes';
345
+ }
346
+ return 'json';
347
+ }
348
+
349
+ function mergeBinarySnapshotColumnTypes(
350
+ current: BinarySnapshotColumnType | null,
351
+ next: BinarySnapshotColumnType
352
+ ): BinarySnapshotColumnType {
353
+ if (!current || current === next) return next;
354
+ if (
355
+ (current === 'integer' && next === 'float') ||
356
+ (current === 'float' && next === 'integer')
357
+ ) {
358
+ return 'float';
359
+ }
360
+ return 'json';
63
361
  }
64
362
 
65
363
  export interface PullResult {
@@ -71,6 +369,8 @@ export interface PullResult {
71
369
  effectiveScopes: ScopeValues;
72
370
  /** Minimum nextCursor across active subscriptions (for pruning cursor tracking). */
73
371
  clientCursor: number;
372
+ /** Internal bootstrap timing breakdown used for benchmark-gated diagnostics. */
373
+ bootstrapTimings?: PullBootstrapTimings;
74
374
  }
75
375
 
76
376
  interface PendingExternalChunkWrite {
@@ -82,23 +382,34 @@ interface PendingExternalChunkWrite {
82
382
  asOfCommitSeq: number;
83
383
  rowCursor: string | null;
84
384
  rowLimit: number;
385
+ nextRowCursor: string | null;
386
+ isLastPage: boolean;
85
387
  };
86
- rowFramePayload: Uint8Array;
388
+ payloadParts: Uint8Array[];
87
389
  expiresAt: string;
88
390
  }
89
391
 
90
- /**
91
- * Generate a stable cache key for snapshot chunks.
92
- */
93
- async function scopesToCacheKey(scopes: ScopeValues): Promise<string> {
94
- const sorted = Object.entries(scopes)
95
- .sort(([a], [b]) => a.localeCompare(b))
96
- .map(([k, v]) => {
97
- const arr = Array.isArray(v) ? [...v].sort() : [v];
98
- return `${k}:${arr.join(',')}`;
99
- })
100
- .join('|');
101
- return await sha256Hex(sorted);
392
+ async function runWithConcurrency<T>(
393
+ items: readonly T[],
394
+ concurrency: number,
395
+ worker: (item: T) => Promise<void>
396
+ ): Promise<void> {
397
+ if (items.length === 0) return;
398
+
399
+ const workerCount = Math.max(1, Math.min(concurrency, items.length));
400
+ let nextIndex = 0;
401
+
402
+ async function runWorker(): Promise<void> {
403
+ while (nextIndex < items.length) {
404
+ const index = nextIndex;
405
+ nextIndex += 1;
406
+ const item = items[index];
407
+ if (item === undefined) continue;
408
+ await worker(item);
409
+ }
410
+ }
411
+
412
+ await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
102
413
  }
103
414
 
104
415
  /**
@@ -116,6 +427,27 @@ function sanitizeLimit(
116
427
  return Math.max(min, Math.min(max, value));
117
428
  }
118
429
 
430
+ function sanitizeGzipLevel(value: number | undefined): number {
431
+ if (value === undefined || value === null || !Number.isFinite(value)) {
432
+ return DEFAULT_SNAPSHOT_CHUNK_GZIP_LEVEL;
433
+ }
434
+ return Math.max(0, Math.min(9, Math.trunc(value)));
435
+ }
436
+
437
+ function isSerializablePullError(error: Error): boolean {
438
+ const withCode = error as Error & { code?: string };
439
+ return (
440
+ withCode.code === '40001' ||
441
+ error.message.toLowerCase().includes('could not serialize access')
442
+ );
443
+ }
444
+
445
+ async function delay(ms: number): Promise<void> {
446
+ await new Promise((resolve) => {
447
+ setTimeout(resolve, ms);
448
+ });
449
+ }
450
+
119
451
  /**
120
452
  * Merge all scope values into a flat ScopeValues for cursor tracking.
121
453
  */
@@ -149,6 +481,90 @@ interface PullResponseStats {
149
481
  snapshotPageCount: number;
150
482
  }
151
483
 
484
+ function assertPullChangeIdentityUnchanged(
485
+ source: string,
486
+ before: SyncChange,
487
+ after: SyncChange
488
+ ): void {
489
+ if (before.table !== after.table) {
490
+ throw new Error(
491
+ `${source} cannot change change.table (${before.table} -> ${after.table})`
492
+ );
493
+ }
494
+ if (before.row_id !== after.row_id) {
495
+ throw new Error(
496
+ `${source} cannot change change.row_id (${before.row_id} -> ${after.row_id})`
497
+ );
498
+ }
499
+ if (before.op !== after.op) {
500
+ throw new Error(
501
+ `${source} cannot change change.op (${before.op} -> ${after.op})`
502
+ );
503
+ }
504
+ }
505
+
506
+ function projectPullChangeForClientSchema<
507
+ DB extends SyncCoreDb,
508
+ Auth extends SyncServerAuth,
509
+ >(args: {
510
+ handler: ServerTableHandler<DB, Auth>;
511
+ change: SyncChange;
512
+ schemaVersion: number;
513
+ }): SyncChange {
514
+ const projected = args.handler.projectChangeForVersion
515
+ ? args.handler.projectChangeForVersion(args.change, args.schemaVersion)
516
+ : args.change;
517
+ assertPullChangeIdentityUnchanged(
518
+ 'Server table handler projectChangeForVersion',
519
+ args.change,
520
+ projected
521
+ );
522
+ return projected;
523
+ }
524
+
525
+ async function transformPullChanges<
526
+ DB extends SyncCoreDb,
527
+ Auth extends SyncServerAuth,
528
+ >(args: {
529
+ plugins: readonly SyncServerPullPlugin<DB, Auth>[];
530
+ ctx: { db: DbExecutor<DB>; actorId: string; auth: Auth };
531
+ tableHandler: ServerTableHandler<DB, Auth>;
532
+ subscription: {
533
+ id: string;
534
+ table: string;
535
+ scopes: ScopeValues;
536
+ params: Record<string, unknown> | undefined;
537
+ cursor: number;
538
+ crdtStateVectors: readonly SyncCrdtStateVectorHint[];
539
+ };
540
+ changes: readonly SyncChange[];
541
+ }): Promise<SyncChange[]> {
542
+ let changes = [...args.changes];
543
+ for (const plugin of args.plugins) {
544
+ if (!plugin.transformPullChanges) continue;
545
+ const nextChanges = await plugin.transformPullChanges({
546
+ ctx: args.ctx,
547
+ tableHandler: args.tableHandler,
548
+ subscription: args.subscription,
549
+ changes,
550
+ });
551
+ if (nextChanges.length !== changes.length) {
552
+ throw new Error(
553
+ `Server pull plugin "${plugin.name}" cannot change pull change count (${changes.length} -> ${nextChanges.length})`
554
+ );
555
+ }
556
+ for (let i = 0; i < changes.length; i += 1) {
557
+ assertPullChangeIdentityUnchanged(
558
+ `Server pull plugin "${plugin.name}"`,
559
+ changes[i]!,
560
+ nextChanges[i]!
561
+ );
562
+ }
563
+ changes = [...nextChanges];
564
+ }
565
+ return changes;
566
+ }
567
+
152
568
  function summarizePullResponse(response: SyncPullResponse): PullResponseStats {
153
569
  const subscriptions = response.subscriptions ?? [];
154
570
  let activeSubscriptionCount = 0;
@@ -239,31 +655,44 @@ function recordPullMetrics(args: {
239
655
  );
240
656
  }
241
657
 
242
- /**
243
- * Read synthetic commits created by notifyExternalDataChange() after a given cursor.
244
- * Returns commit_seq and affected tables for each external change commit.
245
- */
246
- async function readExternalDataChanges<DB extends SyncCoreDb>(
658
+ async function readLatestExternalCommitByTable<DB extends SyncCoreDb>(
247
659
  trx: DbExecutor<DB>,
248
- dialect: ServerSyncDialect,
249
- args: { partitionId: string; afterCursor: number }
250
- ): Promise<Array<{ commitSeq: number; tables: string[] }>> {
660
+ args: { partitionId: string; afterCursor: number; tables: string[] }
661
+ ): Promise<Map<string, number>> {
662
+ const tableNames = Array.from(
663
+ new Set(args.tables.filter((table) => typeof table === 'string'))
664
+ );
665
+ const latestByTable = new Map<string, number>();
666
+ if (tableNames.length === 0) {
667
+ return latestByTable;
668
+ }
669
+
251
670
  type SyncExecutor = Pick<Kysely<SyncCoreDb>, 'selectFrom'>;
252
671
  const executor = trx as SyncExecutor;
253
-
254
672
  const rows = await executor
255
- .selectFrom('sync_commits')
256
- .select(['commit_seq', 'affected_tables'])
257
- .where('partition_id', '=', args.partitionId)
258
- .where('client_id', '=', EXTERNAL_CLIENT_ID)
259
- .where('commit_seq', '>', args.afterCursor)
260
- .orderBy('commit_seq', 'asc')
673
+ .selectFrom('sync_table_commits as tc')
674
+ .innerJoin('sync_commits as cm', (join) =>
675
+ join
676
+ .onRef('cm.commit_seq', '=', 'tc.commit_seq')
677
+ .onRef('cm.partition_id', '=', 'tc.partition_id')
678
+ )
679
+ .select(['tc.table as table'])
680
+ .select((eb) => eb.fn.max('tc.commit_seq').as('latest_commit_seq'))
681
+ .where('tc.partition_id', '=', args.partitionId)
682
+ .where('cm.client_id', '=', EXTERNAL_CLIENT_ID)
683
+ .where('cm.change_count', '=', 0)
684
+ .where('tc.commit_seq', '>', args.afterCursor)
685
+ .where('tc.table', 'in', tableNames)
686
+ .groupBy('tc.table')
261
687
  .execute();
262
688
 
263
- return rows.map((row) => ({
264
- commitSeq: Number(row.commit_seq),
265
- tables: dialect.dbToArray(row.affected_tables),
266
- }));
689
+ for (const row of rows) {
690
+ const commitSeq = Number(row.latest_commit_seq ?? -1);
691
+ if (!Number.isFinite(commitSeq) || commitSeq < 0) continue;
692
+ latestByTable.set(row.table, commitSeq);
693
+ }
694
+
695
+ return latestByTable;
267
696
  }
268
697
 
269
698
  export async function pull<
@@ -287,10 +716,39 @@ export async function pull<
287
716
  * Defaults to process-local memory cache.
288
717
  */
289
718
  scopeCache?: ScopeCacheBackend;
719
+ /**
720
+ * Gzip compression level for generated snapshot chunks. The protocol remains
721
+ * gzip-only; this tunes CPU/size tradeoffs for deployments that know their
722
+ * network constraints.
723
+ *
724
+ * Default: 1, range: 0-9.
725
+ */
726
+ snapshotChunkGzipLevel?: number;
727
+ /**
728
+ * Schema/cache semantic version included in generated snapshot chunk cache
729
+ * keys. Changing this value invalidates cached bootstrap chunks without
730
+ * requiring table data to change.
731
+ */
732
+ snapshotChunkCacheSchemaVersion?: number | string | null;
733
+ /**
734
+ * Optional server plugins for protocol-level pull transforms.
735
+ */
736
+ plugins?: readonly SyncServerPullPlugin<DB, Auth>[];
290
737
  }): Promise<PullResult> {
291
738
  const { request, dialect } = args;
292
739
  const db = args.db;
740
+ const pullPlugins = sortServerPullPlugins(args.plugins);
293
741
  const partitionId = args.auth.partitionId ?? 'default';
742
+ const snapshotChunkGzipLevel = sanitizeGzipLevel(args.snapshotChunkGzipLevel);
743
+ const clientSchemaVersion = request.schemaVersion;
744
+ if (!Number.isInteger(clientSchemaVersion) || clientSchemaVersion < 1) {
745
+ throw new Error('Pull request schemaVersion must be a positive integer');
746
+ }
747
+ const snapshotChunkCacheSchemaVersion =
748
+ args.snapshotChunkCacheSchemaVersion === null ||
749
+ args.snapshotChunkCacheSchemaVersion === undefined
750
+ ? clientSchemaVersion
751
+ : `${clientSchemaVersion}:${args.snapshotChunkCacheSchemaVersion}`;
294
752
  const requestedSubscriptionCount = Array.isArray(request.subscriptions)
295
753
  ? request.subscriptions.length
296
754
  : 0;
@@ -308,12 +766,12 @@ export async function pull<
308
766
  async (span) => {
309
767
  try {
310
768
  // Validate and sanitize request limits
311
- const limitCommits = sanitizeLimit(request.limitCommits, 50, 1, 500);
769
+ const limitCommits = sanitizeLimit(request.limitCommits, 1000, 1, 1000);
312
770
  const limitSnapshotRows = sanitizeLimit(
313
771
  request.limitSnapshotRows,
314
772
  1000,
315
773
  1,
316
- 5000
774
+ 50000
317
775
  );
318
776
  const maxSnapshotPages = sanitizeLimit(
319
777
  request.maxSnapshotPages,
@@ -322,8 +780,13 @@ export async function pull<
322
780
  50
323
781
  );
324
782
  const dedupeRows = request.dedupeRows === true;
325
- const pendingExternalChunkWrites: PendingExternalChunkWrite[] = [];
326
-
783
+ const snapshotChunkEncoding = SYNC_SNAPSHOT_CHUNK_ENCODING;
784
+ const snapshotArtifactSelection = resolveSnapshotArtifactSelection(
785
+ request.snapshotArtifacts,
786
+ clientSchemaVersion
787
+ );
788
+ const snapshotArtifactSchemaVersion =
789
+ snapshotArtifactSelection?.schemaVersion ?? null;
327
790
  // Resolve effective scopes for each subscription
328
791
  const resolved = await resolveEffectiveScopesForSubscriptions({
329
792
  db,
@@ -333,591 +796,1041 @@ export async function pull<
333
796
  scopeCache: args.scopeCache ?? defaultScopeCache,
334
797
  });
335
798
 
336
- const result = await dialect.executeInTransaction(db, async (trx) => {
337
- await dialect.setRepeatableRead(trx);
338
-
339
- const maxCommitSeq = await dialect.readMaxCommitSeq(trx, {
340
- partitionId,
341
- });
342
- const minCommitSeq = await dialect.readMinCommitSeq(trx, {
343
- partitionId,
344
- });
345
-
346
- const subResponses: SyncPullSubscriptionResponse[] = [];
347
- const activeSubscriptions: { scopes: ScopeValues }[] = [];
348
- const nextCursors: number[] = [];
349
-
350
- // Detect external data changes (synthetic commits from notifyExternalDataChange)
351
- // Compute minimum cursor across all active subscriptions to scope the query.
352
- let minSubCursor = Number.MAX_SAFE_INTEGER;
353
- for (const sub of resolved) {
354
- if (
355
- sub.status === 'revoked' ||
356
- Object.keys(sub.scopes).length === 0
357
- )
358
- continue;
359
- const cursor = Math.max(-1, sub.cursor ?? -1);
360
- if (cursor >= 0 && cursor < minSubCursor) {
361
- minSubCursor = cursor;
362
- }
363
- }
364
-
365
- const externalDataChanges =
366
- minSubCursor < Number.MAX_SAFE_INTEGER && minSubCursor >= 0
367
- ? await readExternalDataChanges(trx, dialect, {
799
+ for (
800
+ let attemptIndex = 0;
801
+ attemptIndex < MAX_PULL_TRANSACTION_RETRIES;
802
+ attemptIndex += 1
803
+ ) {
804
+ const pendingExternalChunkWrites: PendingExternalChunkWrite[] = [];
805
+ const bootstrapTimings = createPullBootstrapTimings();
806
+
807
+ try {
808
+ const result = await dialect.executeInTransaction(
809
+ db,
810
+ async (trx) => {
811
+ await dialect.setRepeatableRead(trx);
812
+
813
+ const maxCommitSeq = await dialect.readMaxCommitSeq(trx, {
368
814
  partitionId,
369
- afterCursor: minSubCursor,
370
- })
371
- : [];
372
- const maxExternalCommitByTable = new Map<string, number>();
373
- for (const change of externalDataChanges) {
374
- for (const table of change.tables) {
375
- const previous = maxExternalCommitByTable.get(table) ?? -1;
376
- if (change.commitSeq > previous) {
377
- maxExternalCommitByTable.set(table, change.commitSeq);
378
- }
379
- }
380
- }
381
-
382
- for (const sub of resolved) {
383
- const cursor = Math.max(-1, sub.cursor ?? -1);
384
- // Validate table handler exists (throws if not registered)
385
- getServerHandlerOrThrow(args.handlers, sub.table);
815
+ });
816
+ const minCommitSeq = await dialect.readMinCommitSeq(trx, {
817
+ partitionId,
818
+ });
386
819
 
387
- if (
388
- sub.status === 'revoked' ||
389
- Object.keys(sub.scopes).length === 0
390
- ) {
391
- subResponses.push({
392
- id: sub.id,
393
- status: 'revoked',
394
- scopes: {},
395
- bootstrap: false,
396
- nextCursor: cursor,
397
- commits: [],
398
- });
399
- continue;
400
- }
820
+ const subResponses: SyncPullSubscriptionResponse[] = [];
821
+ const activeSubscriptions: { scopes: ScopeValues }[] = [];
822
+ const nextCursors: number[] = [];
823
+
824
+ // Detect external data changes (synthetic commits from notifyExternalDataChange)
825
+ // Compute minimum cursor across all active subscriptions to scope the query.
826
+ let minSubCursor = Number.MAX_SAFE_INTEGER;
827
+ const activeTables = new Set<string>();
828
+ for (const sub of resolved) {
829
+ if (
830
+ sub.status === 'revoked' ||
831
+ Object.keys(sub.scopes).length === 0
832
+ )
833
+ continue;
834
+ activeTables.add(sub.table);
835
+ const cursor = Math.max(-1, sub.cursor ?? -1);
836
+ if (cursor >= 0 && cursor < minSubCursor) {
837
+ minSubCursor = cursor;
838
+ }
839
+ }
401
840
 
402
- const effectiveScopes = sub.scopes;
403
- activeSubscriptions.push({ scopes: effectiveScopes });
404
- const latestExternalCommitForTable = maxExternalCommitByTable.get(
405
- sub.table
406
- );
841
+ const maxExternalCommitByTable =
842
+ minSubCursor < Number.MAX_SAFE_INTEGER && minSubCursor >= 0
843
+ ? await readLatestExternalCommitByTable(trx, {
844
+ partitionId,
845
+ afterCursor: minSubCursor,
846
+ tables: Array.from(activeTables),
847
+ })
848
+ : new Map<string, number>();
849
+
850
+ for (const sub of resolved) {
851
+ const cursor = Math.max(-1, sub.cursor ?? -1);
852
+ // Validate table handler exists (throws if not registered)
853
+ if (!args.handlers.byTable.has(sub.table)) {
854
+ throw new Error(`Unknown table: ${sub.table}`);
855
+ }
407
856
 
408
- const needsBootstrap =
409
- sub.bootstrapState != null ||
410
- cursor < 0 ||
411
- cursor > maxCommitSeq ||
412
- (minCommitSeq > 0 && cursor < minCommitSeq - 1) ||
413
- (latestExternalCommitForTable !== undefined &&
414
- latestExternalCommitForTable > cursor);
415
-
416
- if (needsBootstrap) {
417
- const tables = getServerBootstrapOrderFor(
418
- args.handlers,
419
- sub.table
420
- ).map((handler) => handler.table);
421
-
422
- const initState: SyncBootstrapState = {
423
- asOfCommitSeq: maxCommitSeq,
424
- tables,
425
- tableIndex: 0,
426
- rowCursor: null,
427
- };
428
-
429
- const requestedState = sub.bootstrapState ?? null;
430
- const state =
431
- requestedState &&
432
- typeof requestedState.asOfCommitSeq === 'number' &&
433
- Array.isArray(requestedState.tables) &&
434
- typeof requestedState.tableIndex === 'number'
435
- ? (requestedState as SyncBootstrapState)
436
- : initState;
437
-
438
- // If the bootstrap state's asOfCommitSeq is no longer catch-up-able, restart bootstrap.
439
- const effectiveState =
440
- state.asOfCommitSeq < minCommitSeq - 1 ? initState : state;
441
-
442
- const tableName =
443
- effectiveState.tables[effectiveState.tableIndex];
444
-
445
- // No tables (or ran past the end): treat bootstrap as complete.
446
- if (!tableName) {
447
- subResponses.push({
448
- id: sub.id,
449
- status: 'active',
450
- scopes: effectiveScopes,
451
- bootstrap: true,
452
- bootstrapState: null,
453
- nextCursor: effectiveState.asOfCommitSeq,
454
- commits: [],
455
- snapshots: [],
456
- });
457
- nextCursors.push(effectiveState.asOfCommitSeq);
458
- continue;
459
- }
857
+ if (
858
+ sub.status === 'revoked' ||
859
+ Object.keys(sub.scopes).length === 0
860
+ ) {
861
+ subResponses.push({
862
+ id: sub.id,
863
+ status: 'revoked',
864
+ scopes: {},
865
+ bootstrap: false,
866
+ nextCursor: cursor,
867
+ commits: [],
868
+ });
869
+ continue;
870
+ }
460
871
 
461
- const snapshots: SyncSnapshot[] = [];
462
- let nextState: SyncBootstrapState | null = effectiveState;
463
- const cacheKey = `${partitionId}:${await scopesToCacheKey(effectiveScopes)}`;
464
-
465
- interface SnapshotBundle {
466
- table: string;
467
- startCursor: string | null;
468
- isFirstPage: boolean;
469
- isLastPage: boolean;
470
- pageCount: number;
471
- ttlMs: number;
472
- rowFrameParts: Uint8Array[];
473
- }
872
+ const effectiveScopes = sub.scopes;
873
+ activeSubscriptions.push({ scopes: effectiveScopes });
874
+ const latestExternalCommitForTable =
875
+ maxExternalCommitByTable.get(sub.table);
876
+
877
+ const needsBootstrap =
878
+ sub.bootstrapState != null ||
879
+ cursor < 0 ||
880
+ cursor > maxCommitSeq ||
881
+ (minCommitSeq > 0 && cursor < minCommitSeq - 1) ||
882
+ (latestExternalCommitForTable !== undefined &&
883
+ latestExternalCommitForTable > cursor);
884
+
885
+ if (needsBootstrap) {
886
+ const tables = getServerBootstrapOrderFor(
887
+ args.handlers,
888
+ sub.table
889
+ ).map((handler) => handler.table);
890
+ const initState: SyncBootstrapState = {
891
+ asOfCommitSeq: maxCommitSeq,
892
+ tables,
893
+ tableIndex: 0,
894
+ rowCursor: null,
895
+ };
474
896
 
475
- const flushSnapshotBundle = async (
476
- bundle: SnapshotBundle
477
- ): Promise<void> => {
478
- const nowIso = new Date().toISOString();
479
- const bundleRowLimit = Math.max(
480
- 1,
481
- limitSnapshotRows * bundle.pageCount
482
- );
897
+ const requestedState = sub.bootstrapState ?? null;
898
+ const state =
899
+ requestedState &&
900
+ typeof requestedState.asOfCommitSeq === 'number' &&
901
+ Array.isArray(requestedState.tables) &&
902
+ typeof requestedState.tableIndex === 'number'
903
+ ? (requestedState as SyncBootstrapState)
904
+ : initState;
905
+
906
+ // If the bootstrap state's asOfCommitSeq is no longer catch-up-able, restart bootstrap.
907
+ const effectiveState =
908
+ state.asOfCommitSeq < minCommitSeq - 1
909
+ ? initState
910
+ : state;
911
+
912
+ const tableName =
913
+ effectiveState.tables[effectiveState.tableIndex];
914
+
915
+ // No tables (or ran past the end): treat bootstrap as complete.
916
+ if (!tableName) {
917
+ subResponses.push({
918
+ id: sub.id,
919
+ status: 'active',
920
+ scopes: effectiveScopes,
921
+ bootstrap: true,
922
+ bootstrapState: null,
923
+ nextCursor: effectiveState.asOfCommitSeq,
924
+ commits: [],
925
+ snapshots: [],
926
+ });
927
+ nextCursors.push(effectiveState.asOfCommitSeq);
928
+ continue;
929
+ }
930
+
931
+ const snapshots: SyncSnapshot[] = [];
932
+ let nextState: SyncBootstrapState | null = effectiveState;
933
+ const cacheKey = await createSnapshotChunkScopeCacheKey({
934
+ partitionId,
935
+ scopes: effectiveScopes,
936
+ schemaVersion: snapshotChunkCacheSchemaVersion,
937
+ encoding: snapshotChunkEncoding,
938
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
939
+ gzipLevel: snapshotChunkGzipLevel,
940
+ });
941
+ const artifactScopeKey =
942
+ snapshotArtifactSelection && snapshotArtifactSchemaVersion
943
+ ? await createScopedSnapshotArtifactScopeCacheKey({
944
+ partitionId,
945
+ subscriptionId: sub.id,
946
+ scopes: effectiveScopes,
947
+ schemaVersion: snapshotArtifactSchemaVersion,
948
+ artifactKind:
949
+ snapshotArtifactSelection.artifactKind,
950
+ compression: snapshotArtifactSelection.compression,
951
+ features: snapshotArtifactSelection.featureSet,
952
+ })
953
+ : null;
954
+
955
+ interface SnapshotBundle {
956
+ table: string;
957
+ tableIndex: number;
958
+ startCursor: string | null;
959
+ nextRowCursor: string | null;
960
+ isFirstPage: boolean;
961
+ isLastPage: boolean;
962
+ pageCount: number;
963
+ cacheRowLimit: number | null;
964
+ ttlMs: number;
965
+ binaryColumns?: readonly BinarySnapshotColumn[];
966
+ binaryEncoder?: BinarySnapshotRowsEncoder;
967
+ binaryRows: unknown[];
968
+ }
969
+
970
+ const createSnapshotBundle = (
971
+ table: string,
972
+ tableIndex: number,
973
+ rowCursor: string | null,
974
+ ttlMs: number,
975
+ binaryColumns?: readonly BinarySnapshotColumn[],
976
+ binaryEncoder?: BinarySnapshotRowsEncoder
977
+ ): SnapshotBundle => {
978
+ return {
979
+ table,
980
+ tableIndex,
981
+ startCursor: rowCursor,
982
+ nextRowCursor: null,
983
+ isFirstPage: rowCursor == null,
984
+ isLastPage: false,
985
+ pageCount: 0,
986
+ cacheRowLimit: null,
987
+ ttlMs,
988
+ binaryColumns,
989
+ binaryEncoder,
990
+ binaryRows: [],
991
+ };
992
+ };
483
993
 
484
- const cached = await readSnapshotChunkRefByPageKey(trx, {
485
- partitionId,
486
- scopeKey: cacheKey,
487
- scope: bundle.table,
488
- asOfCommitSeq: effectiveState.asOfCommitSeq,
489
- rowCursor: bundle.startCursor,
490
- rowLimit: bundleRowLimit,
491
- encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
492
- compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
493
- nowIso,
494
- });
994
+ const snapshotBootstrapStateAfter = (args: {
995
+ tableIndex: number;
996
+ nextRowCursor: string | null;
997
+ isLastPage: boolean;
998
+ }): SyncBootstrapState | null => {
999
+ if (!args.isLastPage) {
1000
+ return {
1001
+ ...effectiveState,
1002
+ tableIndex: args.tableIndex,
1003
+ rowCursor: args.nextRowCursor,
1004
+ };
1005
+ }
1006
+ if (args.tableIndex + 1 < effectiveState.tables.length) {
1007
+ return {
1008
+ ...effectiveState,
1009
+ tableIndex: args.tableIndex + 1,
1010
+ rowCursor: null,
1011
+ };
1012
+ }
1013
+ return null;
1014
+ };
495
1015
 
496
- let chunkRef = cached;
497
- if (!chunkRef) {
498
- const rowFramePayload = concatByteChunks(
499
- bundle.rowFrameParts
500
- );
501
- const expiresAt = new Date(
502
- Date.now() + Math.max(1000, bundle.ttlMs)
503
- ).toISOString();
504
-
505
- if (args.chunkStorage) {
506
- const snapshot: SyncSnapshot = {
507
- table: bundle.table,
508
- rows: [],
509
- chunks: [],
510
- isFirstPage: bundle.isFirstPage,
511
- isLastPage: bundle.isLastPage,
1016
+ const encodeSnapshotBundlePayload = (
1017
+ bundle: SnapshotBundle
1018
+ ): Uint8Array[] => {
1019
+ const encodeStartedAt = Date.now();
1020
+ const payload = bundle.binaryEncoder
1021
+ ? bundle.binaryEncoder(bundle.binaryRows)
1022
+ : encodeBinarySnapshotRows(
1023
+ bundle.table,
1024
+ bundle.binaryRows,
1025
+ bundle.binaryColumns
1026
+ );
1027
+ bootstrapTimings.binaryEncodeMs += Math.max(
1028
+ 0,
1029
+ Date.now() - encodeStartedAt
1030
+ );
1031
+ return [payload];
512
1032
  };
513
- snapshots.push(snapshot);
514
- pendingExternalChunkWrites.push({
515
- snapshot,
516
- cacheLookup: {
1033
+
1034
+ const flushSnapshotBundle = async (
1035
+ bundle: SnapshotBundle
1036
+ ): Promise<void> => {
1037
+ const nowIso = new Date().toISOString();
1038
+ const bundleRowLimit = Math.max(
1039
+ 1,
1040
+ bundle.cacheRowLimit ??
1041
+ limitSnapshotRows * bundle.pageCount
1042
+ );
1043
+
1044
+ const cacheLookupStartedAt = Date.now();
1045
+ const cached = await readSnapshotChunkRefByPageKey(trx, {
517
1046
  partitionId,
518
1047
  scopeKey: cacheKey,
519
1048
  scope: bundle.table,
520
1049
  asOfCommitSeq: effectiveState.asOfCommitSeq,
521
1050
  rowCursor: bundle.startCursor,
522
1051
  rowLimit: bundleRowLimit,
523
- },
524
- rowFramePayload,
525
- expiresAt,
526
- });
527
- return;
528
- }
529
- const sha256 = await sha256Hex(rowFramePayload);
530
- const compressedBody = await gzipBytes(rowFramePayload);
531
- const chunkId = randomId();
532
- chunkRef = await insertSnapshotChunk(trx, {
533
- chunkId,
534
- partitionId,
535
- scopeKey: cacheKey,
536
- scope: bundle.table,
537
- asOfCommitSeq: effectiveState.asOfCommitSeq,
538
- rowCursor: bundle.startCursor,
539
- rowLimit: bundleRowLimit,
540
- encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
541
- compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
542
- sha256,
543
- body: compressedBody,
544
- expiresAt,
545
- });
546
- }
1052
+ encoding: snapshotChunkEncoding,
1053
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
1054
+ nowIso,
1055
+ });
1056
+ bootstrapTimings.chunkCacheLookupMs += Math.max(
1057
+ 0,
1058
+ Date.now() - cacheLookupStartedAt
1059
+ );
1060
+
1061
+ let chunkRef: SyncSnapshotChunkRef | null = cached;
1062
+ if (!chunkRef) {
1063
+ const expiresAt = new Date(
1064
+ Date.now() + Math.max(1000, bundle.ttlMs)
1065
+ ).toISOString();
1066
+
1067
+ if (args.chunkStorage) {
1068
+ const snapshot: SyncSnapshot = {
1069
+ table: bundle.table,
1070
+ rows: [],
1071
+ chunks: [],
1072
+ isFirstPage: bundle.isFirstPage,
1073
+ isLastPage: bundle.isLastPage,
1074
+ bootstrapStateAfter: snapshotBootstrapStateAfter({
1075
+ tableIndex: bundle.tableIndex,
1076
+ nextRowCursor: bundle.nextRowCursor,
1077
+ isLastPage: bundle.isLastPage,
1078
+ }),
1079
+ };
1080
+ snapshots.push(snapshot);
1081
+ pendingExternalChunkWrites.push({
1082
+ snapshot,
1083
+ cacheLookup: {
1084
+ partitionId,
1085
+ scopeKey: cacheKey,
1086
+ scope: bundle.table,
1087
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
1088
+ rowCursor: bundle.startCursor,
1089
+ rowLimit: bundleRowLimit,
1090
+ nextRowCursor: bundle.nextRowCursor,
1091
+ isLastPage: bundle.isLastPage,
1092
+ },
1093
+ payloadParts: encodeSnapshotBundlePayload(bundle),
1094
+ expiresAt,
1095
+ });
1096
+ return;
1097
+ }
1098
+ const encodedChunk =
1099
+ await encodeCompressedSnapshotChunk(
1100
+ encodeSnapshotBundlePayload(bundle),
1101
+ snapshotChunkGzipLevel
1102
+ );
1103
+ bootstrapTimings.chunkGzipMs += encodedChunk.gzipMs;
1104
+ bootstrapTimings.chunkHashMs += encodedChunk.hashMs;
1105
+ const chunkId = randomId();
1106
+ const chunkPersistStartedAt = Date.now();
1107
+ chunkRef = await insertSnapshotChunk(trx, {
1108
+ chunkId,
1109
+ partitionId,
1110
+ scopeKey: cacheKey,
1111
+ scope: bundle.table,
1112
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
1113
+ rowCursor: bundle.startCursor,
1114
+ rowLimit: bundleRowLimit,
1115
+ nextRowCursor: bundle.nextRowCursor,
1116
+ isLastPage: bundle.isLastPage,
1117
+ encoding: snapshotChunkEncoding,
1118
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
1119
+ sha256: encodedChunk.sha256,
1120
+ body: encodedChunk.body,
1121
+ expiresAt,
1122
+ });
1123
+ bootstrapTimings.chunkPersistMs += Math.max(
1124
+ 0,
1125
+ Date.now() - chunkPersistStartedAt
1126
+ );
1127
+ }
1128
+
1129
+ const chunk = toResponseChunkRef(chunkRef);
1130
+ snapshots.push({
1131
+ table: bundle.table,
1132
+ rows: [],
1133
+ chunks: [chunk],
1134
+ manifest: await createChunkedSnapshotManifest({
1135
+ table: bundle.table,
1136
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
1137
+ scopeKey: cacheKey,
1138
+ rowCursor: bundle.startCursor,
1139
+ rowLimit: bundleRowLimit,
1140
+ nextRowCursor: bundle.nextRowCursor,
1141
+ isFirstPage: bundle.isFirstPage,
1142
+ isLastPage: bundle.isLastPage,
1143
+ chunks: [chunk],
1144
+ }),
1145
+ isFirstPage: bundle.isFirstPage,
1146
+ isLastPage: bundle.isLastPage,
1147
+ bootstrapStateAfter: snapshotBootstrapStateAfter({
1148
+ tableIndex: bundle.tableIndex,
1149
+ nextRowCursor: bundle.nextRowCursor,
1150
+ isLastPage: bundle.isLastPage,
1151
+ }),
1152
+ });
1153
+ };
547
1154
 
548
- snapshots.push({
549
- table: bundle.table,
550
- rows: [],
551
- chunks: [chunkRef],
552
- isFirstPage: bundle.isFirstPage,
553
- isLastPage: bundle.isLastPage,
554
- });
555
- };
556
-
557
- let activeBundle: SnapshotBundle | null = null;
558
-
559
- for (
560
- let pageIndex = 0;
561
- pageIndex < maxSnapshotPages;
562
- pageIndex++
563
- ) {
564
- if (!nextState) break;
565
-
566
- const nextTableName: string | undefined =
567
- nextState.tables[nextState.tableIndex];
568
- if (!nextTableName) {
569
- if (activeBundle) {
570
- activeBundle.isLastPage = true;
571
- await flushSnapshotBundle(activeBundle);
572
- activeBundle = null;
1155
+ let activeBundle: SnapshotBundle | null = null;
1156
+
1157
+ for (
1158
+ let pageIndex = 0;
1159
+ pageIndex < maxSnapshotPages;
1160
+ pageIndex++
1161
+ ) {
1162
+ if (!nextState) break;
1163
+
1164
+ const nextTableName: string | undefined =
1165
+ nextState.tables[nextState.tableIndex];
1166
+ if (!nextTableName) {
1167
+ if (activeBundle) {
1168
+ activeBundle.isLastPage = true;
1169
+ await flushSnapshotBundle(activeBundle);
1170
+ activeBundle = null;
1171
+ }
1172
+ nextState = null;
1173
+ break;
1174
+ }
1175
+
1176
+ const tableHandler =
1177
+ args.handlers.byTable.get(nextTableName);
1178
+ if (!tableHandler) {
1179
+ throw new Error(`Unknown table: ${nextTableName}`);
1180
+ }
1181
+ if (
1182
+ !activeBundle ||
1183
+ activeBundle.table !== nextTableName
1184
+ ) {
1185
+ if (activeBundle) {
1186
+ await flushSnapshotBundle(activeBundle);
1187
+ }
1188
+ activeBundle = createSnapshotBundle(
1189
+ nextTableName,
1190
+ nextState.tableIndex,
1191
+ nextState.rowCursor,
1192
+ tableHandler.snapshotChunkTtlMs ??
1193
+ 24 * 60 * 60 * 1000,
1194
+ resolveSnapshotBinaryColumns(
1195
+ tableHandler,
1196
+ clientSchemaVersion
1197
+ ),
1198
+ resolveSnapshotBinaryEncoder(
1199
+ tableHandler,
1200
+ clientSchemaVersion
1201
+ )
1202
+ );
1203
+ }
1204
+
1205
+ if (artifactScopeKey && activeBundle.pageCount === 0) {
1206
+ const pagesRemaining = Math.max(
1207
+ 1,
1208
+ maxSnapshotPages - pageIndex
1209
+ );
1210
+ const artifactRowLimit =
1211
+ resolveBinarySnapshotBundleRowLimit({
1212
+ limitSnapshotRows,
1213
+ pagesRemaining,
1214
+ });
1215
+ const artifactLookupStartedAt = Date.now();
1216
+ const artifact =
1217
+ await readBestScopedSnapshotArtifactRefForPageCapacity(
1218
+ trx,
1219
+ {
1220
+ partitionId,
1221
+ scopeKey: artifactScopeKey,
1222
+ subscriptionId: sub.id,
1223
+ table: nextTableName,
1224
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
1225
+ rowCursor: nextState.rowCursor,
1226
+ maxRowLimit: artifactRowLimit,
1227
+ artifactKind:
1228
+ snapshotArtifactSelection!.artifactKind,
1229
+ schemaVersion: snapshotArtifactSchemaVersion!,
1230
+ compression:
1231
+ snapshotArtifactSelection!.compression,
1232
+ }
1233
+ );
1234
+ bootstrapTimings.artifactCacheLookupMs += Math.max(
1235
+ 0,
1236
+ Date.now() - artifactLookupStartedAt
1237
+ );
1238
+
1239
+ if (
1240
+ artifact &&
1241
+ (artifact.isLastPage ||
1242
+ artifact.nextRowCursor !== null)
1243
+ ) {
1244
+ snapshots.push({
1245
+ table: nextTableName,
1246
+ rows: [],
1247
+ artifacts: [artifact],
1248
+ isFirstPage: artifact.isFirstPage,
1249
+ isLastPage: artifact.isLastPage,
1250
+ bootstrapStateAfter: snapshotBootstrapStateAfter({
1251
+ tableIndex: nextState.tableIndex,
1252
+ nextRowCursor: artifact.nextRowCursor,
1253
+ isLastPage: artifact.isLastPage,
1254
+ }),
1255
+ });
1256
+ activeBundle = null;
1257
+ const selectedArtifactRowLimit =
1258
+ artifact.manifest.rowLimit;
1259
+ pageIndex +=
1260
+ Math.max(
1261
+ 1,
1262
+ Math.ceil(
1263
+ selectedArtifactRowLimit / limitSnapshotRows
1264
+ )
1265
+ ) - 1;
1266
+
1267
+ nextState = snapshotBootstrapStateAfter({
1268
+ tableIndex: nextState.tableIndex,
1269
+ nextRowCursor: artifact.nextRowCursor,
1270
+ isLastPage: artifact.isLastPage,
1271
+ });
1272
+ continue;
1273
+ }
1274
+ }
1275
+
1276
+ if (activeBundle.pageCount === 0) {
1277
+ const pagesRemaining = Math.max(
1278
+ 1,
1279
+ maxSnapshotPages - pageIndex
1280
+ );
1281
+ const cachedRowLimit =
1282
+ resolveBinarySnapshotBundleRowLimit({
1283
+ limitSnapshotRows,
1284
+ pagesRemaining,
1285
+ });
1286
+ activeBundle.cacheRowLimit = cachedRowLimit;
1287
+ const cacheLookupStartedAt = Date.now();
1288
+ const cached: SnapshotChunkRefWithContinuation | null =
1289
+ await readSnapshotChunkRefByPageKey(trx, {
1290
+ partitionId,
1291
+ scopeKey: cacheKey,
1292
+ scope: nextTableName,
1293
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
1294
+ rowCursor: nextState.rowCursor,
1295
+ rowLimit: cachedRowLimit,
1296
+ encoding: snapshotChunkEncoding,
1297
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
1298
+ });
1299
+ bootstrapTimings.chunkCacheLookupMs += Math.max(
1300
+ 0,
1301
+ Date.now() - cacheLookupStartedAt
1302
+ );
1303
+
1304
+ if (
1305
+ cached &&
1306
+ (cached.isLastPage || cached.nextRowCursor !== null)
1307
+ ) {
1308
+ const chunk = toResponseChunkRef(cached);
1309
+ snapshots.push({
1310
+ table: nextTableName,
1311
+ rows: [],
1312
+ chunks: [chunk],
1313
+ manifest: await createChunkedSnapshotManifest({
1314
+ table: nextTableName,
1315
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
1316
+ scopeKey: cacheKey,
1317
+ rowCursor: nextState.rowCursor,
1318
+ rowLimit: cachedRowLimit,
1319
+ nextRowCursor: cached.nextRowCursor,
1320
+ isFirstPage: nextState.rowCursor == null,
1321
+ isLastPage: cached.isLastPage,
1322
+ chunks: [chunk],
1323
+ }),
1324
+ isFirstPage: nextState.rowCursor == null,
1325
+ isLastPage: cached.isLastPage,
1326
+ bootstrapStateAfter: snapshotBootstrapStateAfter({
1327
+ tableIndex: nextState.tableIndex,
1328
+ nextRowCursor: cached.nextRowCursor,
1329
+ isLastPage: cached.isLastPage,
1330
+ }),
1331
+ });
1332
+ activeBundle = null;
1333
+ pageIndex +=
1334
+ Math.max(
1335
+ 1,
1336
+ Math.ceil(cachedRowLimit / limitSnapshotRows)
1337
+ ) - 1;
1338
+
1339
+ nextState = snapshotBootstrapStateAfter({
1340
+ tableIndex: nextState.tableIndex,
1341
+ nextRowCursor: cached.nextRowCursor,
1342
+ isLastPage: cached.isLastPage,
1343
+ });
1344
+ continue;
1345
+ }
1346
+ }
1347
+
1348
+ const snapshotQueryStartedAt = Date.now();
1349
+ const page: {
1350
+ rows: unknown[];
1351
+ nextCursor: string | null;
1352
+ } = await tableHandler.snapshot(
1353
+ {
1354
+ db: trx,
1355
+ actorId: args.auth.actorId,
1356
+ auth: args.auth,
1357
+ scopeValues: effectiveScopes,
1358
+ cursor: nextState.rowCursor,
1359
+ limit: limitSnapshotRows,
1360
+ schemaVersion: clientSchemaVersion,
1361
+ },
1362
+ sub.params
1363
+ );
1364
+ bootstrapTimings.snapshotQueryMs += Math.max(
1365
+ 0,
1366
+ Date.now() - snapshotQueryStartedAt
1367
+ );
1368
+
1369
+ const pageRows = page.rows ?? [];
1370
+ activeBundle.nextRowCursor = page.nextCursor;
1371
+ activeBundle.binaryRows.push(...pageRows);
1372
+ activeBundle.pageCount += 1;
1373
+
1374
+ if (page.nextCursor != null) {
1375
+ const shouldFlushBinaryBundle =
1376
+ activeBundle.binaryRows.length >=
1377
+ DEFAULT_MAX_BINARY_SNAPSHOT_BUNDLE_ROWS;
1378
+ if (shouldFlushBinaryBundle) {
1379
+ await flushSnapshotBundle(activeBundle);
1380
+ activeBundle = null;
1381
+ }
1382
+ nextState = {
1383
+ ...nextState,
1384
+ rowCursor: page.nextCursor,
1385
+ };
1386
+ continue;
1387
+ }
1388
+
1389
+ activeBundle.isLastPage = true;
1390
+ await flushSnapshotBundle(activeBundle);
1391
+ activeBundle = null;
1392
+
1393
+ if (nextState.tableIndex + 1 < nextState.tables.length) {
1394
+ nextState = {
1395
+ ...nextState,
1396
+ tableIndex: nextState.tableIndex + 1,
1397
+ rowCursor: null,
1398
+ };
1399
+ continue;
1400
+ }
1401
+
1402
+ nextState = null;
1403
+ break;
1404
+ }
1405
+
1406
+ if (activeBundle) {
1407
+ await flushSnapshotBundle(activeBundle);
1408
+ }
1409
+
1410
+ subResponses.push({
1411
+ id: sub.id,
1412
+ status: 'active',
1413
+ scopes: effectiveScopes,
1414
+ bootstrap: true,
1415
+ bootstrapState: nextState,
1416
+ nextCursor: effectiveState.asOfCommitSeq,
1417
+ commits: [],
1418
+ snapshots,
1419
+ });
1420
+ nextCursors.push(effectiveState.asOfCommitSeq);
1421
+ continue;
573
1422
  }
574
- nextState = null;
575
- break;
576
- }
577
1423
 
578
- const tableHandler: ServerTableHandler<DB, Auth> =
579
- getServerHandlerOrThrow(args.handlers, nextTableName);
580
- if (!activeBundle || activeBundle.table !== nextTableName) {
581
- if (activeBundle) {
582
- await flushSnapshotBundle(activeBundle);
583
- }
584
- const bundleHeader = encodeSnapshotRows([]);
585
- activeBundle = {
586
- table: nextTableName,
587
- startCursor: nextState.rowCursor,
588
- isFirstPage: nextState.rowCursor == null,
589
- isLastPage: false,
590
- pageCount: 0,
591
- ttlMs:
592
- tableHandler.snapshotChunkTtlMs ?? 24 * 60 * 60 * 1000,
593
- rowFrameParts: [bundleHeader],
594
- };
595
- }
1424
+ // Incremental pull for this subscription. The dialect row query
1425
+ // carries the scanned commit-window max when matching rows exist,
1426
+ // so we only need a separate commit-window scan when the row query
1427
+ // returns no matches at all.
1428
+ const incrementalRows: IncrementalPullRow[] = [];
1429
+ let maxScannedCommitSeq = cursor;
596
1430
 
597
- const page: { rows: unknown[]; nextCursor: string | null } =
598
- await tableHandler.snapshot(
1431
+ for await (const row of dialect.iterateIncrementalPullRows(
1432
+ trx,
599
1433
  {
600
- db: trx,
601
- actorId: args.auth.actorId,
602
- auth: args.auth,
603
- scopeValues: effectiveScopes,
604
- cursor: nextState.rowCursor,
605
- limit: limitSnapshotRows,
606
- },
607
- sub.params
608
- );
609
-
610
- const rowFrames = encodeSnapshotRowFrames(page.rows ?? []);
611
- activeBundle.rowFrameParts.push(rowFrames);
612
- activeBundle.pageCount += 1;
613
-
614
- if (page.nextCursor != null) {
615
- nextState = { ...nextState, rowCursor: page.nextCursor };
616
- continue;
617
- }
618
-
619
- activeBundle.isLastPage = true;
620
- await flushSnapshotBundle(activeBundle);
621
- activeBundle = null;
622
-
623
- if (nextState.tableIndex + 1 < nextState.tables.length) {
624
- nextState = {
625
- ...nextState,
626
- tableIndex: nextState.tableIndex + 1,
627
- rowCursor: null,
628
- };
629
- continue;
630
- }
1434
+ partitionId,
1435
+ table: sub.table,
1436
+ scopes: effectiveScopes,
1437
+ cursor,
1438
+ limitCommits,
1439
+ }
1440
+ )) {
1441
+ incrementalRows.push(row);
1442
+ maxScannedCommitSeq = Math.max(
1443
+ maxScannedCommitSeq,
1444
+ row.scanned_max_commit_seq ?? row.commit_seq
1445
+ );
1446
+ }
631
1447
 
632
- nextState = null;
633
- break;
634
- }
1448
+ if (incrementalRows.length === 0) {
1449
+ const scannedCommitSeqs =
1450
+ await dialect.readCommitSeqsForPull(trx, {
1451
+ partitionId,
1452
+ cursor,
1453
+ limitCommits,
1454
+ tables: [sub.table],
1455
+ });
1456
+ maxScannedCommitSeq =
1457
+ scannedCommitSeqs.length > 0
1458
+ ? scannedCommitSeqs[scannedCommitSeqs.length - 1]!
1459
+ : cursor;
1460
+
1461
+ if (scannedCommitSeqs.length === 0) {
1462
+ subResponses.push({
1463
+ id: sub.id,
1464
+ status: 'active',
1465
+ scopes: effectiveScopes,
1466
+ bootstrap: false,
1467
+ nextCursor: cursor,
1468
+ commits: [],
1469
+ });
1470
+ nextCursors.push(cursor);
1471
+ continue;
1472
+ }
1473
+ }
635
1474
 
636
- if (activeBundle) {
637
- await flushSnapshotBundle(activeBundle);
638
- }
1475
+ const tableHandler = args.handlers.byTable.get(sub.table);
1476
+ if (!tableHandler) {
1477
+ throw new Error(`Unknown table: ${sub.table}`);
1478
+ }
639
1479
 
640
- subResponses.push({
641
- id: sub.id,
642
- status: 'active',
643
- scopes: effectiveScopes,
644
- bootstrap: true,
645
- bootstrapState: nextState,
646
- nextCursor: effectiveState.asOfCommitSeq,
647
- commits: [],
648
- snapshots,
649
- });
650
- nextCursors.push(effectiveState.asOfCommitSeq);
651
- continue;
652
- }
1480
+ const incrementalItems = incrementalRows.map((r) => ({
1481
+ commitSeq: r.commit_seq,
1482
+ createdAt: r.created_at,
1483
+ actorId: r.actor_id,
1484
+ change: projectPullChangeForClientSchema({
1485
+ handler: tableHandler,
1486
+ schemaVersion: clientSchemaVersion,
1487
+ change: {
1488
+ table: r.table,
1489
+ row_id: r.row_id,
1490
+ op: r.op,
1491
+ row_json: r.row_json,
1492
+ row_version: r.row_version,
1493
+ scopes: r.scopes,
1494
+ } satisfies SyncChange,
1495
+ }),
1496
+ }));
1497
+
1498
+ if (pullPlugins.length > 0 && incrementalItems.length > 0) {
1499
+ const transformedChanges = await transformPullChanges({
1500
+ plugins: pullPlugins,
1501
+ ctx: {
1502
+ db: trx,
1503
+ actorId: args.auth.actorId,
1504
+ auth: args.auth,
1505
+ },
1506
+ tableHandler,
1507
+ subscription: {
1508
+ id: sub.id,
1509
+ table: sub.table,
1510
+ scopes: effectiveScopes,
1511
+ params: sub.params,
1512
+ cursor,
1513
+ crdtStateVectors: sub.crdtStateVectors,
1514
+ },
1515
+ changes: incrementalItems.map((item) => item.change),
1516
+ });
1517
+ for (let i = 0; i < incrementalItems.length; i += 1) {
1518
+ incrementalItems[i]!.change = transformedChanges[i]!;
1519
+ }
1520
+ }
653
1521
 
654
- // Incremental pull for this subscription
655
- // Read the commit window for this table up-front so the subscription cursor
656
- // can advance past commits that don't match the requested scopes.
657
- const scannedCommitSeqs = await dialect.readCommitSeqsForPull(trx, {
658
- partitionId,
659
- cursor,
660
- limitCommits,
661
- tables: [sub.table],
662
- });
663
- const maxScannedCommitSeq =
664
- scannedCommitSeqs.length > 0
665
- ? scannedCommitSeqs[scannedCommitSeqs.length - 1]!
666
- : cursor;
667
-
668
- if (scannedCommitSeqs.length === 0) {
669
- subResponses.push({
670
- id: sub.id,
671
- status: 'active',
672
- scopes: effectiveScopes,
673
- bootstrap: false,
674
- nextCursor: cursor,
675
- commits: [],
676
- });
677
- nextCursors.push(cursor);
678
- continue;
679
- }
1522
+ let nextCursor = cursor;
1523
+
1524
+ if (dedupeRows) {
1525
+ const latestByRowKey = new Map<
1526
+ string,
1527
+ {
1528
+ commitSeq: number;
1529
+ createdAt: string;
1530
+ actorId: string;
1531
+ change: SyncChange;
1532
+ }
1533
+ >();
1534
+
1535
+ for (const item of incrementalItems) {
1536
+ nextCursor = Math.max(nextCursor, item.commitSeq);
1537
+ const rowKey = `${item.change.table}\u0000${item.change.row_id}`;
1538
+
1539
+ // Move row keys to insertion tail so Map iteration yields
1540
+ // "latest change wins" order without a full array sort.
1541
+ if (latestByRowKey.has(rowKey)) {
1542
+ latestByRowKey.delete(rowKey);
1543
+ }
1544
+ latestByRowKey.set(rowKey, {
1545
+ commitSeq: item.commitSeq,
1546
+ createdAt: item.createdAt,
1547
+ actorId: item.actorId,
1548
+ change: item.change,
1549
+ });
1550
+ }
1551
+
1552
+ nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
1553
+
1554
+ if (latestByRowKey.size === 0) {
1555
+ subResponses.push({
1556
+ id: sub.id,
1557
+ status: 'active',
1558
+ scopes: effectiveScopes,
1559
+ bootstrap: false,
1560
+ nextCursor,
1561
+ commits: [],
1562
+ });
1563
+ nextCursors.push(nextCursor);
1564
+ continue;
1565
+ }
1566
+
1567
+ const commits: SyncCommit[] = [];
1568
+ for (const item of latestByRowKey.values()) {
1569
+ const lastCommit = commits[commits.length - 1];
1570
+ if (
1571
+ !lastCommit ||
1572
+ lastCommit.commitSeq !== item.commitSeq
1573
+ ) {
1574
+ commits.push({
1575
+ commitSeq: item.commitSeq,
1576
+ createdAt: item.createdAt,
1577
+ actorId: item.actorId,
1578
+ changes: [item.change],
1579
+ });
1580
+ continue;
1581
+ }
1582
+ lastCommit.changes.push(item.change);
1583
+ }
1584
+ const integrity = await createWireSubscriptionIntegrity({
1585
+ partitionId,
1586
+ subscriptionId: sub.id,
1587
+ previousRoot:
1588
+ typeof sub.verifiedRoot === 'string'
1589
+ ? sub.verifiedRoot
1590
+ : SYNCULAR_COMMIT_GENESIS_ROOT,
1591
+ commits,
1592
+ });
680
1593
 
681
- let nextCursor = cursor;
1594
+ subResponses.push({
1595
+ id: sub.id,
1596
+ status: 'active',
1597
+ scopes: effectiveScopes,
1598
+ bootstrap: false,
1599
+ nextCursor,
1600
+ ...(integrity ? { integrity } : {}),
1601
+ commits,
1602
+ });
1603
+ nextCursors.push(nextCursor);
1604
+ continue;
1605
+ }
682
1606
 
683
- if (dedupeRows) {
684
- const latestByRowKey = new Map<
685
- string,
686
- {
687
- commitSeq: number;
688
- createdAt: string;
689
- actorId: string;
690
- change: SyncChange;
691
- }
692
- >();
693
-
694
- for await (const r of dialect.iterateIncrementalPullRows(trx, {
695
- partitionId,
696
- table: sub.table,
697
- scopes: effectiveScopes,
698
- cursor,
699
- limitCommits,
700
- })) {
701
- nextCursor = Math.max(nextCursor, r.commit_seq);
702
- const rowKey = `${r.table}\u0000${r.row_id}`;
703
- const change: SyncChange = {
704
- table: r.table,
705
- row_id: r.row_id,
706
- op: r.op,
707
- row_json: r.row_json,
708
- row_version: r.row_version,
709
- scopes: r.scopes,
710
- };
1607
+ const commits: SyncCommit[] = [];
1608
+
1609
+ for (const item of incrementalItems) {
1610
+ nextCursor = Math.max(nextCursor, item.commitSeq);
1611
+ const seq = item.commitSeq;
1612
+ let commit = commits[commits.length - 1];
1613
+ if (!commit || commit.commitSeq !== seq) {
1614
+ commit = {
1615
+ commitSeq: seq,
1616
+ createdAt: item.createdAt,
1617
+ actorId: item.actorId,
1618
+ changes: [],
1619
+ };
1620
+ commits.push(commit);
1621
+ }
1622
+
1623
+ commit.changes.push(item.change);
1624
+ }
711
1625
 
712
- // Move row keys to insertion tail so Map iteration yields
713
- // "latest change wins" order without a full array sort.
714
- if (latestByRowKey.has(rowKey)) {
715
- latestByRowKey.delete(rowKey);
716
- }
717
- latestByRowKey.set(rowKey, {
718
- commitSeq: r.commit_seq,
719
- createdAt: r.created_at,
720
- actorId: r.actor_id,
721
- change,
722
- });
723
- }
1626
+ const integrity = await createWireSubscriptionIntegrity({
1627
+ partitionId,
1628
+ subscriptionId: sub.id,
1629
+ previousRoot:
1630
+ typeof sub.verifiedRoot === 'string'
1631
+ ? sub.verifiedRoot
1632
+ : SYNCULAR_COMMIT_GENESIS_ROOT,
1633
+ commits,
1634
+ });
724
1635
 
725
- nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
1636
+ nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
726
1637
 
727
- if (latestByRowKey.size === 0) {
728
- subResponses.push({
729
- id: sub.id,
730
- status: 'active',
731
- scopes: effectiveScopes,
732
- bootstrap: false,
733
- nextCursor,
734
- commits: [],
735
- });
736
- nextCursors.push(nextCursor);
737
- continue;
738
- }
1638
+ if (commits.length === 0) {
1639
+ subResponses.push({
1640
+ id: sub.id,
1641
+ status: 'active',
1642
+ scopes: effectiveScopes,
1643
+ bootstrap: false,
1644
+ nextCursor,
1645
+ commits: [],
1646
+ });
1647
+ nextCursors.push(nextCursor);
1648
+ continue;
1649
+ }
739
1650
 
740
- const commits: SyncCommit[] = [];
741
- for (const item of latestByRowKey.values()) {
742
- const lastCommit = commits[commits.length - 1];
743
- if (!lastCommit || lastCommit.commitSeq !== item.commitSeq) {
744
- commits.push({
745
- commitSeq: item.commitSeq,
746
- createdAt: item.createdAt,
747
- actorId: item.actorId,
748
- changes: [item.change],
1651
+ subResponses.push({
1652
+ id: sub.id,
1653
+ status: 'active',
1654
+ scopes: effectiveScopes,
1655
+ bootstrap: false,
1656
+ nextCursor,
1657
+ ...(integrity ? { integrity } : {}),
1658
+ commits,
749
1659
  });
750
- continue;
1660
+ nextCursors.push(nextCursor);
751
1661
  }
752
- lastCommit.changes.push(item.change);
753
- }
754
-
755
- subResponses.push({
756
- id: sub.id,
757
- status: 'active',
758
- scopes: effectiveScopes,
759
- bootstrap: false,
760
- nextCursor,
761
- commits,
762
- });
763
- nextCursors.push(nextCursor);
764
- continue;
765
- }
766
1662
 
767
- const commitsBySeq = new Map<number, SyncCommit>();
768
- const commitSeqs: number[] = [];
769
-
770
- for await (const r of dialect.iterateIncrementalPullRows(trx, {
771
- partitionId,
772
- table: sub.table,
773
- scopes: effectiveScopes,
774
- cursor,
775
- limitCommits,
776
- })) {
777
- nextCursor = Math.max(nextCursor, r.commit_seq);
778
- const seq = r.commit_seq;
779
- let commit = commitsBySeq.get(seq);
780
- if (!commit) {
781
- commit = {
782
- commitSeq: seq,
783
- createdAt: r.created_at,
784
- actorId: r.actor_id,
785
- changes: [],
1663
+ const effectiveScopes = mergeScopes(activeSubscriptions);
1664
+ const clientCursor =
1665
+ nextCursors.length > 0
1666
+ ? Math.min(...nextCursors)
1667
+ : maxCommitSeq;
1668
+
1669
+ return {
1670
+ response: {
1671
+ ok: true as const,
1672
+ subscriptions: subResponses,
1673
+ },
1674
+ effectiveScopes,
1675
+ clientCursor,
786
1676
  };
787
- commitsBySeq.set(seq, commit);
788
- commitSeqs.push(seq);
789
1677
  }
1678
+ );
790
1679
 
791
- const change: SyncChange = {
792
- table: r.table,
793
- row_id: r.row_id,
794
- op: r.op,
795
- row_json: r.row_json,
796
- row_version: r.row_version,
797
- scopes: r.scopes,
798
- };
799
- commit.changes.push(change);
800
- }
1680
+ const chunkStorage = args.chunkStorage;
1681
+ if (chunkStorage && pendingExternalChunkWrites.length > 0) {
1682
+ await runWithConcurrency(
1683
+ pendingExternalChunkWrites,
1684
+ 4,
1685
+ async (pending) => {
1686
+ const cacheLookupStartedAt = Date.now();
1687
+ let chunkRef: SyncSnapshotChunkRef | null =
1688
+ await readSnapshotChunkRefByPageKey(db, {
1689
+ partitionId: pending.cacheLookup.partitionId,
1690
+ scopeKey: pending.cacheLookup.scopeKey,
1691
+ scope: pending.cacheLookup.scope,
1692
+ asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
1693
+ rowCursor: pending.cacheLookup.rowCursor,
1694
+ rowLimit: pending.cacheLookup.rowLimit,
1695
+ encoding: snapshotChunkEncoding,
1696
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
1697
+ });
1698
+ bootstrapTimings.chunkCacheLookupMs += Math.max(
1699
+ 0,
1700
+ Date.now() - cacheLookupStartedAt
1701
+ );
801
1702
 
802
- nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
803
-
804
- if (commitSeqs.length === 0) {
805
- subResponses.push({
806
- id: sub.id,
807
- status: 'active',
808
- scopes: effectiveScopes,
809
- bootstrap: false,
810
- nextCursor,
811
- commits: [],
812
- });
813
- nextCursors.push(nextCursor);
814
- continue;
1703
+ if (!chunkRef) {
1704
+ if (chunkStorage.storeChunkStream) {
1705
+ const {
1706
+ stream: bodyStream,
1707
+ byteLength,
1708
+ sha256,
1709
+ gzipMs,
1710
+ hashMs,
1711
+ } = await encodeCompressedSnapshotChunkToStream(
1712
+ pending.payloadParts,
1713
+ snapshotChunkGzipLevel
1714
+ );
1715
+ bootstrapTimings.chunkGzipMs += gzipMs;
1716
+ bootstrapTimings.chunkHashMs += hashMs;
1717
+ const chunkPersistStartedAt = Date.now();
1718
+ chunkRef = await chunkStorage.storeChunkStream({
1719
+ partitionId: pending.cacheLookup.partitionId,
1720
+ scopeKey: pending.cacheLookup.scopeKey,
1721
+ scope: pending.cacheLookup.scope,
1722
+ asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
1723
+ rowCursor: pending.cacheLookup.rowCursor,
1724
+ rowLimit: pending.cacheLookup.rowLimit,
1725
+ nextRowCursor: pending.cacheLookup.nextRowCursor,
1726
+ isLastPage: pending.cacheLookup.isLastPage,
1727
+ encoding: snapshotChunkEncoding,
1728
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
1729
+ sha256,
1730
+ byteLength,
1731
+ bodyStream,
1732
+ expiresAt: pending.expiresAt,
1733
+ });
1734
+ bootstrapTimings.chunkPersistMs += Math.max(
1735
+ 0,
1736
+ Date.now() - chunkPersistStartedAt
1737
+ );
1738
+ } else {
1739
+ const encodedChunk = await encodeCompressedSnapshotChunk(
1740
+ pending.payloadParts,
1741
+ snapshotChunkGzipLevel
1742
+ );
1743
+ bootstrapTimings.chunkGzipMs += encodedChunk.gzipMs;
1744
+ bootstrapTimings.chunkHashMs += encodedChunk.hashMs;
1745
+ const chunkPersistStartedAt = Date.now();
1746
+ chunkRef = await chunkStorage.storeChunk({
1747
+ partitionId: pending.cacheLookup.partitionId,
1748
+ scopeKey: pending.cacheLookup.scopeKey,
1749
+ scope: pending.cacheLookup.scope,
1750
+ asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
1751
+ rowCursor: pending.cacheLookup.rowCursor,
1752
+ rowLimit: pending.cacheLookup.rowLimit,
1753
+ nextRowCursor: pending.cacheLookup.nextRowCursor,
1754
+ isLastPage: pending.cacheLookup.isLastPage,
1755
+ encoding: snapshotChunkEncoding,
1756
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
1757
+ sha256: encodedChunk.sha256,
1758
+ body: encodedChunk.body,
1759
+ expiresAt: pending.expiresAt,
1760
+ });
1761
+ bootstrapTimings.chunkPersistMs += Math.max(
1762
+ 0,
1763
+ Date.now() - chunkPersistStartedAt
1764
+ );
1765
+ }
1766
+ }
1767
+
1768
+ const chunk = toResponseChunkRef(chunkRef);
1769
+ pending.snapshot.chunks = [chunk];
1770
+ pending.snapshot.manifest =
1771
+ await createChunkedSnapshotManifest({
1772
+ table: pending.snapshot.table,
1773
+ asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
1774
+ scopeKey: pending.cacheLookup.scopeKey,
1775
+ rowCursor: pending.cacheLookup.rowCursor,
1776
+ rowLimit: pending.cacheLookup.rowLimit,
1777
+ nextRowCursor: pending.cacheLookup.nextRowCursor,
1778
+ isFirstPage: pending.snapshot.isFirstPage,
1779
+ isLastPage: pending.cacheLookup.isLastPage,
1780
+ chunks: [chunk],
1781
+ });
1782
+ }
1783
+ );
815
1784
  }
816
1785
 
817
- const commits: SyncCommit[] = commitSeqs
818
- .map((seq) => commitsBySeq.get(seq))
819
- .filter((c): c is SyncCommit => !!c)
820
- .filter((c) => c.changes.length > 0);
821
-
822
- subResponses.push({
823
- id: sub.id,
824
- status: 'active',
825
- scopes: effectiveScopes,
826
- bootstrap: false,
827
- nextCursor,
828
- commits,
1786
+ const durationMs = Math.max(0, Date.now() - startedAtMs);
1787
+ const stats = summarizePullResponse(result.response);
1788
+
1789
+ span.setAttribute('status', 'ok');
1790
+ span.setAttribute('duration_ms', durationMs);
1791
+ span.setAttribute('subscription_count', stats.subscriptionCount);
1792
+ span.setAttribute('commit_count', stats.commitCount);
1793
+ span.setAttribute('change_count', stats.changeCount);
1794
+ span.setAttribute('snapshot_page_count', stats.snapshotPageCount);
1795
+ span.setAttributes({
1796
+ bootstrap_snapshot_query_ms: bootstrapTimings.snapshotQueryMs,
1797
+ bootstrap_snapshot_binary_encode_ms:
1798
+ bootstrapTimings.binaryEncodeMs,
1799
+ bootstrap_chunk_cache_lookup_ms:
1800
+ bootstrapTimings.chunkCacheLookupMs,
1801
+ bootstrap_artifact_cache_lookup_ms:
1802
+ bootstrapTimings.artifactCacheLookupMs,
1803
+ bootstrap_chunk_gzip_ms: bootstrapTimings.chunkGzipMs,
1804
+ bootstrap_chunk_hash_ms: bootstrapTimings.chunkHashMs,
1805
+ bootstrap_chunk_persist_ms: bootstrapTimings.chunkPersistMs,
829
1806
  });
830
- nextCursors.push(nextCursor);
831
- }
1807
+ span.setStatus('ok');
832
1808
 
833
- const effectiveScopes = mergeScopes(activeSubscriptions);
834
- const clientCursor =
835
- nextCursors.length > 0 ? Math.min(...nextCursors) : maxCommitSeq;
836
-
837
- return {
838
- response: {
839
- ok: true as const,
840
- subscriptions: subResponses,
841
- },
842
- effectiveScopes,
843
- clientCursor,
844
- };
845
- });
846
-
847
- const chunkStorage = args.chunkStorage;
848
- if (chunkStorage && pendingExternalChunkWrites.length > 0) {
849
- for (const pending of pendingExternalChunkWrites) {
850
- let chunkRef = await readSnapshotChunkRefByPageKey(db, {
851
- partitionId: pending.cacheLookup.partitionId,
852
- scopeKey: pending.cacheLookup.scopeKey,
853
- scope: pending.cacheLookup.scope,
854
- asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
855
- rowCursor: pending.cacheLookup.rowCursor,
856
- rowLimit: pending.cacheLookup.rowLimit,
857
- encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
858
- compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
1809
+ recordPullMetrics({
1810
+ status: 'ok',
1811
+ dedupeRows,
1812
+ durationMs,
1813
+ stats,
859
1814
  });
860
1815
 
861
- if (!chunkRef) {
862
- const sha256 = await sha256Hex(pending.rowFramePayload);
863
- if (chunkStorage.storeChunkStream) {
864
- const { stream: bodyStream, byteLength } =
865
- await gzipBytesToStream(pending.rowFramePayload);
866
- chunkRef = await chunkStorage.storeChunkStream({
867
- partitionId: pending.cacheLookup.partitionId,
868
- scopeKey: pending.cacheLookup.scopeKey,
869
- scope: pending.cacheLookup.scope,
870
- asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
871
- rowCursor: pending.cacheLookup.rowCursor,
872
- rowLimit: pending.cacheLookup.rowLimit,
873
- encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
874
- compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
875
- sha256,
876
- byteLength,
877
- bodyStream,
878
- expiresAt: pending.expiresAt,
879
- });
880
- } else {
881
- const compressedBody = await gzipBytes(pending.rowFramePayload);
882
- chunkRef = await chunkStorage.storeChunk({
883
- partitionId: pending.cacheLookup.partitionId,
884
- scopeKey: pending.cacheLookup.scopeKey,
885
- scope: pending.cacheLookup.scope,
886
- asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
887
- rowCursor: pending.cacheLookup.rowCursor,
888
- rowLimit: pending.cacheLookup.rowLimit,
889
- encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
890
- compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
891
- sha256,
892
- body: compressedBody,
893
- expiresAt: pending.expiresAt,
894
- });
895
- }
1816
+ return {
1817
+ ...result,
1818
+ bootstrapTimings,
1819
+ };
1820
+ } catch (error) {
1821
+ if (
1822
+ error instanceof Error &&
1823
+ attemptIndex < MAX_PULL_TRANSACTION_RETRIES - 1 &&
1824
+ isSerializablePullError(error)
1825
+ ) {
1826
+ await delay(PULL_TRANSACTION_RETRY_DELAY_MS * (attemptIndex + 1));
1827
+ continue;
896
1828
  }
897
-
898
- pending.snapshot.chunks = [chunkRef];
1829
+ throw error;
899
1830
  }
900
1831
  }
901
1832
 
902
- const durationMs = Math.max(0, Date.now() - startedAtMs);
903
- const stats = summarizePullResponse(result.response);
904
-
905
- span.setAttribute('status', 'ok');
906
- span.setAttribute('duration_ms', durationMs);
907
- span.setAttribute('subscription_count', stats.subscriptionCount);
908
- span.setAttribute('commit_count', stats.commitCount);
909
- span.setAttribute('change_count', stats.changeCount);
910
- span.setAttribute('snapshot_page_count', stats.snapshotPageCount);
911
- span.setStatus('ok');
912
-
913
- recordPullMetrics({
914
- status: 'ok',
915
- dedupeRows,
916
- durationMs,
917
- stats,
918
- });
919
-
920
- return result;
1833
+ throw new Error('Pull transaction retry loop exhausted unexpectedly');
921
1834
  } catch (error) {
922
1835
  const durationMs = Math.max(0, Date.now() - startedAtMs);
923
1836