@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/dist/pull.js CHANGED
@@ -1,38 +1,200 @@
1
- import { captureSyncException, countSyncMetric, distributionSyncMetric, encodeSnapshotRowFrames, encodeSnapshotRows, gzipBytes, gzipBytesToStream, randomId, SYNC_SNAPSHOT_CHUNK_COMPRESSION, SYNC_SNAPSHOT_CHUNK_ENCODING, sha256Hex, startSyncSpan, } from '@syncular/core';
2
- import { getServerBootstrapOrderFor, getServerHandlerOrThrow, } from './handlers/collection.js';
3
- import { EXTERNAL_CLIENT_ID } from './notify.js';
4
- import { insertSnapshotChunk, readSnapshotChunkRefByPageKey, } from './snapshot-chunks.js';
5
- import { createMemoryScopeCache, } from './subscriptions/cache.js';
6
- import { resolveEffectiveScopesForSubscriptions } from './subscriptions/resolve.js';
1
+ import { bytesToReadableStream, captureSyncException, concatByteChunks, countSyncMetric, createSnapshotManifest, distributionSyncMetric, encodeBinarySnapshotTable, gzipBytes, randomId, SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1, SYNC_SNAPSHOT_CHUNK_COMPRESSION, SYNC_SNAPSHOT_CHUNK_ENCODING, sha256Hex, snapshotScopeDigestFromCacheKey, startSyncSpan, } from '@syncular/core';
2
+ import { createWireSubscriptionIntegrity, SYNCULAR_COMMIT_GENESIS_ROOT, } from './commit-integrity';
3
+ import { getServerBootstrapOrderFor, } from './handlers/collection';
4
+ import { EXTERNAL_CLIENT_ID } from './notify';
5
+ import { sortServerPullPlugins, } from './plugins/types';
6
+ import { createScopedSnapshotArtifactScopeCacheKey, readBestScopedSnapshotArtifactRefForPageCapacity, } from './snapshot-artifacts';
7
+ import { createSnapshotChunkScopeCacheKey, insertSnapshotChunk, readSnapshotChunkRefByPageKey, } from './snapshot-chunks';
8
+ import { createMemoryScopeCache, } from './subscriptions/cache';
9
+ import { resolveEffectiveScopesForSubscriptions } from './subscriptions/resolve';
7
10
  const defaultScopeCache = createMemoryScopeCache();
8
- function concatByteChunks(chunks) {
9
- if (chunks.length === 1) {
10
- return chunks[0] ?? new Uint8Array();
11
+ const DEFAULT_MAX_BINARY_SNAPSHOT_BUNDLE_ROWS = 50_000;
12
+ const DEFAULT_SNAPSHOT_CHUNK_GZIP_LEVEL = 1;
13
+ const MAX_PULL_TRANSACTION_RETRIES = 2;
14
+ const PULL_TRANSACTION_RETRY_DELAY_MS = 15;
15
+ function toResponseChunkRef(ref) {
16
+ return {
17
+ id: ref.id,
18
+ byteLength: ref.byteLength,
19
+ sha256: ref.sha256,
20
+ encoding: ref.encoding,
21
+ compression: ref.compression,
22
+ };
23
+ }
24
+ async function createChunkedSnapshotManifest(args) {
25
+ return createSnapshotManifest({
26
+ version: 1,
27
+ table: args.table,
28
+ asOfCommitSeq: args.asOfCommitSeq,
29
+ scopeDigest: snapshotScopeDigestFromCacheKey(args.scopeKey),
30
+ rowCursor: args.rowCursor,
31
+ rowLimit: args.rowLimit,
32
+ nextRowCursor: args.nextRowCursor,
33
+ isFirstPage: args.isFirstPage,
34
+ isLastPage: args.isLastPage,
35
+ chunks: args.chunks.map(toResponseChunkRef),
36
+ });
37
+ }
38
+ function normalizeFeatureSet(features) {
39
+ return Array.from(new Set(features ?? [])).sort();
40
+ }
41
+ function resolveSnapshotArtifactSelection(request, clientSchemaVersion) {
42
+ if (!request)
43
+ return null;
44
+ if (!request.artifactKinds.includes(SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1)) {
45
+ return null;
11
46
  }
12
- let total = 0;
13
- for (const chunk of chunks) {
14
- total += chunk.length;
47
+ if (!request.compressions?.includes(SYNC_SNAPSHOT_CHUNK_COMPRESSION)) {
48
+ return null;
15
49
  }
16
- const merged = new Uint8Array(total);
17
- let offset = 0;
18
- for (const chunk of chunks) {
19
- merged.set(chunk, offset);
20
- offset += chunk.length;
50
+ return {
51
+ artifactKind: SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1,
52
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
53
+ schemaVersion: String(clientSchemaVersion),
54
+ featureSet: normalizeFeatureSet(request.featureSet),
55
+ };
56
+ }
57
+ function resolveSnapshotBinaryColumns(handler, schemaVersion) {
58
+ const versioned = handler.snapshotBinaryColumnsForVersion?.(schemaVersion);
59
+ return versioned === undefined
60
+ ? handler.snapshotBinaryColumns
61
+ : (versioned ?? undefined);
62
+ }
63
+ function resolveSnapshotBinaryEncoder(handler, schemaVersion) {
64
+ const versioned = handler.snapshotBinaryEncoderForVersion?.(schemaVersion);
65
+ return versioned === undefined
66
+ ? handler.snapshotBinaryEncoder
67
+ : (versioned ?? undefined);
68
+ }
69
+ function createPullBootstrapTimings() {
70
+ return {
71
+ snapshotQueryMs: 0,
72
+ binaryEncodeMs: 0,
73
+ chunkCacheLookupMs: 0,
74
+ artifactCacheLookupMs: 0,
75
+ chunkGzipMs: 0,
76
+ chunkHashMs: 0,
77
+ chunkPersistMs: 0,
78
+ };
79
+ }
80
+ function resolveBinarySnapshotBundleRowLimit(args) {
81
+ const pageSize = Math.max(1, args.limitSnapshotRows);
82
+ const pagesRemaining = Math.max(1, args.pagesRemaining);
83
+ const maxBundlePages = Math.max(1, Math.ceil(DEFAULT_MAX_BINARY_SNAPSHOT_BUNDLE_ROWS / pageSize));
84
+ return pageSize * Math.min(pagesRemaining, maxBundlePages);
85
+ }
86
+ async function gzipByteChunks(chunks, gzipLevel) {
87
+ return gzipBytes(concatByteChunks(chunks), {
88
+ level: gzipLevel,
89
+ });
90
+ }
91
+ async function encodeCompressedSnapshotChunk(chunks, gzipLevel) {
92
+ const gzipStartedAt = Date.now();
93
+ const body = await gzipByteChunks(chunks, gzipLevel);
94
+ const gzipMs = Math.max(0, Date.now() - gzipStartedAt);
95
+ const hashStartedAt = Date.now();
96
+ const sha256 = await sha256Hex(body);
97
+ const hashMs = Math.max(0, Date.now() - hashStartedAt);
98
+ return { body, sha256, gzipMs, hashMs };
99
+ }
100
+ async function encodeCompressedSnapshotChunkToStream(chunks, gzipLevel) {
101
+ const encoded = await encodeCompressedSnapshotChunk(chunks, gzipLevel);
102
+ return {
103
+ stream: bytesToReadableStream(encoded.body),
104
+ byteLength: encoded.body.length,
105
+ sha256: encoded.sha256,
106
+ gzipMs: encoded.gzipMs,
107
+ hashMs: encoded.hashMs,
108
+ };
109
+ }
110
+ function encodeBinarySnapshotRows(table, rows, columns) {
111
+ const recordRows = rows.map((row) => toSnapshotRecordRow(table, row));
112
+ return encodeBinarySnapshotTable({
113
+ table,
114
+ columns: columns ?? inferBinarySnapshotColumns(recordRows),
115
+ rows: recordRows,
116
+ });
117
+ }
118
+ function toSnapshotRecordRow(table, row) {
119
+ if (row == null ||
120
+ typeof row !== 'object' ||
121
+ Array.isArray(row) ||
122
+ row instanceof Uint8Array ||
123
+ row instanceof ArrayBuffer) {
124
+ throw new Error(`Cannot encode binary snapshot for table ${table}: snapshot rows must be objects`);
21
125
  }
22
- return merged;
126
+ return row;
23
127
  }
24
- /**
25
- * Generate a stable cache key for snapshot chunks.
26
- */
27
- async function scopesToCacheKey(scopes) {
28
- const sorted = Object.entries(scopes)
29
- .sort(([a], [b]) => a.localeCompare(b))
30
- .map(([k, v]) => {
31
- const arr = Array.isArray(v) ? [...v].sort() : [v];
32
- return `${k}:${arr.join(',')}`;
33
- })
34
- .join('|');
35
- return await sha256Hex(sorted);
128
+ function inferBinarySnapshotColumns(rows) {
129
+ const columns = [];
130
+ const columnsByName = new Map();
131
+ for (const row of rows) {
132
+ for (const name in row) {
133
+ if (!Object.hasOwn(row, name))
134
+ continue;
135
+ const value = row[name];
136
+ let column = columnsByName.get(name);
137
+ if (!column) {
138
+ column = { name, type: null, nullable: false, presentCount: 0 };
139
+ columnsByName.set(name, column);
140
+ columns.push(column);
141
+ }
142
+ column.presentCount += 1;
143
+ if (value == null) {
144
+ column.nullable = true;
145
+ continue;
146
+ }
147
+ column.type = mergeBinarySnapshotColumnTypes(column.type, inferBinarySnapshotColumnType(value));
148
+ }
149
+ }
150
+ return columns.map((column) => ({
151
+ name: column.name,
152
+ type: column.type ?? 'json',
153
+ ...(column.nullable || column.presentCount < rows.length
154
+ ? { nullable: true }
155
+ : {}),
156
+ }));
157
+ }
158
+ function inferBinarySnapshotColumnType(value) {
159
+ if (typeof value === 'string')
160
+ return 'string';
161
+ if (typeof value === 'boolean')
162
+ return 'boolean';
163
+ if (typeof value === 'bigint')
164
+ return 'integer';
165
+ if (typeof value === 'number') {
166
+ return Number.isSafeInteger(value) ? 'integer' : 'float';
167
+ }
168
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
169
+ return 'bytes';
170
+ }
171
+ return 'json';
172
+ }
173
+ function mergeBinarySnapshotColumnTypes(current, next) {
174
+ if (!current || current === next)
175
+ return next;
176
+ if ((current === 'integer' && next === 'float') ||
177
+ (current === 'float' && next === 'integer')) {
178
+ return 'float';
179
+ }
180
+ return 'json';
181
+ }
182
+ async function runWithConcurrency(items, concurrency, worker) {
183
+ if (items.length === 0)
184
+ return;
185
+ const workerCount = Math.max(1, Math.min(concurrency, items.length));
186
+ let nextIndex = 0;
187
+ async function runWorker() {
188
+ while (nextIndex < items.length) {
189
+ const index = nextIndex;
190
+ nextIndex += 1;
191
+ const item = items[index];
192
+ if (item === undefined)
193
+ continue;
194
+ await worker(item);
195
+ }
196
+ }
197
+ await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
36
198
  }
37
199
  /**
38
200
  * Sanitize a numeric limit parameter with bounds checking.
@@ -45,6 +207,22 @@ function sanitizeLimit(value, defaultValue, min, max) {
45
207
  return defaultValue;
46
208
  return Math.max(min, Math.min(max, value));
47
209
  }
210
+ function sanitizeGzipLevel(value) {
211
+ if (value === undefined || value === null || !Number.isFinite(value)) {
212
+ return DEFAULT_SNAPSHOT_CHUNK_GZIP_LEVEL;
213
+ }
214
+ return Math.max(0, Math.min(9, Math.trunc(value)));
215
+ }
216
+ function isSerializablePullError(error) {
217
+ const withCode = error;
218
+ return (withCode.code === '40001' ||
219
+ error.message.toLowerCase().includes('could not serialize access'));
220
+ }
221
+ async function delay(ms) {
222
+ await new Promise((resolve) => {
223
+ setTimeout(resolve, ms);
224
+ });
225
+ }
48
226
  /**
49
227
  * Merge all scope values into a flat ScopeValues for cursor tracking.
50
228
  */
@@ -68,6 +246,45 @@ function mergeScopes(subscriptions) {
68
246
  }
69
247
  return merged;
70
248
  }
249
+ function assertPullChangeIdentityUnchanged(source, before, after) {
250
+ if (before.table !== after.table) {
251
+ throw new Error(`${source} cannot change change.table (${before.table} -> ${after.table})`);
252
+ }
253
+ if (before.row_id !== after.row_id) {
254
+ throw new Error(`${source} cannot change change.row_id (${before.row_id} -> ${after.row_id})`);
255
+ }
256
+ if (before.op !== after.op) {
257
+ throw new Error(`${source} cannot change change.op (${before.op} -> ${after.op})`);
258
+ }
259
+ }
260
+ function projectPullChangeForClientSchema(args) {
261
+ const projected = args.handler.projectChangeForVersion
262
+ ? args.handler.projectChangeForVersion(args.change, args.schemaVersion)
263
+ : args.change;
264
+ assertPullChangeIdentityUnchanged('Server table handler projectChangeForVersion', args.change, projected);
265
+ return projected;
266
+ }
267
+ async function transformPullChanges(args) {
268
+ let changes = [...args.changes];
269
+ for (const plugin of args.plugins) {
270
+ if (!plugin.transformPullChanges)
271
+ continue;
272
+ const nextChanges = await plugin.transformPullChanges({
273
+ ctx: args.ctx,
274
+ tableHandler: args.tableHandler,
275
+ subscription: args.subscription,
276
+ changes,
277
+ });
278
+ if (nextChanges.length !== changes.length) {
279
+ throw new Error(`Server pull plugin "${plugin.name}" cannot change pull change count (${changes.length} -> ${nextChanges.length})`);
280
+ }
281
+ for (let i = 0; i < changes.length; i += 1) {
282
+ assertPullChangeIdentityUnchanged(`Server pull plugin "${plugin.name}"`, changes[i], nextChanges[i]);
283
+ }
284
+ changes = [...nextChanges];
285
+ }
286
+ return changes;
287
+ }
71
288
  function summarizePullResponse(response) {
72
289
  const subscriptions = response.subscriptions ?? [];
73
290
  let activeSubscriptionCount = 0;
@@ -126,29 +343,49 @@ function recordPullMetrics(args) {
126
343
  });
127
344
  distributionSyncMetric('sync.server.pull.snapshot_pages', stats.snapshotPageCount, { attributes });
128
345
  }
129
- /**
130
- * Read synthetic commits created by notifyExternalDataChange() after a given cursor.
131
- * Returns commit_seq and affected tables for each external change commit.
132
- */
133
- async function readExternalDataChanges(trx, dialect, args) {
346
+ async function readLatestExternalCommitByTable(trx, args) {
347
+ const tableNames = Array.from(new Set(args.tables.filter((table) => typeof table === 'string')));
348
+ const latestByTable = new Map();
349
+ if (tableNames.length === 0) {
350
+ return latestByTable;
351
+ }
134
352
  const executor = trx;
135
353
  const rows = await executor
136
- .selectFrom('sync_commits')
137
- .select(['commit_seq', 'affected_tables'])
138
- .where('partition_id', '=', args.partitionId)
139
- .where('client_id', '=', EXTERNAL_CLIENT_ID)
140
- .where('commit_seq', '>', args.afterCursor)
141
- .orderBy('commit_seq', 'asc')
354
+ .selectFrom('sync_table_commits as tc')
355
+ .innerJoin('sync_commits as cm', (join) => join
356
+ .onRef('cm.commit_seq', '=', 'tc.commit_seq')
357
+ .onRef('cm.partition_id', '=', 'tc.partition_id'))
358
+ .select(['tc.table as table'])
359
+ .select((eb) => eb.fn.max('tc.commit_seq').as('latest_commit_seq'))
360
+ .where('tc.partition_id', '=', args.partitionId)
361
+ .where('cm.client_id', '=', EXTERNAL_CLIENT_ID)
362
+ .where('cm.change_count', '=', 0)
363
+ .where('tc.commit_seq', '>', args.afterCursor)
364
+ .where('tc.table', 'in', tableNames)
365
+ .groupBy('tc.table')
142
366
  .execute();
143
- return rows.map((row) => ({
144
- commitSeq: Number(row.commit_seq),
145
- tables: dialect.dbToArray(row.affected_tables),
146
- }));
367
+ for (const row of rows) {
368
+ const commitSeq = Number(row.latest_commit_seq ?? -1);
369
+ if (!Number.isFinite(commitSeq) || commitSeq < 0)
370
+ continue;
371
+ latestByTable.set(row.table, commitSeq);
372
+ }
373
+ return latestByTable;
147
374
  }
148
375
  export async function pull(args) {
149
376
  const { request, dialect } = args;
150
377
  const db = args.db;
378
+ const pullPlugins = sortServerPullPlugins(args.plugins);
151
379
  const partitionId = args.auth.partitionId ?? 'default';
380
+ const snapshotChunkGzipLevel = sanitizeGzipLevel(args.snapshotChunkGzipLevel);
381
+ const clientSchemaVersion = request.schemaVersion;
382
+ if (!Number.isInteger(clientSchemaVersion) || clientSchemaVersion < 1) {
383
+ throw new Error('Pull request schemaVersion must be a positive integer');
384
+ }
385
+ const snapshotChunkCacheSchemaVersion = args.snapshotChunkCacheSchemaVersion === null ||
386
+ args.snapshotChunkCacheSchemaVersion === undefined
387
+ ? clientSchemaVersion
388
+ : `${clientSchemaVersion}:${args.snapshotChunkCacheSchemaVersion}`;
152
389
  const requestedSubscriptionCount = Array.isArray(request.subscriptions)
153
390
  ? request.subscriptions.length
154
391
  : 0;
@@ -163,11 +400,13 @@ export async function pull(args) {
163
400
  }, async (span) => {
164
401
  try {
165
402
  // Validate and sanitize request limits
166
- const limitCommits = sanitizeLimit(request.limitCommits, 50, 1, 500);
167
- const limitSnapshotRows = sanitizeLimit(request.limitSnapshotRows, 1000, 1, 5000);
403
+ const limitCommits = sanitizeLimit(request.limitCommits, 1000, 1, 1000);
404
+ const limitSnapshotRows = sanitizeLimit(request.limitSnapshotRows, 1000, 1, 50000);
168
405
  const maxSnapshotPages = sanitizeLimit(request.maxSnapshotPages, 4, 1, 50);
169
406
  const dedupeRows = request.dedupeRows === true;
170
- const pendingExternalChunkWrites = [];
407
+ const snapshotChunkEncoding = SYNC_SNAPSHOT_CHUNK_ENCODING;
408
+ const snapshotArtifactSelection = resolveSnapshotArtifactSelection(request.snapshotArtifacts, clientSchemaVersion);
409
+ const snapshotArtifactSchemaVersion = snapshotArtifactSelection?.schemaVersion ?? null;
171
410
  // Resolve effective scopes for each subscription
172
411
  const resolved = await resolveEffectiveScopesForSubscriptions({
173
412
  db,
@@ -176,480 +415,781 @@ export async function pull(args) {
176
415
  handlers: args.handlers,
177
416
  scopeCache: args.scopeCache ?? defaultScopeCache,
178
417
  });
179
- const result = await dialect.executeInTransaction(db, async (trx) => {
180
- await dialect.setRepeatableRead(trx);
181
- const maxCommitSeq = await dialect.readMaxCommitSeq(trx, {
182
- partitionId,
183
- });
184
- const minCommitSeq = await dialect.readMinCommitSeq(trx, {
185
- partitionId,
186
- });
187
- const subResponses = [];
188
- const activeSubscriptions = [];
189
- const nextCursors = [];
190
- // Detect external data changes (synthetic commits from notifyExternalDataChange)
191
- // Compute minimum cursor across all active subscriptions to scope the query.
192
- let minSubCursor = Number.MAX_SAFE_INTEGER;
193
- for (const sub of resolved) {
194
- if (sub.status === 'revoked' ||
195
- Object.keys(sub.scopes).length === 0)
196
- continue;
197
- const cursor = Math.max(-1, sub.cursor ?? -1);
198
- if (cursor >= 0 && cursor < minSubCursor) {
199
- minSubCursor = cursor;
200
- }
201
- }
202
- const externalDataChanges = minSubCursor < Number.MAX_SAFE_INTEGER && minSubCursor >= 0
203
- ? await readExternalDataChanges(trx, dialect, {
204
- partitionId,
205
- afterCursor: minSubCursor,
206
- })
207
- : [];
208
- const maxExternalCommitByTable = new Map();
209
- for (const change of externalDataChanges) {
210
- for (const table of change.tables) {
211
- const previous = maxExternalCommitByTable.get(table) ?? -1;
212
- if (change.commitSeq > previous) {
213
- maxExternalCommitByTable.set(table, change.commitSeq);
214
- }
215
- }
216
- }
217
- for (const sub of resolved) {
218
- const cursor = Math.max(-1, sub.cursor ?? -1);
219
- // Validate table handler exists (throws if not registered)
220
- getServerHandlerOrThrow(args.handlers, sub.table);
221
- if (sub.status === 'revoked' ||
222
- Object.keys(sub.scopes).length === 0) {
223
- subResponses.push({
224
- id: sub.id,
225
- status: 'revoked',
226
- scopes: {},
227
- bootstrap: false,
228
- nextCursor: cursor,
229
- commits: [],
418
+ for (let attemptIndex = 0; attemptIndex < MAX_PULL_TRANSACTION_RETRIES; attemptIndex += 1) {
419
+ const pendingExternalChunkWrites = [];
420
+ const bootstrapTimings = createPullBootstrapTimings();
421
+ try {
422
+ const result = await dialect.executeInTransaction(db, async (trx) => {
423
+ await dialect.setRepeatableRead(trx);
424
+ const maxCommitSeq = await dialect.readMaxCommitSeq(trx, {
425
+ partitionId,
230
426
  });
231
- continue;
232
- }
233
- const effectiveScopes = sub.scopes;
234
- activeSubscriptions.push({ scopes: effectiveScopes });
235
- const latestExternalCommitForTable = maxExternalCommitByTable.get(sub.table);
236
- const needsBootstrap = sub.bootstrapState != null ||
237
- cursor < 0 ||
238
- cursor > maxCommitSeq ||
239
- (minCommitSeq > 0 && cursor < minCommitSeq - 1) ||
240
- (latestExternalCommitForTable !== undefined &&
241
- latestExternalCommitForTable > cursor);
242
- if (needsBootstrap) {
243
- const tables = getServerBootstrapOrderFor(args.handlers, sub.table).map((handler) => handler.table);
244
- const initState = {
245
- asOfCommitSeq: maxCommitSeq,
246
- tables,
247
- tableIndex: 0,
248
- rowCursor: null,
249
- };
250
- const requestedState = sub.bootstrapState ?? null;
251
- const state = requestedState &&
252
- typeof requestedState.asOfCommitSeq === 'number' &&
253
- Array.isArray(requestedState.tables) &&
254
- typeof requestedState.tableIndex === 'number'
255
- ? requestedState
256
- : initState;
257
- // If the bootstrap state's asOfCommitSeq is no longer catch-up-able, restart bootstrap.
258
- const effectiveState = state.asOfCommitSeq < minCommitSeq - 1 ? initState : state;
259
- const tableName = effectiveState.tables[effectiveState.tableIndex];
260
- // No tables (or ran past the end): treat bootstrap as complete.
261
- if (!tableName) {
262
- subResponses.push({
263
- id: sub.id,
264
- status: 'active',
265
- scopes: effectiveScopes,
266
- bootstrap: true,
267
- bootstrapState: null,
268
- nextCursor: effectiveState.asOfCommitSeq,
269
- commits: [],
270
- snapshots: [],
271
- });
272
- nextCursors.push(effectiveState.asOfCommitSeq);
273
- continue;
427
+ const minCommitSeq = await dialect.readMinCommitSeq(trx, {
428
+ partitionId,
429
+ });
430
+ const subResponses = [];
431
+ const activeSubscriptions = [];
432
+ const nextCursors = [];
433
+ // Detect external data changes (synthetic commits from notifyExternalDataChange)
434
+ // Compute minimum cursor across all active subscriptions to scope the query.
435
+ let minSubCursor = Number.MAX_SAFE_INTEGER;
436
+ const activeTables = new Set();
437
+ for (const sub of resolved) {
438
+ if (sub.status === 'revoked' ||
439
+ Object.keys(sub.scopes).length === 0)
440
+ continue;
441
+ activeTables.add(sub.table);
442
+ const cursor = Math.max(-1, sub.cursor ?? -1);
443
+ if (cursor >= 0 && cursor < minSubCursor) {
444
+ minSubCursor = cursor;
445
+ }
274
446
  }
275
- const snapshots = [];
276
- let nextState = effectiveState;
277
- const cacheKey = `${partitionId}:${await scopesToCacheKey(effectiveScopes)}`;
278
- const flushSnapshotBundle = async (bundle) => {
279
- const nowIso = new Date().toISOString();
280
- const bundleRowLimit = Math.max(1, limitSnapshotRows * bundle.pageCount);
281
- const cached = await readSnapshotChunkRefByPageKey(trx, {
447
+ const maxExternalCommitByTable = minSubCursor < Number.MAX_SAFE_INTEGER && minSubCursor >= 0
448
+ ? await readLatestExternalCommitByTable(trx, {
282
449
  partitionId,
283
- scopeKey: cacheKey,
284
- scope: bundle.table,
285
- asOfCommitSeq: effectiveState.asOfCommitSeq,
286
- rowCursor: bundle.startCursor,
287
- rowLimit: bundleRowLimit,
288
- encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
289
- compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
290
- nowIso,
291
- });
292
- let chunkRef = cached;
293
- if (!chunkRef) {
294
- const rowFramePayload = concatByteChunks(bundle.rowFrameParts);
295
- const expiresAt = new Date(Date.now() + Math.max(1000, bundle.ttlMs)).toISOString();
296
- if (args.chunkStorage) {
297
- const snapshot = {
298
- table: bundle.table,
299
- rows: [],
300
- chunks: [],
301
- isFirstPage: bundle.isFirstPage,
302
- isLastPage: bundle.isLastPage,
450
+ afterCursor: minSubCursor,
451
+ tables: Array.from(activeTables),
452
+ })
453
+ : new Map();
454
+ for (const sub of resolved) {
455
+ const cursor = Math.max(-1, sub.cursor ?? -1);
456
+ // Validate table handler exists (throws if not registered)
457
+ if (!args.handlers.byTable.has(sub.table)) {
458
+ throw new Error(`Unknown table: ${sub.table}`);
459
+ }
460
+ if (sub.status === 'revoked' ||
461
+ Object.keys(sub.scopes).length === 0) {
462
+ subResponses.push({
463
+ id: sub.id,
464
+ status: 'revoked',
465
+ scopes: {},
466
+ bootstrap: false,
467
+ nextCursor: cursor,
468
+ commits: [],
469
+ });
470
+ continue;
471
+ }
472
+ const effectiveScopes = sub.scopes;
473
+ activeSubscriptions.push({ scopes: effectiveScopes });
474
+ const latestExternalCommitForTable = maxExternalCommitByTable.get(sub.table);
475
+ const needsBootstrap = sub.bootstrapState != null ||
476
+ cursor < 0 ||
477
+ cursor > maxCommitSeq ||
478
+ (minCommitSeq > 0 && cursor < minCommitSeq - 1) ||
479
+ (latestExternalCommitForTable !== undefined &&
480
+ latestExternalCommitForTable > cursor);
481
+ if (needsBootstrap) {
482
+ const tables = getServerBootstrapOrderFor(args.handlers, sub.table).map((handler) => handler.table);
483
+ const initState = {
484
+ asOfCommitSeq: maxCommitSeq,
485
+ tables,
486
+ tableIndex: 0,
487
+ rowCursor: null,
488
+ };
489
+ const requestedState = sub.bootstrapState ?? null;
490
+ const state = requestedState &&
491
+ typeof requestedState.asOfCommitSeq === 'number' &&
492
+ Array.isArray(requestedState.tables) &&
493
+ typeof requestedState.tableIndex === 'number'
494
+ ? requestedState
495
+ : initState;
496
+ // If the bootstrap state's asOfCommitSeq is no longer catch-up-able, restart bootstrap.
497
+ const effectiveState = state.asOfCommitSeq < minCommitSeq - 1
498
+ ? initState
499
+ : state;
500
+ const tableName = effectiveState.tables[effectiveState.tableIndex];
501
+ // No tables (or ran past the end): treat bootstrap as complete.
502
+ if (!tableName) {
503
+ subResponses.push({
504
+ id: sub.id,
505
+ status: 'active',
506
+ scopes: effectiveScopes,
507
+ bootstrap: true,
508
+ bootstrapState: null,
509
+ nextCursor: effectiveState.asOfCommitSeq,
510
+ commits: [],
511
+ snapshots: [],
512
+ });
513
+ nextCursors.push(effectiveState.asOfCommitSeq);
514
+ continue;
515
+ }
516
+ const snapshots = [];
517
+ let nextState = effectiveState;
518
+ const cacheKey = await createSnapshotChunkScopeCacheKey({
519
+ partitionId,
520
+ scopes: effectiveScopes,
521
+ schemaVersion: snapshotChunkCacheSchemaVersion,
522
+ encoding: snapshotChunkEncoding,
523
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
524
+ gzipLevel: snapshotChunkGzipLevel,
525
+ });
526
+ const artifactScopeKey = snapshotArtifactSelection && snapshotArtifactSchemaVersion
527
+ ? await createScopedSnapshotArtifactScopeCacheKey({
528
+ partitionId,
529
+ subscriptionId: sub.id,
530
+ scopes: effectiveScopes,
531
+ schemaVersion: snapshotArtifactSchemaVersion,
532
+ artifactKind: snapshotArtifactSelection.artifactKind,
533
+ compression: snapshotArtifactSelection.compression,
534
+ features: snapshotArtifactSelection.featureSet,
535
+ })
536
+ : null;
537
+ const createSnapshotBundle = (table, tableIndex, rowCursor, ttlMs, binaryColumns, binaryEncoder) => {
538
+ return {
539
+ table,
540
+ tableIndex,
541
+ startCursor: rowCursor,
542
+ nextRowCursor: null,
543
+ isFirstPage: rowCursor == null,
544
+ isLastPage: false,
545
+ pageCount: 0,
546
+ cacheRowLimit: null,
547
+ ttlMs,
548
+ binaryColumns,
549
+ binaryEncoder,
550
+ binaryRows: [],
303
551
  };
304
- snapshots.push(snapshot);
305
- pendingExternalChunkWrites.push({
306
- snapshot,
307
- cacheLookup: {
552
+ };
553
+ const snapshotBootstrapStateAfter = (args) => {
554
+ if (!args.isLastPage) {
555
+ return {
556
+ ...effectiveState,
557
+ tableIndex: args.tableIndex,
558
+ rowCursor: args.nextRowCursor,
559
+ };
560
+ }
561
+ if (args.tableIndex + 1 < effectiveState.tables.length) {
562
+ return {
563
+ ...effectiveState,
564
+ tableIndex: args.tableIndex + 1,
565
+ rowCursor: null,
566
+ };
567
+ }
568
+ return null;
569
+ };
570
+ const encodeSnapshotBundlePayload = (bundle) => {
571
+ const encodeStartedAt = Date.now();
572
+ const payload = bundle.binaryEncoder
573
+ ? bundle.binaryEncoder(bundle.binaryRows)
574
+ : encodeBinarySnapshotRows(bundle.table, bundle.binaryRows, bundle.binaryColumns);
575
+ bootstrapTimings.binaryEncodeMs += Math.max(0, Date.now() - encodeStartedAt);
576
+ return [payload];
577
+ };
578
+ const flushSnapshotBundle = async (bundle) => {
579
+ const nowIso = new Date().toISOString();
580
+ const bundleRowLimit = Math.max(1, bundle.cacheRowLimit ??
581
+ limitSnapshotRows * bundle.pageCount);
582
+ const cacheLookupStartedAt = Date.now();
583
+ const cached = await readSnapshotChunkRefByPageKey(trx, {
584
+ partitionId,
585
+ scopeKey: cacheKey,
586
+ scope: bundle.table,
587
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
588
+ rowCursor: bundle.startCursor,
589
+ rowLimit: bundleRowLimit,
590
+ encoding: snapshotChunkEncoding,
591
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
592
+ nowIso,
593
+ });
594
+ bootstrapTimings.chunkCacheLookupMs += Math.max(0, Date.now() - cacheLookupStartedAt);
595
+ let chunkRef = cached;
596
+ if (!chunkRef) {
597
+ const expiresAt = new Date(Date.now() + Math.max(1000, bundle.ttlMs)).toISOString();
598
+ if (args.chunkStorage) {
599
+ const snapshot = {
600
+ table: bundle.table,
601
+ rows: [],
602
+ chunks: [],
603
+ isFirstPage: bundle.isFirstPage,
604
+ isLastPage: bundle.isLastPage,
605
+ bootstrapStateAfter: snapshotBootstrapStateAfter({
606
+ tableIndex: bundle.tableIndex,
607
+ nextRowCursor: bundle.nextRowCursor,
608
+ isLastPage: bundle.isLastPage,
609
+ }),
610
+ };
611
+ snapshots.push(snapshot);
612
+ pendingExternalChunkWrites.push({
613
+ snapshot,
614
+ cacheLookup: {
615
+ partitionId,
616
+ scopeKey: cacheKey,
617
+ scope: bundle.table,
618
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
619
+ rowCursor: bundle.startCursor,
620
+ rowLimit: bundleRowLimit,
621
+ nextRowCursor: bundle.nextRowCursor,
622
+ isLastPage: bundle.isLastPage,
623
+ },
624
+ payloadParts: encodeSnapshotBundlePayload(bundle),
625
+ expiresAt,
626
+ });
627
+ return;
628
+ }
629
+ const encodedChunk = await encodeCompressedSnapshotChunk(encodeSnapshotBundlePayload(bundle), snapshotChunkGzipLevel);
630
+ bootstrapTimings.chunkGzipMs += encodedChunk.gzipMs;
631
+ bootstrapTimings.chunkHashMs += encodedChunk.hashMs;
632
+ const chunkId = randomId();
633
+ const chunkPersistStartedAt = Date.now();
634
+ chunkRef = await insertSnapshotChunk(trx, {
635
+ chunkId,
308
636
  partitionId,
309
637
  scopeKey: cacheKey,
310
638
  scope: bundle.table,
311
639
  asOfCommitSeq: effectiveState.asOfCommitSeq,
312
640
  rowCursor: bundle.startCursor,
313
641
  rowLimit: bundleRowLimit,
314
- },
315
- rowFramePayload,
316
- expiresAt,
642
+ nextRowCursor: bundle.nextRowCursor,
643
+ isLastPage: bundle.isLastPage,
644
+ encoding: snapshotChunkEncoding,
645
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
646
+ sha256: encodedChunk.sha256,
647
+ body: encodedChunk.body,
648
+ expiresAt,
649
+ });
650
+ bootstrapTimings.chunkPersistMs += Math.max(0, Date.now() - chunkPersistStartedAt);
651
+ }
652
+ const chunk = toResponseChunkRef(chunkRef);
653
+ snapshots.push({
654
+ table: bundle.table,
655
+ rows: [],
656
+ chunks: [chunk],
657
+ manifest: await createChunkedSnapshotManifest({
658
+ table: bundle.table,
659
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
660
+ scopeKey: cacheKey,
661
+ rowCursor: bundle.startCursor,
662
+ rowLimit: bundleRowLimit,
663
+ nextRowCursor: bundle.nextRowCursor,
664
+ isFirstPage: bundle.isFirstPage,
665
+ isLastPage: bundle.isLastPage,
666
+ chunks: [chunk],
667
+ }),
668
+ isFirstPage: bundle.isFirstPage,
669
+ isLastPage: bundle.isLastPage,
670
+ bootstrapStateAfter: snapshotBootstrapStateAfter({
671
+ tableIndex: bundle.tableIndex,
672
+ nextRowCursor: bundle.nextRowCursor,
673
+ isLastPage: bundle.isLastPage,
674
+ }),
317
675
  });
318
- return;
319
- }
320
- const sha256 = await sha256Hex(rowFramePayload);
321
- const compressedBody = await gzipBytes(rowFramePayload);
322
- const chunkId = randomId();
323
- chunkRef = await insertSnapshotChunk(trx, {
324
- chunkId,
325
- partitionId,
326
- scopeKey: cacheKey,
327
- scope: bundle.table,
328
- asOfCommitSeq: effectiveState.asOfCommitSeq,
329
- rowCursor: bundle.startCursor,
330
- rowLimit: bundleRowLimit,
331
- encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
332
- compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
333
- sha256,
334
- body: compressedBody,
335
- expiresAt,
336
- });
337
- }
338
- snapshots.push({
339
- table: bundle.table,
340
- rows: [],
341
- chunks: [chunkRef],
342
- isFirstPage: bundle.isFirstPage,
343
- isLastPage: bundle.isLastPage,
344
- });
345
- };
346
- let activeBundle = null;
347
- for (let pageIndex = 0; pageIndex < maxSnapshotPages; pageIndex++) {
348
- if (!nextState)
349
- break;
350
- const nextTableName = nextState.tables[nextState.tableIndex];
351
- if (!nextTableName) {
352
- if (activeBundle) {
676
+ };
677
+ let activeBundle = null;
678
+ for (let pageIndex = 0; pageIndex < maxSnapshotPages; pageIndex++) {
679
+ if (!nextState)
680
+ break;
681
+ const nextTableName = nextState.tables[nextState.tableIndex];
682
+ if (!nextTableName) {
683
+ if (activeBundle) {
684
+ activeBundle.isLastPage = true;
685
+ await flushSnapshotBundle(activeBundle);
686
+ activeBundle = null;
687
+ }
688
+ nextState = null;
689
+ break;
690
+ }
691
+ const tableHandler = args.handlers.byTable.get(nextTableName);
692
+ if (!tableHandler) {
693
+ throw new Error(`Unknown table: ${nextTableName}`);
694
+ }
695
+ if (!activeBundle ||
696
+ activeBundle.table !== nextTableName) {
697
+ if (activeBundle) {
698
+ await flushSnapshotBundle(activeBundle);
699
+ }
700
+ activeBundle = createSnapshotBundle(nextTableName, nextState.tableIndex, nextState.rowCursor, tableHandler.snapshotChunkTtlMs ??
701
+ 24 * 60 * 60 * 1000, resolveSnapshotBinaryColumns(tableHandler, clientSchemaVersion), resolveSnapshotBinaryEncoder(tableHandler, clientSchemaVersion));
702
+ }
703
+ if (artifactScopeKey && activeBundle.pageCount === 0) {
704
+ const pagesRemaining = Math.max(1, maxSnapshotPages - pageIndex);
705
+ const artifactRowLimit = resolveBinarySnapshotBundleRowLimit({
706
+ limitSnapshotRows,
707
+ pagesRemaining,
708
+ });
709
+ const artifactLookupStartedAt = Date.now();
710
+ const artifact = await readBestScopedSnapshotArtifactRefForPageCapacity(trx, {
711
+ partitionId,
712
+ scopeKey: artifactScopeKey,
713
+ subscriptionId: sub.id,
714
+ table: nextTableName,
715
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
716
+ rowCursor: nextState.rowCursor,
717
+ maxRowLimit: artifactRowLimit,
718
+ artifactKind: snapshotArtifactSelection.artifactKind,
719
+ schemaVersion: snapshotArtifactSchemaVersion,
720
+ compression: snapshotArtifactSelection.compression,
721
+ });
722
+ bootstrapTimings.artifactCacheLookupMs += Math.max(0, Date.now() - artifactLookupStartedAt);
723
+ if (artifact &&
724
+ (artifact.isLastPage ||
725
+ artifact.nextRowCursor !== null)) {
726
+ snapshots.push({
727
+ table: nextTableName,
728
+ rows: [],
729
+ artifacts: [artifact],
730
+ isFirstPage: artifact.isFirstPage,
731
+ isLastPage: artifact.isLastPage,
732
+ bootstrapStateAfter: snapshotBootstrapStateAfter({
733
+ tableIndex: nextState.tableIndex,
734
+ nextRowCursor: artifact.nextRowCursor,
735
+ isLastPage: artifact.isLastPage,
736
+ }),
737
+ });
738
+ activeBundle = null;
739
+ const selectedArtifactRowLimit = artifact.manifest.rowLimit;
740
+ pageIndex +=
741
+ Math.max(1, Math.ceil(selectedArtifactRowLimit / limitSnapshotRows)) - 1;
742
+ nextState = snapshotBootstrapStateAfter({
743
+ tableIndex: nextState.tableIndex,
744
+ nextRowCursor: artifact.nextRowCursor,
745
+ isLastPage: artifact.isLastPage,
746
+ });
747
+ continue;
748
+ }
749
+ }
750
+ if (activeBundle.pageCount === 0) {
751
+ const pagesRemaining = Math.max(1, maxSnapshotPages - pageIndex);
752
+ const cachedRowLimit = resolveBinarySnapshotBundleRowLimit({
753
+ limitSnapshotRows,
754
+ pagesRemaining,
755
+ });
756
+ activeBundle.cacheRowLimit = cachedRowLimit;
757
+ const cacheLookupStartedAt = Date.now();
758
+ const cached = await readSnapshotChunkRefByPageKey(trx, {
759
+ partitionId,
760
+ scopeKey: cacheKey,
761
+ scope: nextTableName,
762
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
763
+ rowCursor: nextState.rowCursor,
764
+ rowLimit: cachedRowLimit,
765
+ encoding: snapshotChunkEncoding,
766
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
767
+ });
768
+ bootstrapTimings.chunkCacheLookupMs += Math.max(0, Date.now() - cacheLookupStartedAt);
769
+ if (cached &&
770
+ (cached.isLastPage || cached.nextRowCursor !== null)) {
771
+ const chunk = toResponseChunkRef(cached);
772
+ snapshots.push({
773
+ table: nextTableName,
774
+ rows: [],
775
+ chunks: [chunk],
776
+ manifest: await createChunkedSnapshotManifest({
777
+ table: nextTableName,
778
+ asOfCommitSeq: effectiveState.asOfCommitSeq,
779
+ scopeKey: cacheKey,
780
+ rowCursor: nextState.rowCursor,
781
+ rowLimit: cachedRowLimit,
782
+ nextRowCursor: cached.nextRowCursor,
783
+ isFirstPage: nextState.rowCursor == null,
784
+ isLastPage: cached.isLastPage,
785
+ chunks: [chunk],
786
+ }),
787
+ isFirstPage: nextState.rowCursor == null,
788
+ isLastPage: cached.isLastPage,
789
+ bootstrapStateAfter: snapshotBootstrapStateAfter({
790
+ tableIndex: nextState.tableIndex,
791
+ nextRowCursor: cached.nextRowCursor,
792
+ isLastPage: cached.isLastPage,
793
+ }),
794
+ });
795
+ activeBundle = null;
796
+ pageIndex +=
797
+ Math.max(1, Math.ceil(cachedRowLimit / limitSnapshotRows)) - 1;
798
+ nextState = snapshotBootstrapStateAfter({
799
+ tableIndex: nextState.tableIndex,
800
+ nextRowCursor: cached.nextRowCursor,
801
+ isLastPage: cached.isLastPage,
802
+ });
803
+ continue;
804
+ }
805
+ }
806
+ const snapshotQueryStartedAt = Date.now();
807
+ const page = await tableHandler.snapshot({
808
+ db: trx,
809
+ actorId: args.auth.actorId,
810
+ auth: args.auth,
811
+ scopeValues: effectiveScopes,
812
+ cursor: nextState.rowCursor,
813
+ limit: limitSnapshotRows,
814
+ schemaVersion: clientSchemaVersion,
815
+ }, sub.params);
816
+ bootstrapTimings.snapshotQueryMs += Math.max(0, Date.now() - snapshotQueryStartedAt);
817
+ const pageRows = page.rows ?? [];
818
+ activeBundle.nextRowCursor = page.nextCursor;
819
+ activeBundle.binaryRows.push(...pageRows);
820
+ activeBundle.pageCount += 1;
821
+ if (page.nextCursor != null) {
822
+ const shouldFlushBinaryBundle = activeBundle.binaryRows.length >=
823
+ DEFAULT_MAX_BINARY_SNAPSHOT_BUNDLE_ROWS;
824
+ if (shouldFlushBinaryBundle) {
825
+ await flushSnapshotBundle(activeBundle);
826
+ activeBundle = null;
827
+ }
828
+ nextState = {
829
+ ...nextState,
830
+ rowCursor: page.nextCursor,
831
+ };
832
+ continue;
833
+ }
353
834
  activeBundle.isLastPage = true;
354
835
  await flushSnapshotBundle(activeBundle);
355
836
  activeBundle = null;
837
+ if (nextState.tableIndex + 1 < nextState.tables.length) {
838
+ nextState = {
839
+ ...nextState,
840
+ tableIndex: nextState.tableIndex + 1,
841
+ rowCursor: null,
842
+ };
843
+ continue;
844
+ }
845
+ nextState = null;
846
+ break;
356
847
  }
357
- nextState = null;
358
- break;
359
- }
360
- const tableHandler = getServerHandlerOrThrow(args.handlers, nextTableName);
361
- if (!activeBundle || activeBundle.table !== nextTableName) {
362
848
  if (activeBundle) {
363
849
  await flushSnapshotBundle(activeBundle);
364
850
  }
365
- const bundleHeader = encodeSnapshotRows([]);
366
- activeBundle = {
367
- table: nextTableName,
368
- startCursor: nextState.rowCursor,
369
- isFirstPage: nextState.rowCursor == null,
370
- isLastPage: false,
371
- pageCount: 0,
372
- ttlMs: tableHandler.snapshotChunkTtlMs ?? 24 * 60 * 60 * 1000,
373
- rowFrameParts: [bundleHeader],
374
- };
375
- }
376
- const page = await tableHandler.snapshot({
377
- db: trx,
378
- actorId: args.auth.actorId,
379
- auth: args.auth,
380
- scopeValues: effectiveScopes,
381
- cursor: nextState.rowCursor,
382
- limit: limitSnapshotRows,
383
- }, sub.params);
384
- const rowFrames = encodeSnapshotRowFrames(page.rows ?? []);
385
- activeBundle.rowFrameParts.push(rowFrames);
386
- activeBundle.pageCount += 1;
387
- if (page.nextCursor != null) {
388
- nextState = { ...nextState, rowCursor: page.nextCursor };
851
+ subResponses.push({
852
+ id: sub.id,
853
+ status: 'active',
854
+ scopes: effectiveScopes,
855
+ bootstrap: true,
856
+ bootstrapState: nextState,
857
+ nextCursor: effectiveState.asOfCommitSeq,
858
+ commits: [],
859
+ snapshots,
860
+ });
861
+ nextCursors.push(effectiveState.asOfCommitSeq);
389
862
  continue;
390
863
  }
391
- activeBundle.isLastPage = true;
392
- await flushSnapshotBundle(activeBundle);
393
- activeBundle = null;
394
- if (nextState.tableIndex + 1 < nextState.tables.length) {
395
- nextState = {
396
- ...nextState,
397
- tableIndex: nextState.tableIndex + 1,
398
- rowCursor: null,
399
- };
400
- continue;
864
+ // Incremental pull for this subscription. The dialect row query
865
+ // carries the scanned commit-window max when matching rows exist,
866
+ // so we only need a separate commit-window scan when the row query
867
+ // returns no matches at all.
868
+ const incrementalRows = [];
869
+ let maxScannedCommitSeq = cursor;
870
+ for await (const row of dialect.iterateIncrementalPullRows(trx, {
871
+ partitionId,
872
+ table: sub.table,
873
+ scopes: effectiveScopes,
874
+ cursor,
875
+ limitCommits,
876
+ })) {
877
+ incrementalRows.push(row);
878
+ maxScannedCommitSeq = Math.max(maxScannedCommitSeq, row.scanned_max_commit_seq ?? row.commit_seq);
401
879
  }
402
- nextState = null;
403
- break;
404
- }
405
- if (activeBundle) {
406
- await flushSnapshotBundle(activeBundle);
407
- }
408
- subResponses.push({
409
- id: sub.id,
410
- status: 'active',
411
- scopes: effectiveScopes,
412
- bootstrap: true,
413
- bootstrapState: nextState,
414
- nextCursor: effectiveState.asOfCommitSeq,
415
- commits: [],
416
- snapshots,
417
- });
418
- nextCursors.push(effectiveState.asOfCommitSeq);
419
- continue;
420
- }
421
- // Incremental pull for this subscription
422
- // Read the commit window for this table up-front so the subscription cursor
423
- // can advance past commits that don't match the requested scopes.
424
- const scannedCommitSeqs = await dialect.readCommitSeqsForPull(trx, {
425
- partitionId,
426
- cursor,
427
- limitCommits,
428
- tables: [sub.table],
429
- });
430
- const maxScannedCommitSeq = scannedCommitSeqs.length > 0
431
- ? scannedCommitSeqs[scannedCommitSeqs.length - 1]
432
- : cursor;
433
- if (scannedCommitSeqs.length === 0) {
434
- subResponses.push({
435
- id: sub.id,
436
- status: 'active',
437
- scopes: effectiveScopes,
438
- bootstrap: false,
439
- nextCursor: cursor,
440
- commits: [],
441
- });
442
- nextCursors.push(cursor);
443
- continue;
444
- }
445
- let nextCursor = cursor;
446
- if (dedupeRows) {
447
- const latestByRowKey = new Map();
448
- for await (const r of dialect.iterateIncrementalPullRows(trx, {
449
- partitionId,
450
- table: sub.table,
451
- scopes: effectiveScopes,
452
- cursor,
453
- limitCommits,
454
- })) {
455
- nextCursor = Math.max(nextCursor, r.commit_seq);
456
- const rowKey = `${r.table}\u0000${r.row_id}`;
457
- const change = {
458
- table: r.table,
459
- row_id: r.row_id,
460
- op: r.op,
461
- row_json: r.row_json,
462
- row_version: r.row_version,
463
- scopes: r.scopes,
464
- };
465
- // Move row keys to insertion tail so Map iteration yields
466
- // "latest change wins" order without a full array sort.
467
- if (latestByRowKey.has(rowKey)) {
468
- latestByRowKey.delete(rowKey);
880
+ if (incrementalRows.length === 0) {
881
+ const scannedCommitSeqs = await dialect.readCommitSeqsForPull(trx, {
882
+ partitionId,
883
+ cursor,
884
+ limitCommits,
885
+ tables: [sub.table],
886
+ });
887
+ maxScannedCommitSeq =
888
+ scannedCommitSeqs.length > 0
889
+ ? scannedCommitSeqs[scannedCommitSeqs.length - 1]
890
+ : cursor;
891
+ if (scannedCommitSeqs.length === 0) {
892
+ subResponses.push({
893
+ id: sub.id,
894
+ status: 'active',
895
+ scopes: effectiveScopes,
896
+ bootstrap: false,
897
+ nextCursor: cursor,
898
+ commits: [],
899
+ });
900
+ nextCursors.push(cursor);
901
+ continue;
902
+ }
903
+ }
904
+ const tableHandler = args.handlers.byTable.get(sub.table);
905
+ if (!tableHandler) {
906
+ throw new Error(`Unknown table: ${sub.table}`);
469
907
  }
470
- latestByRowKey.set(rowKey, {
908
+ const incrementalItems = incrementalRows.map((r) => ({
471
909
  commitSeq: r.commit_seq,
472
910
  createdAt: r.created_at,
473
911
  actorId: r.actor_id,
474
- change,
912
+ change: projectPullChangeForClientSchema({
913
+ handler: tableHandler,
914
+ schemaVersion: clientSchemaVersion,
915
+ change: {
916
+ table: r.table,
917
+ row_id: r.row_id,
918
+ op: r.op,
919
+ row_json: r.row_json,
920
+ row_version: r.row_version,
921
+ scopes: r.scopes,
922
+ },
923
+ }),
924
+ }));
925
+ if (pullPlugins.length > 0 && incrementalItems.length > 0) {
926
+ const transformedChanges = await transformPullChanges({
927
+ plugins: pullPlugins,
928
+ ctx: {
929
+ db: trx,
930
+ actorId: args.auth.actorId,
931
+ auth: args.auth,
932
+ },
933
+ tableHandler,
934
+ subscription: {
935
+ id: sub.id,
936
+ table: sub.table,
937
+ scopes: effectiveScopes,
938
+ params: sub.params,
939
+ cursor,
940
+ crdtStateVectors: sub.crdtStateVectors,
941
+ },
942
+ changes: incrementalItems.map((item) => item.change),
943
+ });
944
+ for (let i = 0; i < incrementalItems.length; i += 1) {
945
+ incrementalItems[i].change = transformedChanges[i];
946
+ }
947
+ }
948
+ let nextCursor = cursor;
949
+ if (dedupeRows) {
950
+ const latestByRowKey = new Map();
951
+ for (const item of incrementalItems) {
952
+ nextCursor = Math.max(nextCursor, item.commitSeq);
953
+ const rowKey = `${item.change.table}\u0000${item.change.row_id}`;
954
+ // Move row keys to insertion tail so Map iteration yields
955
+ // "latest change wins" order without a full array sort.
956
+ if (latestByRowKey.has(rowKey)) {
957
+ latestByRowKey.delete(rowKey);
958
+ }
959
+ latestByRowKey.set(rowKey, {
960
+ commitSeq: item.commitSeq,
961
+ createdAt: item.createdAt,
962
+ actorId: item.actorId,
963
+ change: item.change,
964
+ });
965
+ }
966
+ nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
967
+ if (latestByRowKey.size === 0) {
968
+ subResponses.push({
969
+ id: sub.id,
970
+ status: 'active',
971
+ scopes: effectiveScopes,
972
+ bootstrap: false,
973
+ nextCursor,
974
+ commits: [],
975
+ });
976
+ nextCursors.push(nextCursor);
977
+ continue;
978
+ }
979
+ const commits = [];
980
+ for (const item of latestByRowKey.values()) {
981
+ const lastCommit = commits[commits.length - 1];
982
+ if (!lastCommit ||
983
+ lastCommit.commitSeq !== item.commitSeq) {
984
+ commits.push({
985
+ commitSeq: item.commitSeq,
986
+ createdAt: item.createdAt,
987
+ actorId: item.actorId,
988
+ changes: [item.change],
989
+ });
990
+ continue;
991
+ }
992
+ lastCommit.changes.push(item.change);
993
+ }
994
+ const integrity = await createWireSubscriptionIntegrity({
995
+ partitionId,
996
+ subscriptionId: sub.id,
997
+ previousRoot: typeof sub.verifiedRoot === 'string'
998
+ ? sub.verifiedRoot
999
+ : SYNCULAR_COMMIT_GENESIS_ROOT,
1000
+ commits,
1001
+ });
1002
+ subResponses.push({
1003
+ id: sub.id,
1004
+ status: 'active',
1005
+ scopes: effectiveScopes,
1006
+ bootstrap: false,
1007
+ nextCursor,
1008
+ ...(integrity ? { integrity } : {}),
1009
+ commits,
1010
+ });
1011
+ nextCursors.push(nextCursor);
1012
+ continue;
1013
+ }
1014
+ const commits = [];
1015
+ for (const item of incrementalItems) {
1016
+ nextCursor = Math.max(nextCursor, item.commitSeq);
1017
+ const seq = item.commitSeq;
1018
+ let commit = commits[commits.length - 1];
1019
+ if (!commit || commit.commitSeq !== seq) {
1020
+ commit = {
1021
+ commitSeq: seq,
1022
+ createdAt: item.createdAt,
1023
+ actorId: item.actorId,
1024
+ changes: [],
1025
+ };
1026
+ commits.push(commit);
1027
+ }
1028
+ commit.changes.push(item.change);
1029
+ }
1030
+ const integrity = await createWireSubscriptionIntegrity({
1031
+ partitionId,
1032
+ subscriptionId: sub.id,
1033
+ previousRoot: typeof sub.verifiedRoot === 'string'
1034
+ ? sub.verifiedRoot
1035
+ : SYNCULAR_COMMIT_GENESIS_ROOT,
1036
+ commits,
475
1037
  });
476
- }
477
- nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
478
- if (latestByRowKey.size === 0) {
1038
+ nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
1039
+ if (commits.length === 0) {
1040
+ subResponses.push({
1041
+ id: sub.id,
1042
+ status: 'active',
1043
+ scopes: effectiveScopes,
1044
+ bootstrap: false,
1045
+ nextCursor,
1046
+ commits: [],
1047
+ });
1048
+ nextCursors.push(nextCursor);
1049
+ continue;
1050
+ }
479
1051
  subResponses.push({
480
1052
  id: sub.id,
481
1053
  status: 'active',
482
1054
  scopes: effectiveScopes,
483
1055
  bootstrap: false,
484
1056
  nextCursor,
485
- commits: [],
1057
+ ...(integrity ? { integrity } : {}),
1058
+ commits,
486
1059
  });
487
1060
  nextCursors.push(nextCursor);
488
- continue;
489
1061
  }
490
- const commits = [];
491
- for (const item of latestByRowKey.values()) {
492
- const lastCommit = commits[commits.length - 1];
493
- if (!lastCommit || lastCommit.commitSeq !== item.commitSeq) {
494
- commits.push({
495
- commitSeq: item.commitSeq,
496
- createdAt: item.createdAt,
497
- actorId: item.actorId,
498
- changes: [item.change],
499
- });
500
- continue;
501
- }
502
- lastCommit.changes.push(item.change);
503
- }
504
- subResponses.push({
505
- id: sub.id,
506
- status: 'active',
507
- scopes: effectiveScopes,
508
- bootstrap: false,
509
- nextCursor,
510
- commits,
511
- });
512
- nextCursors.push(nextCursor);
513
- continue;
514
- }
515
- const commitsBySeq = new Map();
516
- const commitSeqs = [];
517
- for await (const r of dialect.iterateIncrementalPullRows(trx, {
518
- partitionId,
519
- table: sub.table,
520
- scopes: effectiveScopes,
521
- cursor,
522
- limitCommits,
523
- })) {
524
- nextCursor = Math.max(nextCursor, r.commit_seq);
525
- const seq = r.commit_seq;
526
- let commit = commitsBySeq.get(seq);
527
- if (!commit) {
528
- commit = {
529
- commitSeq: seq,
530
- createdAt: r.created_at,
531
- actorId: r.actor_id,
532
- changes: [],
533
- };
534
- commitsBySeq.set(seq, commit);
535
- commitSeqs.push(seq);
536
- }
537
- const change = {
538
- table: r.table,
539
- row_id: r.row_id,
540
- op: r.op,
541
- row_json: r.row_json,
542
- row_version: r.row_version,
543
- scopes: r.scopes,
1062
+ const effectiveScopes = mergeScopes(activeSubscriptions);
1063
+ const clientCursor = nextCursors.length > 0
1064
+ ? Math.min(...nextCursors)
1065
+ : maxCommitSeq;
1066
+ return {
1067
+ response: {
1068
+ ok: true,
1069
+ subscriptions: subResponses,
1070
+ },
1071
+ effectiveScopes,
1072
+ clientCursor,
544
1073
  };
545
- commit.changes.push(change);
546
- }
547
- nextCursor = Math.max(nextCursor, maxScannedCommitSeq);
548
- if (commitSeqs.length === 0) {
549
- subResponses.push({
550
- id: sub.id,
551
- status: 'active',
552
- scopes: effectiveScopes,
553
- bootstrap: false,
554
- nextCursor,
555
- commits: [],
556
- });
557
- nextCursors.push(nextCursor);
558
- continue;
559
- }
560
- const commits = commitSeqs
561
- .map((seq) => commitsBySeq.get(seq))
562
- .filter((c) => !!c)
563
- .filter((c) => c.changes.length > 0);
564
- subResponses.push({
565
- id: sub.id,
566
- status: 'active',
567
- scopes: effectiveScopes,
568
- bootstrap: false,
569
- nextCursor,
570
- commits,
571
- });
572
- nextCursors.push(nextCursor);
573
- }
574
- const effectiveScopes = mergeScopes(activeSubscriptions);
575
- const clientCursor = nextCursors.length > 0 ? Math.min(...nextCursors) : maxCommitSeq;
576
- return {
577
- response: {
578
- ok: true,
579
- subscriptions: subResponses,
580
- },
581
- effectiveScopes,
582
- clientCursor,
583
- };
584
- });
585
- const chunkStorage = args.chunkStorage;
586
- if (chunkStorage && pendingExternalChunkWrites.length > 0) {
587
- for (const pending of pendingExternalChunkWrites) {
588
- let chunkRef = await readSnapshotChunkRefByPageKey(db, {
589
- partitionId: pending.cacheLookup.partitionId,
590
- scopeKey: pending.cacheLookup.scopeKey,
591
- scope: pending.cacheLookup.scope,
592
- asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
593
- rowCursor: pending.cacheLookup.rowCursor,
594
- rowLimit: pending.cacheLookup.rowLimit,
595
- encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
596
- compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
597
1074
  });
598
- if (!chunkRef) {
599
- const sha256 = await sha256Hex(pending.rowFramePayload);
600
- if (chunkStorage.storeChunkStream) {
601
- const { stream: bodyStream, byteLength } = await gzipBytesToStream(pending.rowFramePayload);
602
- chunkRef = await chunkStorage.storeChunkStream({
1075
+ const chunkStorage = args.chunkStorage;
1076
+ if (chunkStorage && pendingExternalChunkWrites.length > 0) {
1077
+ await runWithConcurrency(pendingExternalChunkWrites, 4, async (pending) => {
1078
+ const cacheLookupStartedAt = Date.now();
1079
+ let chunkRef = await readSnapshotChunkRefByPageKey(db, {
603
1080
  partitionId: pending.cacheLookup.partitionId,
604
1081
  scopeKey: pending.cacheLookup.scopeKey,
605
1082
  scope: pending.cacheLookup.scope,
606
1083
  asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
607
1084
  rowCursor: pending.cacheLookup.rowCursor,
608
1085
  rowLimit: pending.cacheLookup.rowLimit,
609
- encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
1086
+ encoding: snapshotChunkEncoding,
610
1087
  compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
611
- sha256,
612
- byteLength,
613
- bodyStream,
614
- expiresAt: pending.expiresAt,
615
1088
  });
616
- }
617
- else {
618
- const compressedBody = await gzipBytes(pending.rowFramePayload);
619
- chunkRef = await chunkStorage.storeChunk({
620
- partitionId: pending.cacheLookup.partitionId,
621
- scopeKey: pending.cacheLookup.scopeKey,
622
- scope: pending.cacheLookup.scope,
623
- asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
624
- rowCursor: pending.cacheLookup.rowCursor,
625
- rowLimit: pending.cacheLookup.rowLimit,
626
- encoding: SYNC_SNAPSHOT_CHUNK_ENCODING,
627
- compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
628
- sha256,
629
- body: compressedBody,
630
- expiresAt: pending.expiresAt,
631
- });
632
- }
1089
+ bootstrapTimings.chunkCacheLookupMs += Math.max(0, Date.now() - cacheLookupStartedAt);
1090
+ if (!chunkRef) {
1091
+ if (chunkStorage.storeChunkStream) {
1092
+ const { stream: bodyStream, byteLength, sha256, gzipMs, hashMs, } = await encodeCompressedSnapshotChunkToStream(pending.payloadParts, snapshotChunkGzipLevel);
1093
+ bootstrapTimings.chunkGzipMs += gzipMs;
1094
+ bootstrapTimings.chunkHashMs += hashMs;
1095
+ const chunkPersistStartedAt = Date.now();
1096
+ chunkRef = await chunkStorage.storeChunkStream({
1097
+ partitionId: pending.cacheLookup.partitionId,
1098
+ scopeKey: pending.cacheLookup.scopeKey,
1099
+ scope: pending.cacheLookup.scope,
1100
+ asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
1101
+ rowCursor: pending.cacheLookup.rowCursor,
1102
+ rowLimit: pending.cacheLookup.rowLimit,
1103
+ nextRowCursor: pending.cacheLookup.nextRowCursor,
1104
+ isLastPage: pending.cacheLookup.isLastPage,
1105
+ encoding: snapshotChunkEncoding,
1106
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
1107
+ sha256,
1108
+ byteLength,
1109
+ bodyStream,
1110
+ expiresAt: pending.expiresAt,
1111
+ });
1112
+ bootstrapTimings.chunkPersistMs += Math.max(0, Date.now() - chunkPersistStartedAt);
1113
+ }
1114
+ else {
1115
+ const encodedChunk = await encodeCompressedSnapshotChunk(pending.payloadParts, snapshotChunkGzipLevel);
1116
+ bootstrapTimings.chunkGzipMs += encodedChunk.gzipMs;
1117
+ bootstrapTimings.chunkHashMs += encodedChunk.hashMs;
1118
+ const chunkPersistStartedAt = Date.now();
1119
+ chunkRef = await chunkStorage.storeChunk({
1120
+ partitionId: pending.cacheLookup.partitionId,
1121
+ scopeKey: pending.cacheLookup.scopeKey,
1122
+ scope: pending.cacheLookup.scope,
1123
+ asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
1124
+ rowCursor: pending.cacheLookup.rowCursor,
1125
+ rowLimit: pending.cacheLookup.rowLimit,
1126
+ nextRowCursor: pending.cacheLookup.nextRowCursor,
1127
+ isLastPage: pending.cacheLookup.isLastPage,
1128
+ encoding: snapshotChunkEncoding,
1129
+ compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
1130
+ sha256: encodedChunk.sha256,
1131
+ body: encodedChunk.body,
1132
+ expiresAt: pending.expiresAt,
1133
+ });
1134
+ bootstrapTimings.chunkPersistMs += Math.max(0, Date.now() - chunkPersistStartedAt);
1135
+ }
1136
+ }
1137
+ const chunk = toResponseChunkRef(chunkRef);
1138
+ pending.snapshot.chunks = [chunk];
1139
+ pending.snapshot.manifest =
1140
+ await createChunkedSnapshotManifest({
1141
+ table: pending.snapshot.table,
1142
+ asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
1143
+ scopeKey: pending.cacheLookup.scopeKey,
1144
+ rowCursor: pending.cacheLookup.rowCursor,
1145
+ rowLimit: pending.cacheLookup.rowLimit,
1146
+ nextRowCursor: pending.cacheLookup.nextRowCursor,
1147
+ isFirstPage: pending.snapshot.isFirstPage,
1148
+ isLastPage: pending.cacheLookup.isLastPage,
1149
+ chunks: [chunk],
1150
+ });
1151
+ });
633
1152
  }
634
- pending.snapshot.chunks = [chunkRef];
1153
+ const durationMs = Math.max(0, Date.now() - startedAtMs);
1154
+ const stats = summarizePullResponse(result.response);
1155
+ span.setAttribute('status', 'ok');
1156
+ span.setAttribute('duration_ms', durationMs);
1157
+ span.setAttribute('subscription_count', stats.subscriptionCount);
1158
+ span.setAttribute('commit_count', stats.commitCount);
1159
+ span.setAttribute('change_count', stats.changeCount);
1160
+ span.setAttribute('snapshot_page_count', stats.snapshotPageCount);
1161
+ span.setAttributes({
1162
+ bootstrap_snapshot_query_ms: bootstrapTimings.snapshotQueryMs,
1163
+ bootstrap_snapshot_binary_encode_ms: bootstrapTimings.binaryEncodeMs,
1164
+ bootstrap_chunk_cache_lookup_ms: bootstrapTimings.chunkCacheLookupMs,
1165
+ bootstrap_artifact_cache_lookup_ms: bootstrapTimings.artifactCacheLookupMs,
1166
+ bootstrap_chunk_gzip_ms: bootstrapTimings.chunkGzipMs,
1167
+ bootstrap_chunk_hash_ms: bootstrapTimings.chunkHashMs,
1168
+ bootstrap_chunk_persist_ms: bootstrapTimings.chunkPersistMs,
1169
+ });
1170
+ span.setStatus('ok');
1171
+ recordPullMetrics({
1172
+ status: 'ok',
1173
+ dedupeRows,
1174
+ durationMs,
1175
+ stats,
1176
+ });
1177
+ return {
1178
+ ...result,
1179
+ bootstrapTimings,
1180
+ };
1181
+ }
1182
+ catch (error) {
1183
+ if (error instanceof Error &&
1184
+ attemptIndex < MAX_PULL_TRANSACTION_RETRIES - 1 &&
1185
+ isSerializablePullError(error)) {
1186
+ await delay(PULL_TRANSACTION_RETRY_DELAY_MS * (attemptIndex + 1));
1187
+ continue;
1188
+ }
1189
+ throw error;
635
1190
  }
636
1191
  }
637
- const durationMs = Math.max(0, Date.now() - startedAtMs);
638
- const stats = summarizePullResponse(result.response);
639
- span.setAttribute('status', 'ok');
640
- span.setAttribute('duration_ms', durationMs);
641
- span.setAttribute('subscription_count', stats.subscriptionCount);
642
- span.setAttribute('commit_count', stats.commitCount);
643
- span.setAttribute('change_count', stats.changeCount);
644
- span.setAttribute('snapshot_page_count', stats.snapshotPageCount);
645
- span.setStatus('ok');
646
- recordPullMetrics({
647
- status: 'ok',
648
- dedupeRows,
649
- durationMs,
650
- stats,
651
- });
652
- return result;
1192
+ throw new Error('Pull transaction retry loop exhausted unexpectedly');
653
1193
  }
654
1194
  catch (error) {
655
1195
  const durationMs = Math.max(0, Date.now() - startedAtMs);