@syncular/server 0.1.3 → 0.2.1

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 (758) hide show
  1. package/README.md +773 -13
  2. package/dist/admin.d.ts +136 -0
  3. package/dist/admin.js +168 -0
  4. package/dist/blob-handlers.d.ts +69 -0
  5. package/dist/blob-handlers.js +245 -0
  6. package/dist/blob-store.d.ts +111 -0
  7. package/dist/blob-store.js +0 -0
  8. package/dist/content-encoding.d.ts +27 -0
  9. package/dist/content-encoding.js +69 -0
  10. package/dist/context.d.ts +151 -0
  11. package/dist/context.js +21 -0
  12. package/dist/crdt-merger.d.ts +28 -0
  13. package/dist/crdt-merger.js +11 -0
  14. package/dist/d1-storage.d.ts +50 -0
  15. package/dist/d1-storage.js +0 -0
  16. package/dist/errors.d.ts +35 -9
  17. package/dist/errors.js +220 -11
  18. package/dist/events-ring.d.ts +55 -0
  19. package/dist/events-ring.js +96 -0
  20. package/dist/events.d.ts +231 -0
  21. package/dist/events.js +22 -0
  22. package/dist/frame-bytes.d.ts +20 -0
  23. package/dist/frame-bytes.js +75 -0
  24. package/dist/handler.d.ts +9 -0
  25. package/dist/handler.js +490 -0
  26. package/dist/index.d.ts +42 -33
  27. package/dist/index.js +45 -27
  28. package/dist/lease-store.d.ts +50 -0
  29. package/dist/lease-store.js +0 -0
  30. package/dist/pg-executor-pglite.d.ts +27 -0
  31. package/dist/pg-executor-pglite.js +33 -0
  32. package/dist/pg-executor.d.ts +67 -0
  33. package/dist/pg-executor.js +56 -0
  34. package/dist/postgres-fanout.d.ts +83 -0
  35. package/dist/postgres-fanout.js +71 -0
  36. package/dist/postgres-storage.d.ts +69 -0
  37. package/dist/postgres-storage.js +766 -0
  38. package/dist/prune.d.ts +24 -39
  39. package/dist/prune.js +44 -146
  40. package/dist/pull.d.ts +42 -67
  41. package/dist/pull.js +387 -1187
  42. package/dist/push.d.ts +31 -66
  43. package/dist/push.js +406 -655
  44. package/dist/realtime.d.ts +197 -0
  45. package/dist/realtime.js +848 -0
  46. package/dist/relational-rows.d.ts +229 -0
  47. package/dist/relational-rows.js +514 -0
  48. package/dist/s3-blob-store.d.ts +142 -0
  49. package/dist/s3-blob-store.js +458 -0
  50. package/dist/s3-segment-store.d.ts +95 -0
  51. package/dist/s3-segment-store.js +380 -0
  52. package/dist/schema.d.ts +82 -286
  53. package/dist/schema.js +133 -9
  54. package/dist/scopes.d.ts +48 -0
  55. package/dist/scopes.js +98 -0
  56. package/dist/segment-download.d.ts +14 -0
  57. package/dist/segment-download.js +125 -0
  58. package/dist/segment-store.d.ts +81 -0
  59. package/dist/segment-store.js +63 -0
  60. package/dist/signed-url.d.ts +139 -0
  61. package/dist/signed-url.js +143 -0
  62. package/dist/sigv4.d.ts +97 -0
  63. package/dist/sigv4.js +161 -0
  64. package/dist/sqlite-blob-store.d.ts +22 -0
  65. package/dist/sqlite-blob-store.js +86 -0
  66. package/dist/sqlite-dialect.d.ts +101 -0
  67. package/dist/sqlite-dialect.js +250 -0
  68. package/dist/sqlite-image.d.ts +27 -0
  69. package/dist/sqlite-image.js +83 -0
  70. package/dist/sqlite-lease-store.d.ts +21 -0
  71. package/dist/sqlite-lease-store.js +94 -0
  72. package/dist/sqlite-segment-store.d.ts +25 -0
  73. package/dist/sqlite-segment-store.js +115 -0
  74. package/dist/sqlite-storage.d.ts +47 -0
  75. package/dist/sqlite-storage.js +477 -0
  76. package/dist/storage.d.ts +252 -0
  77. package/dist/storage.js +1 -0
  78. package/dist/validate.d.ts +83 -0
  79. package/dist/validate.js +44 -0
  80. package/package.json +17 -325
  81. package/src/admin.ts +312 -0
  82. package/src/blob-handlers.ts +340 -0
  83. package/src/blob-store.ts +0 -0
  84. package/src/content-encoding.ts +94 -0
  85. package/src/context.ts +176 -0
  86. package/src/crdt-merger.ts +33 -0
  87. package/src/d1-storage.ts +0 -0
  88. package/src/errors.ts +241 -19
  89. package/src/events-ring.ts +116 -0
  90. package/src/events.ts +291 -0
  91. package/src/frame-bytes.ts +94 -0
  92. package/src/handler.ts +678 -0
  93. package/src/index.ts +44 -27
  94. package/src/lease-store.ts +0 -0
  95. package/src/pg-executor-pglite.ts +63 -0
  96. package/src/pg-executor.ts +87 -0
  97. package/src/postgres-fanout.ts +143 -0
  98. package/src/postgres-storage.ts +1182 -0
  99. package/src/prune.ts +63 -289
  100. package/src/pull.ts +505 -1805
  101. package/src/push.ts +623 -1006
  102. package/src/realtime.ts +1082 -0
  103. package/src/relational-rows.ts +633 -0
  104. package/src/s3-blob-store.ts +616 -0
  105. package/src/s3-segment-store.ts +519 -0
  106. package/src/schema.ts +242 -281
  107. package/src/scopes.ts +127 -0
  108. package/src/segment-download.ts +169 -0
  109. package/src/segment-store.ts +154 -0
  110. package/src/signed-url.ts +308 -0
  111. package/src/sigv4.ts +267 -0
  112. package/src/sqlite-blob-store.ts +125 -0
  113. package/src/sqlite-dialect.ts +337 -0
  114. package/src/sqlite-image.ts +124 -0
  115. package/src/sqlite-lease-store.ts +144 -0
  116. package/src/sqlite-segment-store.ts +204 -0
  117. package/src/sqlite-storage.ts +792 -0
  118. package/src/storage.ts +316 -0
  119. package/src/validate.ts +123 -0
  120. package/dist/auth-leases.d.ts +0 -85
  121. package/dist/auth-leases.d.ts.map +0 -1
  122. package/dist/auth-leases.js +0 -368
  123. package/dist/auth-leases.js.map +0 -1
  124. package/dist/better-sqlite3.d.ts +0 -22
  125. package/dist/better-sqlite3.d.ts.map +0 -1
  126. package/dist/better-sqlite3.js +0 -16
  127. package/dist/better-sqlite3.js.map +0 -1
  128. package/dist/blobs/access.d.ts +0 -35
  129. package/dist/blobs/access.d.ts.map +0 -1
  130. package/dist/blobs/access.js +0 -149
  131. package/dist/blobs/access.js.map +0 -1
  132. package/dist/blobs/adapters/database.d.ts +0 -100
  133. package/dist/blobs/adapters/database.d.ts.map +0 -1
  134. package/dist/blobs/adapters/database.js +0 -247
  135. package/dist/blobs/adapters/database.js.map +0 -1
  136. package/dist/blobs/index.d.ts +0 -9
  137. package/dist/blobs/index.d.ts.map +0 -1
  138. package/dist/blobs/index.js +0 -9
  139. package/dist/blobs/index.js.map +0 -1
  140. package/dist/blobs/manager.d.ts +0 -220
  141. package/dist/blobs/manager.d.ts.map +0 -1
  142. package/dist/blobs/manager.js +0 -625
  143. package/dist/blobs/manager.js.map +0 -1
  144. package/dist/blobs/migrate.d.ts +0 -27
  145. package/dist/blobs/migrate.d.ts.map +0 -1
  146. package/dist/blobs/migrate.js +0 -127
  147. package/dist/blobs/migrate.js.map +0 -1
  148. package/dist/blobs/types.d.ts +0 -58
  149. package/dist/blobs/types.d.ts.map +0 -1
  150. package/dist/blobs/types.js +0 -5
  151. package/dist/blobs/types.js.map +0 -1
  152. package/dist/bun-sqlite.d.ts +0 -19
  153. package/dist/bun-sqlite.d.ts.map +0 -1
  154. package/dist/bun-sqlite.js +0 -19
  155. package/dist/bun-sqlite.js.map +0 -1
  156. package/dist/clients.d.ts +0 -15
  157. package/dist/clients.d.ts.map +0 -1
  158. package/dist/clients.js +0 -7
  159. package/dist/clients.js.map +0 -1
  160. package/dist/cloudflare/durable-object.d.ts +0 -93
  161. package/dist/cloudflare/durable-object.d.ts.map +0 -1
  162. package/dist/cloudflare/durable-object.js +0 -210
  163. package/dist/cloudflare/durable-object.js.map +0 -1
  164. package/dist/cloudflare/index.d.ts +0 -22
  165. package/dist/cloudflare/index.d.ts.map +0 -1
  166. package/dist/cloudflare/index.js +0 -22
  167. package/dist/cloudflare/index.js.map +0 -1
  168. package/dist/cloudflare/r2.d.ts +0 -180
  169. package/dist/cloudflare/r2.d.ts.map +0 -1
  170. package/dist/cloudflare/r2.js +0 -258
  171. package/dist/cloudflare/r2.js.map +0 -1
  172. package/dist/cloudflare/scope-cache.d.ts +0 -54
  173. package/dist/cloudflare/scope-cache.d.ts.map +0 -1
  174. package/dist/cloudflare/scope-cache.js +0 -223
  175. package/dist/cloudflare/scope-cache.js.map +0 -1
  176. package/dist/cloudflare/sentry.d.ts +0 -47
  177. package/dist/cloudflare/sentry.d.ts.map +0 -1
  178. package/dist/cloudflare/sentry.js +0 -163
  179. package/dist/cloudflare/sentry.js.map +0 -1
  180. package/dist/cloudflare/worker.d.ts +0 -46
  181. package/dist/cloudflare/worker.d.ts.map +0 -1
  182. package/dist/cloudflare/worker.js +0 -63
  183. package/dist/cloudflare/worker.js.map +0 -1
  184. package/dist/commit-integrity.d.ts +0 -38
  185. package/dist/commit-integrity.d.ts.map +0 -1
  186. package/dist/commit-integrity.js +0 -260
  187. package/dist/commit-integrity.js.map +0 -1
  188. package/dist/compaction.d.ts +0 -27
  189. package/dist/compaction.d.ts.map +0 -1
  190. package/dist/compaction.js +0 -49
  191. package/dist/compaction.js.map +0 -1
  192. package/dist/crdt-yjs/index.d.ts +0 -99
  193. package/dist/crdt-yjs/index.d.ts.map +0 -1
  194. package/dist/crdt-yjs/index.js +0 -629
  195. package/dist/crdt-yjs/index.js.map +0 -1
  196. package/dist/d1.d.ts +0 -13
  197. package/dist/d1.d.ts.map +0 -1
  198. package/dist/d1.js +0 -14
  199. package/dist/d1.js.map +0 -1
  200. package/dist/dialect/base.d.ts +0 -93
  201. package/dist/dialect/base.d.ts.map +0 -1
  202. package/dist/dialect/base.js +0 -181
  203. package/dist/dialect/base.js.map +0 -1
  204. package/dist/dialect/helpers.d.ts +0 -16
  205. package/dist/dialect/helpers.d.ts.map +0 -1
  206. package/dist/dialect/helpers.js +0 -83
  207. package/dist/dialect/helpers.js.map +0 -1
  208. package/dist/dialect/index.d.ts +0 -7
  209. package/dist/dialect/index.d.ts.map +0 -1
  210. package/dist/dialect/index.js +0 -7
  211. package/dist/dialect/index.js.map +0 -1
  212. package/dist/dialect/types.d.ts +0 -187
  213. package/dist/dialect/types.d.ts.map +0 -1
  214. package/dist/dialect/types.js +0 -8
  215. package/dist/dialect/types.js.map +0 -1
  216. package/dist/encrypted-crdt.d.ts +0 -67
  217. package/dist/encrypted-crdt.d.ts.map +0 -1
  218. package/dist/encrypted-crdt.js +0 -426
  219. package/dist/encrypted-crdt.js.map +0 -1
  220. package/dist/errors.d.ts.map +0 -1
  221. package/dist/errors.js.map +0 -1
  222. package/dist/filesystem/index.d.ts +0 -44
  223. package/dist/filesystem/index.d.ts.map +0 -1
  224. package/dist/filesystem/index.js +0 -164
  225. package/dist/filesystem/index.js.map +0 -1
  226. package/dist/handlers/collection.d.ts +0 -20
  227. package/dist/handlers/collection.d.ts.map +0 -1
  228. package/dist/handlers/collection.js +0 -42
  229. package/dist/handlers/collection.js.map +0 -1
  230. package/dist/handlers/create-handler.d.ts +0 -156
  231. package/dist/handlers/create-handler.d.ts.map +0 -1
  232. package/dist/handlers/create-handler.js +0 -626
  233. package/dist/handlers/create-handler.js.map +0 -1
  234. package/dist/handlers/index.d.ts +0 -4
  235. package/dist/handlers/index.d.ts.map +0 -1
  236. package/dist/handlers/index.js +0 -4
  237. package/dist/handlers/index.js.map +0 -1
  238. package/dist/handlers/types.d.ts +0 -295
  239. package/dist/handlers/types.d.ts.map +0 -1
  240. package/dist/handlers/types.js +0 -2
  241. package/dist/handlers/types.js.map +0 -1
  242. package/dist/helpers/conflict.d.ts +0 -52
  243. package/dist/helpers/conflict.d.ts.map +0 -1
  244. package/dist/helpers/conflict.js +0 -49
  245. package/dist/helpers/conflict.js.map +0 -1
  246. package/dist/helpers/emitted-change.d.ts +0 -56
  247. package/dist/helpers/emitted-change.d.ts.map +0 -1
  248. package/dist/helpers/emitted-change.js +0 -46
  249. package/dist/helpers/emitted-change.js.map +0 -1
  250. package/dist/helpers/index.d.ts +0 -12
  251. package/dist/helpers/index.d.ts.map +0 -1
  252. package/dist/helpers/index.js +0 -12
  253. package/dist/helpers/index.js.map +0 -1
  254. package/dist/helpers/paginate.d.ts +0 -49
  255. package/dist/helpers/paginate.d.ts.map +0 -1
  256. package/dist/helpers/paginate.js +0 -54
  257. package/dist/helpers/paginate.js.map +0 -1
  258. package/dist/helpers/scope-authorization.d.ts +0 -7
  259. package/dist/helpers/scope-authorization.d.ts.map +0 -1
  260. package/dist/helpers/scope-authorization.js +0 -19
  261. package/dist/helpers/scope-authorization.js.map +0 -1
  262. package/dist/helpers/scope-commit-index.d.ts +0 -12
  263. package/dist/helpers/scope-commit-index.d.ts.map +0 -1
  264. package/dist/helpers/scope-commit-index.js +0 -35
  265. package/dist/helpers/scope-commit-index.js.map +0 -1
  266. package/dist/helpers/scope-strings.d.ts +0 -74
  267. package/dist/helpers/scope-strings.d.ts.map +0 -1
  268. package/dist/helpers/scope-strings.js +0 -82
  269. package/dist/helpers/scope-strings.js.map +0 -1
  270. package/dist/hono/api-key-auth.d.ts +0 -49
  271. package/dist/hono/api-key-auth.d.ts.map +0 -1
  272. package/dist/hono/api-key-auth.js +0 -108
  273. package/dist/hono/api-key-auth.js.map +0 -1
  274. package/dist/hono/audit-redaction.d.ts +0 -20
  275. package/dist/hono/audit-redaction.d.ts.map +0 -1
  276. package/dist/hono/audit-redaction.js +0 -85
  277. package/dist/hono/audit-redaction.js.map +0 -1
  278. package/dist/hono/blobs.d.ts +0 -75
  279. package/dist/hono/blobs.d.ts.map +0 -1
  280. package/dist/hono/blobs.js +0 -586
  281. package/dist/hono/blobs.js.map +0 -1
  282. package/dist/hono/console/gateway.d.ts +0 -42
  283. package/dist/hono/console/gateway.d.ts.map +0 -1
  284. package/dist/hono/console/gateway.js +0 -2158
  285. package/dist/hono/console/gateway.js.map +0 -1
  286. package/dist/hono/console/live-auth.d.ts +0 -7
  287. package/dist/hono/console/live-auth.d.ts.map +0 -1
  288. package/dist/hono/console/live-auth.js +0 -39
  289. package/dist/hono/console/live-auth.js.map +0 -1
  290. package/dist/hono/console/route-descriptor.d.ts +0 -6
  291. package/dist/hono/console/route-descriptor.d.ts.map +0 -1
  292. package/dist/hono/console/route-descriptor.js +0 -16
  293. package/dist/hono/console/route-descriptor.js.map +0 -1
  294. package/dist/hono/console/routes/api-keys.d.ts +0 -8
  295. package/dist/hono/console/routes/api-keys.d.ts.map +0 -1
  296. package/dist/hono/console/routes/api-keys.js +0 -575
  297. package/dist/hono/console/routes/api-keys.js.map +0 -1
  298. package/dist/hono/console/routes/clients.d.ts +0 -8
  299. package/dist/hono/console/routes/clients.d.ts.map +0 -1
  300. package/dist/hono/console/routes/clients.js +0 -322
  301. package/dist/hono/console/routes/clients.js.map +0 -1
  302. package/dist/hono/console/routes/commits.d.ts +0 -8
  303. package/dist/hono/console/routes/commits.d.ts.map +0 -1
  304. package/dist/hono/console/routes/commits.js +0 -882
  305. package/dist/hono/console/routes/commits.js.map +0 -1
  306. package/dist/hono/console/routes/context.d.ts +0 -188
  307. package/dist/hono/console/routes/context.d.ts.map +0 -1
  308. package/dist/hono/console/routes/context.js +0 -633
  309. package/dist/hono/console/routes/context.js.map +0 -1
  310. package/dist/hono/console/routes/events.d.ts +0 -8
  311. package/dist/hono/console/routes/events.d.ts.map +0 -1
  312. package/dist/hono/console/routes/events.js +0 -501
  313. package/dist/hono/console/routes/events.js.map +0 -1
  314. package/dist/hono/console/routes/maintenance.d.ts +0 -8
  315. package/dist/hono/console/routes/maintenance.d.ts.map +0 -1
  316. package/dist/hono/console/routes/maintenance.js +0 -341
  317. package/dist/hono/console/routes/maintenance.js.map +0 -1
  318. package/dist/hono/console/routes/shared.d.ts +0 -174
  319. package/dist/hono/console/routes/shared.d.ts.map +0 -1
  320. package/dist/hono/console/routes/shared.js +0 -583
  321. package/dist/hono/console/routes/shared.js.map +0 -1
  322. package/dist/hono/console/routes/stats.d.ts +0 -8
  323. package/dist/hono/console/routes/stats.d.ts.map +0 -1
  324. package/dist/hono/console/routes/stats.js +0 -295
  325. package/dist/hono/console/routes/stats.js.map +0 -1
  326. package/dist/hono/console/routes/storage.d.ts +0 -8
  327. package/dist/hono/console/routes/storage.d.ts.map +0 -1
  328. package/dist/hono/console/routes/storage.js +0 -121
  329. package/dist/hono/console/routes/storage.js.map +0 -1
  330. package/dist/hono/console/routes.d.ts +0 -36
  331. package/dist/hono/console/routes.d.ts.map +0 -1
  332. package/dist/hono/console/routes.js +0 -112
  333. package/dist/hono/console/routes.js.map +0 -1
  334. package/dist/hono/console/schema-errors.d.ts +0 -2
  335. package/dist/hono/console/schema-errors.d.ts.map +0 -1
  336. package/dist/hono/console/schema-errors.js +0 -17
  337. package/dist/hono/console/schema-errors.js.map +0 -1
  338. package/dist/hono/console/schemas.d.ts +0 -1515
  339. package/dist/hono/console/schemas.d.ts.map +0 -1
  340. package/dist/hono/console/schemas.js +0 -661
  341. package/dist/hono/console/schemas.js.map +0 -1
  342. package/dist/hono/console/types.d.ts +0 -213
  343. package/dist/hono/console/types.d.ts.map +0 -1
  344. package/dist/hono/console/types.js +0 -2
  345. package/dist/hono/console/types.js.map +0 -1
  346. package/dist/hono/console/ui.d.ts +0 -38
  347. package/dist/hono/console/ui.d.ts.map +0 -1
  348. package/dist/hono/console/ui.js +0 -43
  349. package/dist/hono/console/ui.js.map +0 -1
  350. package/dist/hono/create-server.d.ts +0 -71
  351. package/dist/hono/create-server.d.ts.map +0 -1
  352. package/dist/hono/create-server.js +0 -121
  353. package/dist/hono/create-server.js.map +0 -1
  354. package/dist/hono/errors.d.ts +0 -13
  355. package/dist/hono/errors.d.ts.map +0 -1
  356. package/dist/hono/errors.js +0 -21
  357. package/dist/hono/errors.js.map +0 -1
  358. package/dist/hono/index.d.ts +0 -20
  359. package/dist/hono/index.d.ts.map +0 -1
  360. package/dist/hono/index.js +0 -31
  361. package/dist/hono/index.js.map +0 -1
  362. package/dist/hono/openapi.d.ts +0 -72
  363. package/dist/hono/openapi.d.ts.map +0 -1
  364. package/dist/hono/openapi.js +0 -99
  365. package/dist/hono/openapi.js.map +0 -1
  366. package/dist/hono/proxy/connection-manager.d.ts +0 -78
  367. package/dist/hono/proxy/connection-manager.d.ts.map +0 -1
  368. package/dist/hono/proxy/connection-manager.js +0 -251
  369. package/dist/hono/proxy/connection-manager.js.map +0 -1
  370. package/dist/hono/proxy/index.d.ts +0 -8
  371. package/dist/hono/proxy/index.d.ts.map +0 -1
  372. package/dist/hono/proxy/index.js +0 -8
  373. package/dist/hono/proxy/index.js.map +0 -1
  374. package/dist/hono/proxy/routes.d.ts +0 -86
  375. package/dist/hono/proxy/routes.d.ts.map +0 -1
  376. package/dist/hono/proxy/routes.js +0 -183
  377. package/dist/hono/proxy/routes.js.map +0 -1
  378. package/dist/hono/rate-limit.d.ts +0 -101
  379. package/dist/hono/rate-limit.d.ts.map +0 -1
  380. package/dist/hono/rate-limit.js +0 -184
  381. package/dist/hono/rate-limit.js.map +0 -1
  382. package/dist/hono/realtime-sync-packs.d.ts +0 -43
  383. package/dist/hono/realtime-sync-packs.d.ts.map +0 -1
  384. package/dist/hono/realtime-sync-packs.js +0 -219
  385. package/dist/hono/realtime-sync-packs.js.map +0 -1
  386. package/dist/hono/routes/audit.d.ts +0 -12
  387. package/dist/hono/routes/audit.d.ts.map +0 -1
  388. package/dist/hono/routes/audit.js +0 -385
  389. package/dist/hono/routes/audit.js.map +0 -1
  390. package/dist/hono/routes/auth-leases.d.ts +0 -8
  391. package/dist/hono/routes/auth-leases.d.ts.map +0 -1
  392. package/dist/hono/routes/auth-leases.js +0 -88
  393. package/dist/hono/routes/auth-leases.js.map +0 -1
  394. package/dist/hono/routes/combined.d.ts +0 -8
  395. package/dist/hono/routes/combined.d.ts.map +0 -1
  396. package/dist/hono/routes/combined.js +0 -392
  397. package/dist/hono/routes/combined.js.map +0 -1
  398. package/dist/hono/routes/context.d.ts +0 -314
  399. package/dist/hono/routes/context.d.ts.map +0 -1
  400. package/dist/hono/routes/context.js +0 -1147
  401. package/dist/hono/routes/context.js.map +0 -1
  402. package/dist/hono/routes/health.d.ts +0 -8
  403. package/dist/hono/routes/health.d.ts.map +0 -1
  404. package/dist/hono/routes/health.js +0 -16
  405. package/dist/hono/routes/health.js.map +0 -1
  406. package/dist/hono/routes/realtime.d.ts +0 -9
  407. package/dist/hono/routes/realtime.d.ts.map +0 -1
  408. package/dist/hono/routes/realtime.js +0 -626
  409. package/dist/hono/routes/realtime.js.map +0 -1
  410. package/dist/hono/routes/shared.d.ts +0 -689
  411. package/dist/hono/routes/shared.d.ts.map +0 -1
  412. package/dist/hono/routes/shared.js +0 -972
  413. package/dist/hono/routes/shared.js.map +0 -1
  414. package/dist/hono/routes/snapshots.d.ts +0 -10
  415. package/dist/hono/routes/snapshots.d.ts.map +0 -1
  416. package/dist/hono/routes/snapshots.js +0 -281
  417. package/dist/hono/routes/snapshots.js.map +0 -1
  418. package/dist/hono/routes.d.ts +0 -19
  419. package/dist/hono/routes.d.ts.map +0 -1
  420. package/dist/hono/routes.js +0 -37
  421. package/dist/hono/routes.js.map +0 -1
  422. package/dist/hono/validation.d.ts +0 -5
  423. package/dist/hono/validation.d.ts.map +0 -1
  424. package/dist/hono/validation.js +0 -47
  425. package/dist/hono/validation.js.map +0 -1
  426. package/dist/hono/websocket-origin.d.ts +0 -9
  427. package/dist/hono/websocket-origin.d.ts.map +0 -1
  428. package/dist/hono/websocket-origin.js +0 -96
  429. package/dist/hono/websocket-origin.js.map +0 -1
  430. package/dist/hono/ws.d.ts +0 -341
  431. package/dist/hono/ws.d.ts.map +0 -1
  432. package/dist/hono/ws.js +0 -711
  433. package/dist/hono/ws.js.map +0 -1
  434. package/dist/index.d.ts.map +0 -1
  435. package/dist/index.js.map +0 -1
  436. package/dist/libsql.d.ts +0 -29
  437. package/dist/libsql.d.ts.map +0 -1
  438. package/dist/libsql.js +0 -25
  439. package/dist/libsql.js.map +0 -1
  440. package/dist/migrate.d.ts +0 -14
  441. package/dist/migrate.d.ts.map +0 -1
  442. package/dist/migrate.js +0 -13
  443. package/dist/migrate.js.map +0 -1
  444. package/dist/neon.d.ts +0 -21
  445. package/dist/neon.d.ts.map +0 -1
  446. package/dist/neon.js +0 -22
  447. package/dist/neon.js.map +0 -1
  448. package/dist/notify.d.ts +0 -77
  449. package/dist/notify.d.ts.map +0 -1
  450. package/dist/notify.js +0 -233
  451. package/dist/notify.js.map +0 -1
  452. package/dist/pglite.d.ts +0 -37
  453. package/dist/pglite.d.ts.map +0 -1
  454. package/dist/pglite.js +0 -37
  455. package/dist/pglite.js.map +0 -1
  456. package/dist/plugins/index.d.ts +0 -2
  457. package/dist/plugins/index.d.ts.map +0 -1
  458. package/dist/plugins/index.js +0 -2
  459. package/dist/plugins/index.js.map +0 -1
  460. package/dist/plugins/types.d.ts +0 -73
  461. package/dist/plugins/types.d.ts.map +0 -1
  462. package/dist/plugins/types.js +0 -30
  463. package/dist/plugins/types.js.map +0 -1
  464. package/dist/postgres/index.d.ts +0 -68
  465. package/dist/postgres/index.d.ts.map +0 -1
  466. package/dist/postgres/index.js +0 -924
  467. package/dist/postgres/index.js.map +0 -1
  468. package/dist/proxy/collection.d.ts +0 -7
  469. package/dist/proxy/collection.d.ts.map +0 -1
  470. package/dist/proxy/collection.js +0 -6
  471. package/dist/proxy/collection.js.map +0 -1
  472. package/dist/proxy/handler.d.ts +0 -42
  473. package/dist/proxy/handler.d.ts.map +0 -1
  474. package/dist/proxy/handler.js +0 -102
  475. package/dist/proxy/handler.js.map +0 -1
  476. package/dist/proxy/index.d.ts +0 -9
  477. package/dist/proxy/index.d.ts.map +0 -1
  478. package/dist/proxy/index.js +0 -14
  479. package/dist/proxy/index.js.map +0 -1
  480. package/dist/proxy/mutation-detector.d.ts +0 -35
  481. package/dist/proxy/mutation-detector.d.ts.map +0 -1
  482. package/dist/proxy/mutation-detector.js +0 -246
  483. package/dist/proxy/mutation-detector.js.map +0 -1
  484. package/dist/proxy/oplog.d.ts +0 -30
  485. package/dist/proxy/oplog.d.ts.map +0 -1
  486. package/dist/proxy/oplog.js +0 -137
  487. package/dist/proxy/oplog.js.map +0 -1
  488. package/dist/proxy/types.d.ts +0 -44
  489. package/dist/proxy/types.d.ts.map +0 -1
  490. package/dist/proxy/types.js +0 -7
  491. package/dist/proxy/types.js.map +0 -1
  492. package/dist/prune.d.ts.map +0 -1
  493. package/dist/prune.js.map +0 -1
  494. package/dist/pull.d.ts.map +0 -1
  495. package/dist/pull.js.map +0 -1
  496. package/dist/push.d.ts.map +0 -1
  497. package/dist/push.js.map +0 -1
  498. package/dist/realtime/in-memory.d.ts +0 -13
  499. package/dist/realtime/in-memory.d.ts.map +0 -1
  500. package/dist/realtime/in-memory.js +0 -28
  501. package/dist/realtime/in-memory.js.map +0 -1
  502. package/dist/realtime/index.d.ts +0 -4
  503. package/dist/realtime/index.d.ts.map +0 -1
  504. package/dist/realtime/index.js +0 -3
  505. package/dist/realtime/index.js.map +0 -1
  506. package/dist/realtime/types.d.ts +0 -62
  507. package/dist/realtime/types.d.ts.map +0 -1
  508. package/dist/realtime/types.js +0 -19
  509. package/dist/realtime/types.js.map +0 -1
  510. package/dist/relay/client-role/forward-engine.d.ts +0 -63
  511. package/dist/relay/client-role/forward-engine.d.ts.map +0 -1
  512. package/dist/relay/client-role/forward-engine.js +0 -267
  513. package/dist/relay/client-role/forward-engine.js.map +0 -1
  514. package/dist/relay/client-role/index.d.ts +0 -9
  515. package/dist/relay/client-role/index.d.ts.map +0 -1
  516. package/dist/relay/client-role/index.js +0 -9
  517. package/dist/relay/client-role/index.js.map +0 -1
  518. package/dist/relay/client-role/pull-engine.d.ts +0 -72
  519. package/dist/relay/client-role/pull-engine.d.ts.map +0 -1
  520. package/dist/relay/client-role/pull-engine.js +0 -249
  521. package/dist/relay/client-role/pull-engine.js.map +0 -1
  522. package/dist/relay/client-role/sequence-mapper.d.ts +0 -65
  523. package/dist/relay/client-role/sequence-mapper.d.ts.map +0 -1
  524. package/dist/relay/client-role/sequence-mapper.js +0 -161
  525. package/dist/relay/client-role/sequence-mapper.js.map +0 -1
  526. package/dist/relay/evaluation/relay-paths.d.ts +0 -41
  527. package/dist/relay/evaluation/relay-paths.d.ts.map +0 -1
  528. package/dist/relay/evaluation/relay-paths.js +0 -504
  529. package/dist/relay/evaluation/relay-paths.js.map +0 -1
  530. package/dist/relay/evaluation/rust-boundary.d.ts +0 -47
  531. package/dist/relay/evaluation/rust-boundary.d.ts.map +0 -1
  532. package/dist/relay/evaluation/rust-boundary.js +0 -220
  533. package/dist/relay/evaluation/rust-boundary.js.map +0 -1
  534. package/dist/relay/index.d.ts +0 -37
  535. package/dist/relay/index.d.ts.map +0 -1
  536. package/dist/relay/index.js +0 -44
  537. package/dist/relay/index.js.map +0 -1
  538. package/dist/relay/migrate.d.ts +0 -18
  539. package/dist/relay/migrate.d.ts.map +0 -1
  540. package/dist/relay/migrate.js +0 -99
  541. package/dist/relay/migrate.js.map +0 -1
  542. package/dist/relay/mode-manager.d.ts +0 -60
  543. package/dist/relay/mode-manager.d.ts.map +0 -1
  544. package/dist/relay/mode-manager.js +0 -114
  545. package/dist/relay/mode-manager.js.map +0 -1
  546. package/dist/relay/realtime.d.ts +0 -90
  547. package/dist/relay/realtime.d.ts.map +0 -1
  548. package/dist/relay/realtime.js +0 -147
  549. package/dist/relay/realtime.js.map +0 -1
  550. package/dist/relay/relay.d.ts +0 -190
  551. package/dist/relay/relay.d.ts.map +0 -1
  552. package/dist/relay/relay.js +0 -320
  553. package/dist/relay/relay.js.map +0 -1
  554. package/dist/relay/schema.d.ts +0 -158
  555. package/dist/relay/schema.d.ts.map +0 -1
  556. package/dist/relay/schema.js +0 -7
  557. package/dist/relay/schema.js.map +0 -1
  558. package/dist/relay/server-role/index.d.ts +0 -56
  559. package/dist/relay/server-role/index.d.ts.map +0 -1
  560. package/dist/relay/server-role/index.js +0 -193
  561. package/dist/relay/server-role/index.js.map +0 -1
  562. package/dist/relay/server-role/pull.d.ts +0 -27
  563. package/dist/relay/server-role/pull.d.ts.map +0 -1
  564. package/dist/relay/server-role/pull.js +0 -24
  565. package/dist/relay/server-role/pull.js.map +0 -1
  566. package/dist/relay/server-role/push.d.ts +0 -29
  567. package/dist/relay/server-role/push.d.ts.map +0 -1
  568. package/dist/relay/server-role/push.js +0 -122
  569. package/dist/relay/server-role/push.js.map +0 -1
  570. package/dist/s3/index.d.ts +0 -83
  571. package/dist/s3/index.d.ts.map +0 -1
  572. package/dist/s3/index.js +0 -223
  573. package/dist/s3/index.js.map +0 -1
  574. package/dist/schema.d.ts.map +0 -1
  575. package/dist/schema.js.map +0 -1
  576. package/dist/service-worker/index.d.ts +0 -114
  577. package/dist/service-worker/index.d.ts.map +0 -1
  578. package/dist/service-worker/index.js +0 -487
  579. package/dist/service-worker/index.js.map +0 -1
  580. package/dist/snapshot-artifacts/sqlite-bun.d.ts +0 -15
  581. package/dist/snapshot-artifacts/sqlite-bun.d.ts.map +0 -1
  582. package/dist/snapshot-artifacts/sqlite-bun.js +0 -128
  583. package/dist/snapshot-artifacts/sqlite-bun.js.map +0 -1
  584. package/dist/snapshot-artifacts.d.ts +0 -206
  585. package/dist/snapshot-artifacts.d.ts.map +0 -1
  586. package/dist/snapshot-artifacts.js +0 -545
  587. package/dist/snapshot-artifacts.js.map +0 -1
  588. package/dist/snapshot-chunks/db-metadata.d.ts +0 -56
  589. package/dist/snapshot-chunks/db-metadata.d.ts.map +0 -1
  590. package/dist/snapshot-chunks/db-metadata.js +0 -360
  591. package/dist/snapshot-chunks/db-metadata.js.map +0 -1
  592. package/dist/snapshot-chunks/index.d.ts +0 -8
  593. package/dist/snapshot-chunks/index.d.ts.map +0 -1
  594. package/dist/snapshot-chunks/index.js +0 -8
  595. package/dist/snapshot-chunks/index.js.map +0 -1
  596. package/dist/snapshot-chunks/types.d.ts +0 -80
  597. package/dist/snapshot-chunks/types.d.ts.map +0 -1
  598. package/dist/snapshot-chunks/types.js +0 -8
  599. package/dist/snapshot-chunks/types.js.map +0 -1
  600. package/dist/snapshot-chunks.d.ts +0 -90
  601. package/dist/snapshot-chunks.d.ts.map +0 -1
  602. package/dist/snapshot-chunks.js +0 -304
  603. package/dist/snapshot-chunks.js.map +0 -1
  604. package/dist/sqlite/index.d.ts +0 -53
  605. package/dist/sqlite/index.d.ts.map +0 -1
  606. package/dist/sqlite/index.js +0 -795
  607. package/dist/sqlite/index.js.map +0 -1
  608. package/dist/sqlite3.d.ts +0 -22
  609. package/dist/sqlite3.d.ts.map +0 -1
  610. package/dist/sqlite3.js +0 -99
  611. package/dist/sqlite3.js.map +0 -1
  612. package/dist/stats.d.ts +0 -28
  613. package/dist/stats.d.ts.map +0 -1
  614. package/dist/stats.js +0 -93
  615. package/dist/stats.js.map +0 -1
  616. package/dist/subscriptions/cache.d.ts +0 -58
  617. package/dist/subscriptions/cache.d.ts.map +0 -1
  618. package/dist/subscriptions/cache.js +0 -250
  619. package/dist/subscriptions/cache.js.map +0 -1
  620. package/dist/subscriptions/index.d.ts +0 -3
  621. package/dist/subscriptions/index.d.ts.map +0 -1
  622. package/dist/subscriptions/index.js +0 -3
  623. package/dist/subscriptions/index.js.map +0 -1
  624. package/dist/subscriptions/resolve.d.ts +0 -40
  625. package/dist/subscriptions/resolve.d.ts.map +0 -1
  626. package/dist/subscriptions/resolve.js +0 -275
  627. package/dist/subscriptions/resolve.js.map +0 -1
  628. package/dist/sync.d.ts +0 -24
  629. package/dist/sync.d.ts.map +0 -1
  630. package/dist/sync.js +0 -27
  631. package/dist/sync.js.map +0 -1
  632. package/src/auth-leases.ts +0 -649
  633. package/src/better-sqlite3.ts +0 -35
  634. package/src/blobs/access.ts +0 -244
  635. package/src/blobs/adapters/database.ts +0 -397
  636. package/src/blobs/index.ts +0 -9
  637. package/src/blobs/manager.ts +0 -901
  638. package/src/blobs/migrate.ts +0 -158
  639. package/src/blobs/types.ts +0 -74
  640. package/src/bun-sqlite-ambient.d.ts +0 -19
  641. package/src/bun-sqlite.ts +0 -27
  642. package/src/clients.ts +0 -22
  643. package/src/cloudflare/durable-object.ts +0 -289
  644. package/src/cloudflare/index.ts +0 -22
  645. package/src/cloudflare/r2.ts +0 -526
  646. package/src/cloudflare/scope-cache.ts +0 -341
  647. package/src/cloudflare/sentry.ts +0 -230
  648. package/src/cloudflare/worker.ts +0 -77
  649. package/src/commit-integrity.ts +0 -371
  650. package/src/compaction.ts +0 -77
  651. package/src/crdt-yjs/index.ts +0 -931
  652. package/src/d1.ts +0 -16
  653. package/src/dialect/base.ts +0 -360
  654. package/src/dialect/helpers.ts +0 -92
  655. package/src/dialect/index.ts +0 -7
  656. package/src/dialect/types.ts +0 -247
  657. package/src/encrypted-crdt.ts +0 -786
  658. package/src/filesystem/index.ts +0 -262
  659. package/src/handlers/collection.ts +0 -121
  660. package/src/handlers/create-handler.ts +0 -1134
  661. package/src/handlers/index.ts +0 -3
  662. package/src/handlers/types.ts +0 -403
  663. package/src/helpers/conflict.ts +0 -64
  664. package/src/helpers/emitted-change.ts +0 -69
  665. package/src/helpers/index.ts +0 -12
  666. package/src/helpers/paginate.ts +0 -82
  667. package/src/helpers/scope-authorization.ts +0 -27
  668. package/src/helpers/scope-commit-index.ts +0 -52
  669. package/src/helpers/scope-strings.ts +0 -101
  670. package/src/hono/api-key-auth.ts +0 -177
  671. package/src/hono/audit-redaction.ts +0 -135
  672. package/src/hono/blobs.ts +0 -851
  673. package/src/hono/console/gateway.ts +0 -3046
  674. package/src/hono/console/live-auth.ts +0 -46
  675. package/src/hono/console/route-descriptor.ts +0 -22
  676. package/src/hono/console/routes/api-keys.ts +0 -721
  677. package/src/hono/console/routes/clients.ts +0 -447
  678. package/src/hono/console/routes/commits.ts +0 -1137
  679. package/src/hono/console/routes/context.ts +0 -956
  680. package/src/hono/console/routes/events.ts +0 -669
  681. package/src/hono/console/routes/maintenance.ts +0 -461
  682. package/src/hono/console/routes/shared.ts +0 -816
  683. package/src/hono/console/routes/stats.ts +0 -392
  684. package/src/hono/console/routes/storage.ts +0 -159
  685. package/src/hono/console/routes.ts +0 -146
  686. package/src/hono/console/schema-errors.ts +0 -23
  687. package/src/hono/console/schemas.ts +0 -914
  688. package/src/hono/console/types.ts +0 -223
  689. package/src/hono/console/ui.ts +0 -100
  690. package/src/hono/create-server.ts +0 -230
  691. package/src/hono/errors.ts +0 -50
  692. package/src/hono/index.ts +0 -54
  693. package/src/hono/openapi.ts +0 -139
  694. package/src/hono/proxy/connection-manager.ts +0 -340
  695. package/src/hono/proxy/index.ts +0 -8
  696. package/src/hono/proxy/routes.ts +0 -272
  697. package/src/hono/rate-limit.ts +0 -319
  698. package/src/hono/realtime-sync-packs.ts +0 -354
  699. package/src/hono/routes/audit.ts +0 -499
  700. package/src/hono/routes/auth-leases.ts +0 -119
  701. package/src/hono/routes/combined.ts +0 -583
  702. package/src/hono/routes/context.ts +0 -1629
  703. package/src/hono/routes/health.ts +0 -26
  704. package/src/hono/routes/realtime.ts +0 -808
  705. package/src/hono/routes/shared.ts +0 -1626
  706. package/src/hono/routes/snapshots.ts +0 -345
  707. package/src/hono/routes.ts +0 -68
  708. package/src/hono/validation.ts +0 -81
  709. package/src/hono/websocket-origin.ts +0 -131
  710. package/src/hono/ws.ts +0 -1134
  711. package/src/libsql.ts +0 -51
  712. package/src/migrate.ts +0 -20
  713. package/src/neon.ts +0 -28
  714. package/src/notify.ts +0 -341
  715. package/src/pglite.ts +0 -68
  716. package/src/plugins/index.ts +0 -1
  717. package/src/plugins/types.ts +0 -144
  718. package/src/postgres/index.ts +0 -1291
  719. package/src/proxy/collection.ts +0 -17
  720. package/src/proxy/handler.ts +0 -159
  721. package/src/proxy/index.ts +0 -21
  722. package/src/proxy/mutation-detector.ts +0 -281
  723. package/src/proxy/oplog.ts +0 -181
  724. package/src/proxy/types.ts +0 -46
  725. package/src/realtime/in-memory.ts +0 -33
  726. package/src/realtime/index.ts +0 -7
  727. package/src/realtime/types.ts +0 -90
  728. package/src/relay/bun-types.d.ts +0 -50
  729. package/src/relay/client-role/forward-engine.ts +0 -355
  730. package/src/relay/client-role/index.ts +0 -9
  731. package/src/relay/client-role/pull-engine.ts +0 -329
  732. package/src/relay/client-role/sequence-mapper.ts +0 -201
  733. package/src/relay/evaluation/relay-paths.ts +0 -699
  734. package/src/relay/evaluation/rust-boundary.ts +0 -464
  735. package/src/relay/index.ts +0 -50
  736. package/src/relay/migrate.ts +0 -113
  737. package/src/relay/mode-manager.ts +0 -142
  738. package/src/relay/realtime.ts +0 -207
  739. package/src/relay/relay.ts +0 -431
  740. package/src/relay/schema.ts +0 -171
  741. package/src/relay/server-role/index.ts +0 -338
  742. package/src/relay/server-role/pull.ts +0 -43
  743. package/src/relay/server-role/push.ts +0 -164
  744. package/src/s3/index.ts +0 -346
  745. package/src/service-worker/index.ts +0 -773
  746. package/src/snapshot-artifacts/sqlite-bun.ts +0 -168
  747. package/src/snapshot-artifacts.ts +0 -896
  748. package/src/snapshot-chunks/db-metadata.ts +0 -537
  749. package/src/snapshot-chunks/index.ts +0 -8
  750. package/src/snapshot-chunks/types.ts +0 -105
  751. package/src/snapshot-chunks.ts +0 -453
  752. package/src/sqlite/index.ts +0 -1064
  753. package/src/sqlite3.ts +0 -137
  754. package/src/stats.ts +0 -180
  755. package/src/subscriptions/cache.ts +0 -376
  756. package/src/subscriptions/index.ts +0 -2
  757. package/src/subscriptions/resolve.ts +0 -357
  758. package/src/sync.ts +0 -111
package/dist/pull.js CHANGED
@@ -1,1222 +1,422 @@
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.js';
3
- import { getServerBootstrapOrderFor, } from './handlers/collection.js';
4
- import { EXTERNAL_CLIENT_ID } from './notify.js';
5
- import { sortServerPullPlugins, } from './plugins/types.js';
6
- import { createScopedSnapshotArtifactScopeCacheKey, readBestScopedSnapshotArtifactRefForPageCapacity, } from './snapshot-artifacts.js';
7
- import { createSnapshotChunkScopeCacheKey, insertSnapshotChunk, readSnapshotChunkRefByPageKey, } from './snapshot-chunks.js';
8
- import { createMemoryScopeCache, } from './subscriptions/cache.js';
9
- import { resolveEffectiveScopesForSubscriptions } from './subscriptions/resolve.js';
10
- const defaultScopeCache = createMemoryScopeCache();
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;
46
- }
47
- if (!request.compressions?.includes(SYNC_SNAPSHOT_CHUNK_COMPRESSION)) {
48
- return null;
1
+ /**
2
+ * Pull: incremental commit delivery, cursors, bootstrap segments
3
+ * (SPEC.md §4, §5).
4
+ */
5
+ import { decodeRow, encodeRowsSegment, } from '@syncular/core';
6
+ import { clockOf, limitsOf } from './context.js';
7
+ import { scopeDigest } from './scopes.js';
8
+ import { issueSegmentUrl } from './signed-url.js';
9
+ /**
10
+ * Resolve the §5.3 image builder: the host-injected one if present, else the
11
+ * in-tree `buildSqliteImage` on a Bun runtime (dynamic import so `bun:sqlite`
12
+ * is never a static dep of the neutral core), else `undefined` (rows lane).
13
+ * Memoized so the dynamic import happens at most once per process.
14
+ */
15
+ let cachedDefaultBuilder;
16
+ async function resolveImageBuilder(ctx) {
17
+ if (ctx.sqliteImageBuilder !== undefined)
18
+ return ctx.sqliteImageBuilder;
19
+ if (cachedDefaultBuilder === undefined) {
20
+ const hasBun = globalThis.Bun !== undefined;
21
+ cachedDefaultBuilder = hasBun
22
+ ? (await import('./sqlite-image.js')).buildSqliteImage
23
+ : null;
49
24
  }
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
- });
25
+ return cachedDefaultBuilder ?? undefined;
90
26
  }
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 };
27
+ /** §4.2 accept bitmask. */
28
+ export const ACCEPT_INLINE_ROWS = 1 << 0;
29
+ export const ACCEPT_EXTERNAL_ROWS = 1 << 1;
30
+ export const ACCEPT_SQLITE = 1 << 2;
31
+ export const ACCEPT_SIGNED_URLS = 1 << 3;
32
+ function clamp(value, min, max, dflt) {
33
+ if (value === 0)
34
+ return dflt;
35
+ return Math.min(max, Math.max(min, value));
99
36
  }
100
- async function encodeCompressedSnapshotChunkToStream(chunks, gzipLevel) {
101
- const encoded = await encodeCompressedSnapshotChunk(chunks, gzipLevel);
37
+ /** §4.2 defaults and silent clamps (the v1 values). */
38
+ export function clampPullLimits(header) {
102
39
  return {
103
- stream: bytesToReadableStream(encoded.body),
104
- byteLength: encoded.body.length,
105
- sha256: encoded.sha256,
106
- gzipMs: encoded.gzipMs,
107
- hashMs: encoded.hashMs,
40
+ limitCommits: clamp(header.limitCommits, 1, 1000, 1000),
41
+ limitSnapshotRows: clamp(header.limitSnapshotRows, 1, 50000, 1000),
42
+ maxSnapshotPages: clamp(header.maxSnapshotPages, 1, 50, 4),
43
+ accept: header.accept,
108
44
  };
109
45
  }
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`);
125
- }
126
- return row;
127
- }
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));
46
+ function parseBootstrapToken(raw, table) {
47
+ if (raw === undefined)
48
+ return undefined;
49
+ try {
50
+ const parsed = JSON.parse(raw);
51
+ if (typeof parsed.asOfCommitSeq !== 'number' ||
52
+ !Array.isArray(parsed.tables) ||
53
+ typeof parsed.tableIndex !== 'number' ||
54
+ (parsed.rowCursor !== null && typeof parsed.rowCursor !== 'string') ||
55
+ parsed.tables[0] !== table) {
56
+ return undefined;
148
57
  }
58
+ return {
59
+ asOfCommitSeq: parsed.asOfCommitSeq,
60
+ tables: parsed.tables,
61
+ tableIndex: parsed.tableIndex,
62
+ rowCursor: parsed.rowCursor ?? null,
63
+ };
149
64
  }
150
- return columns.map((column) => ({
151
- name: column.name,
152
- type: column.type ?? 'json',
153
- ...(column.nullable || column.presentCount < rows.length
154
- ? { nullable: true }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ function commitFrame(table, commit) {
70
+ const changes = commit.changes.map((change) => ({
71
+ tableIndex: 0,
72
+ rowId: change.rowId,
73
+ op: change.op,
74
+ ...(change.rowVersion !== undefined
75
+ ? { rowVersion: change.rowVersion }
155
76
  : {}),
77
+ scopes: change.scopes,
78
+ ...(change.payload !== undefined ? { row: change.payload } : {}),
156
79
  }));
80
+ return {
81
+ type: 'COMMIT',
82
+ commitSeq: commit.commitSeq,
83
+ createdAtMs: commit.createdAtMs,
84
+ actorId: commit.actorId,
85
+ tables: [table],
86
+ changes,
87
+ };
157
88
  }
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';
89
+ function chunkRows(rows, size) {
90
+ if (rows.length === 0)
91
+ return [];
92
+ const blocks = [];
93
+ for (let i = 0; i < rows.length; i += size) {
94
+ blocks.push(rows.slice(i, i + size));
170
95
  }
171
- return 'json';
96
+ return blocks;
172
97
  }
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';
98
+ /** §5.4 signed-URL fields for a descriptor, when the client asked (bit 3). */
99
+ async function signedUrlFields(ctx, limits, segmentId, digest, now) {
100
+ if ((limits.accept & ACCEPT_SIGNED_URLS) === 0 || !ctx.signedUrls)
101
+ return {};
102
+ return issueSegmentUrl(ctx.signedUrls, {
103
+ segmentId,
104
+ partition: ctx.partition,
105
+ scopeDigest: digest,
106
+ nowMs: now,
107
+ });
181
108
  }
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()));
109
+ function segmentRefFrame(record, extra) {
110
+ return {
111
+ type: 'SEGMENT_REF',
112
+ segmentId: record.segmentId,
113
+ mediaType: record.mediaType,
114
+ table: record.table,
115
+ byteLength: record.byteLength,
116
+ rowCount: record.rowCount,
117
+ asOfCommitSeq: record.asOfCommitSeq,
118
+ scopeDigest: record.scopeDigest,
119
+ ...(record.rowCursor !== null ? { rowCursor: record.rowCursor } : {}),
120
+ ...(record.nextRowCursor !== null
121
+ ? { nextRowCursor: record.nextRowCursor }
122
+ : {}),
123
+ ...(extra.url !== undefined ? { url: extra.url } : {}),
124
+ ...(extra.urlExpiresAtMs !== undefined
125
+ ? { urlExpiresAtMs: extra.urlExpiresAtMs }
126
+ : {}),
127
+ };
198
128
  }
199
129
  /**
200
- * Sanitize a numeric limit parameter with bounds checking.
201
- * Handles NaN, negative values, and undefined.
130
+ * §5.3 sqlite-image lane: only at the start of a table, only when the
131
+ * client advertised bit 2, and only when the snapshot exceeds one rows
132
+ * page. Reuses an unexpired stored image for the same (partition, table,
133
+ * schemaVersion, scope digest, pin) instead of rebuilding — the
134
+ * bootstrap-storm rule. Returns false when the table is not eligible
135
+ * (the rows lane takes over).
202
136
  */
203
- function sanitizeLimit(value, defaultValue, min, max) {
204
- if (value === undefined || value === null)
205
- return defaultValue;
206
- if (Number.isNaN(value))
207
- return defaultValue;
208
- return Math.max(min, Math.min(max, value));
209
- }
210
- function sanitizeGzipLevel(value) {
211
- if (value === undefined || value === null || !Number.isFinite(value)) {
212
- return DEFAULT_SNAPSHOT_CHUNK_GZIP_LEVEL;
137
+ async function* sqliteImageSegment(ctx, schema, limits, plan, asOf, digest, trace) {
138
+ const { storage, segments, partition } = ctx;
139
+ const now = clockOf(ctx)();
140
+ const existing = await segments.find({
141
+ partition,
142
+ table: plan.table.name,
143
+ schemaVersion: schema.version,
144
+ mediaType: 'sqlite',
145
+ scopeDigest: digest,
146
+ asOfCommitSeq: asOf,
147
+ }, now);
148
+ if (existing !== undefined) {
149
+ trace?.segments.push({
150
+ mediaType: 'sqlite',
151
+ delivery: 'ref',
152
+ origin: 'reused',
153
+ bytes: existing.byteLength,
154
+ rows: existing.rowCount,
155
+ });
156
+ yield segmentRefFrame(existing, await signedUrlFields(ctx, limits, existing.segmentId, digest, now));
157
+ return true;
213
158
  }
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);
159
+ // Eligibility probe (§5.3): image only when the snapshot exceeds one
160
+ // rows page — smaller tables stay on the (typically inline) rows lane.
161
+ const probe = await storage.scanRows(partition, {
162
+ table: plan.table.name,
163
+ scopeFilter: plan.effective,
164
+ afterRowId: null,
165
+ limit: limits.limitSnapshotRows + 1,
224
166
  });
225
- }
226
- /**
227
- * Merge all scope values into a flat ScopeValues for cursor tracking.
228
- */
229
- function mergeScopes(subscriptions) {
230
- const result = {};
231
- for (const sub of subscriptions) {
232
- for (const [key, value] of Object.entries(sub.scopes)) {
233
- if (!result[key])
234
- result[key] = new Set();
235
- const arr = Array.isArray(value) ? value : [value];
236
- for (const v of arr)
237
- result[key].add(v);
238
- }
239
- }
240
- const merged = {};
241
- for (const [key, set] of Object.entries(result)) {
242
- const arr = Array.from(set);
243
- if (arr.length === 0)
244
- continue;
245
- merged[key] = arr.length === 1 ? arr[0] : arr;
167
+ if (probe.length <= limits.limitSnapshotRows)
168
+ return false;
169
+ // §5.3: building an image needs a SQLite engine. The host injects the
170
+ // builder through `sqliteImageBuilder`; when omitted we default to the
171
+ // in-tree `buildSqliteImage` ONLY on a Bun runtime, reached by a *dynamic*
172
+ // import so `bun:sqlite` is never a static dependency of the pull path
173
+ // (TODO §4.2 neutrality — enforced by test/runtime-neutrality.test.ts). On
174
+ // Workers/edge (no `Bun`) this yields the rows lane for a bit-2 client — a
175
+ // support floor, not a fallback (§5.3: sqlite is an *accept*, not a demand).
176
+ const buildImage = await resolveImageBuilder(ctx);
177
+ if (buildImage === undefined)
178
+ return false;
179
+ // The probe rows are the snapshot's first page — keep them and scan on
180
+ // from the probe's cursor instead of re-reading the whole prefix (the
181
+ // scan is keyset-ordered by rowId, so the concatenation is exactly the
182
+ // rows a single full scan would return).
183
+ const rows = [...probe];
184
+ let afterRowId = probe[probe.length - 1]?.rowId ?? null;
185
+ for (;;) {
186
+ const scanned = await storage.scanRows(partition, {
187
+ table: plan.table.name,
188
+ scopeFilter: plan.effective,
189
+ afterRowId,
190
+ limit: 50_000,
191
+ });
192
+ rows.push(...scanned);
193
+ const last = scanned[scanned.length - 1];
194
+ if (scanned.length < 50_000 || last === undefined)
195
+ break;
196
+ afterRowId = last.rowId;
246
197
  }
247
- return merged;
198
+ const bytes = buildImage({
199
+ table: plan.table,
200
+ schemaVersion: schema.version,
201
+ asOfCommitSeq: asOf,
202
+ scopeDigest: digest,
203
+ rows,
204
+ });
205
+ const record = await segments.put({
206
+ partition,
207
+ table: plan.table.name,
208
+ schemaVersion: schema.version,
209
+ mediaType: 'sqlite',
210
+ scopeDigest: digest,
211
+ asOfCommitSeq: asOf,
212
+ rowCount: rows.length,
213
+ rowCursor: null,
214
+ nextRowCursor: null,
215
+ }, bytes, now);
216
+ trace?.segments.push({
217
+ mediaType: 'sqlite',
218
+ delivery: 'ref',
219
+ origin: 'built',
220
+ bytes: record.byteLength,
221
+ rows: record.rowCount,
222
+ });
223
+ yield segmentRefFrame(record, await signedUrlFields(ctx, limits, record.segmentId, digest, now));
224
+ return true;
248
225
  }
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})`);
226
+ async function* bootstrapSegments(ctx, schema, limits, plan, asOf, startRowCursor, trace) {
227
+ const { storage, segments, partition } = ctx;
228
+ const serverLimits = limitsOf(ctx);
229
+ const digest = await scopeDigest(plan.effective);
230
+ const now = clockOf(ctx)();
231
+ // §5.3: the sqlite-image lane — whole-table, chosen only at the start
232
+ // of a table (a mid-table resume stays on the rows lane, never
233
+ // switching lanes), and only when the client advertised bit 2.
234
+ // §5.11: a table with any encrypted column is image-INELIGIBLE — an image
235
+ // copies ciphertext wholesale with no per-row decrypt pass, so it MUST be
236
+ // served on the rows lane (which decrypts per row on the client).
237
+ if ((limits.accept & ACCEPT_SQLITE) !== 0 &&
238
+ startRowCursor === null &&
239
+ plan.table.encryptedColumnIndices.length === 0) {
240
+ const imaged = yield* sqliteImageSegment(ctx, schema, limits, plan, asOf, digest, trace);
241
+ if (imaged)
242
+ return { complete: true, rowCursor: null };
252
243
  }
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,
244
+ let rowCursor = startRowCursor;
245
+ for (let page = 0; page < limits.maxSnapshotPages; page++) {
246
+ const scanned = await storage.scanRows(partition, {
247
+ table: plan.table.name,
248
+ scopeFilter: plan.effective,
249
+ afterRowId: rowCursor,
250
+ limit: limits.limitSnapshotRows + 1,
251
+ });
252
+ const pageRows = scanned.slice(0, limits.limitSnapshotRows);
253
+ const hasMore = scanned.length > limits.limitSnapshotRows;
254
+ // §5.2: every row record carries the row's current server_version.
255
+ const decoded = pageRows.map((row) => ({
256
+ serverVersion: row.serverVersion,
257
+ values: decodeRow(plan.table.columns, row.payload),
258
+ }));
259
+ const bytes = encodeRowsSegment({
260
+ table: plan.table.name,
261
+ schemaVersion: schema.version,
262
+ columns: plan.table.columns,
263
+ blocks: chunkRows(decoded, 1000),
277
264
  });
278
- if (nextChanges.length !== changes.length) {
279
- throw new Error(`Server pull plugin "${plugin.name}" cannot change pull change count (${changes.length} -> ${nextChanges.length})`);
265
+ const lastRow = pageRows[pageRows.length - 1];
266
+ const nextRowCursor = hasMore && lastRow !== undefined ? lastRow.rowId : null;
267
+ const canInline = (limits.accept & ACCEPT_INLINE_ROWS) !== 0;
268
+ const canExternal = (limits.accept & ACCEPT_EXTERNAL_ROWS) !== 0;
269
+ const inline = canInline &&
270
+ (bytes.length <= serverLimits.inlineSegmentMaxBytes || !canExternal);
271
+ if (inline) {
272
+ trace?.segments.push({
273
+ mediaType: 'rows',
274
+ delivery: 'inline',
275
+ origin: 'built',
276
+ bytes: bytes.length,
277
+ rows: pageRows.length,
278
+ });
279
+ yield { type: 'SEGMENT_INLINE', payload: bytes };
280
280
  }
281
- for (let i = 0; i < changes.length; i += 1) {
282
- assertPullChangeIdentityUnchanged(`Server pull plugin "${plugin.name}"`, changes[i], nextChanges[i]);
281
+ else {
282
+ const record = await segments.put({
283
+ partition,
284
+ table: plan.table.name,
285
+ schemaVersion: schema.version,
286
+ mediaType: 'rows',
287
+ scopeDigest: digest,
288
+ asOfCommitSeq: asOf,
289
+ rowCount: pageRows.length,
290
+ rowCursor,
291
+ nextRowCursor,
292
+ }, bytes, now);
293
+ trace?.segments.push({
294
+ mediaType: 'rows',
295
+ delivery: 'ref',
296
+ origin: 'built',
297
+ bytes: record.byteLength,
298
+ rows: record.rowCount,
299
+ });
300
+ yield segmentRefFrame(record, await signedUrlFields(ctx, limits, record.segmentId, digest, now));
283
301
  }
284
- changes = [...nextChanges];
302
+ if (!hasMore)
303
+ return { complete: true, rowCursor: null };
304
+ rowCursor = nextRowCursor;
285
305
  }
286
- return changes;
306
+ return { complete: false, rowCursor };
287
307
  }
288
- function summarizePullResponse(response) {
289
- const subscriptions = response.subscriptions ?? [];
290
- let activeSubscriptionCount = 0;
291
- let revokedSubscriptionCount = 0;
292
- let bootstrapSubscriptionCount = 0;
293
- let commitCount = 0;
294
- let changeCount = 0;
295
- let snapshotPageCount = 0;
296
- for (const sub of subscriptions) {
297
- if (sub.status === 'revoked') {
298
- revokedSubscriptionCount += 1;
308
+ /**
309
+ * Produce the `SUB_START … SUB_END` section for one subscription (§1.6),
310
+ * returning the cursor recorded for the retention watermark (§4.5).
311
+ */
312
+ export async function* subscriptionSection(ctx, schema, limits, plan, maxSeq, horizonSeq, trace) {
313
+ const sub = plan.frame;
314
+ if (plan.status === 'revoked') {
315
+ yield {
316
+ type: 'SUB_START',
317
+ id: sub.id,
318
+ status: 'revoked',
319
+ reasonCode: 'sync.scope_revoked',
320
+ effectiveScopes: {},
321
+ bootstrap: false,
322
+ };
323
+ yield { type: 'SUB_END', nextCursor: sub.cursor };
324
+ return { nextCursor: sub.cursor, active: false };
325
+ }
326
+ const token = parseBootstrapToken(sub.bootstrapState, sub.table);
327
+ // §4.6: a cursor behind the horizon (and not resuming a bootstrap)
328
+ // cannot compute deltas — answer `reset` and echo the cursor.
329
+ if (token === undefined && sub.cursor >= 0 && sub.cursor < horizonSeq) {
330
+ yield {
331
+ type: 'SUB_START',
332
+ id: sub.id,
333
+ status: 'reset',
334
+ reasonCode: 'sync.cursor_expired',
335
+ effectiveScopes: {},
336
+ bootstrap: false,
337
+ };
338
+ yield { type: 'SUB_END', nextCursor: sub.cursor };
339
+ return { nextCursor: sub.cursor, active: false };
340
+ }
341
+ const bootstrapping = token !== undefined || sub.cursor < 0 || sub.cursor > maxSeq;
342
+ if (bootstrapping) {
343
+ // §4.7: resume at the pinned point unless the pin fell behind the
344
+ // horizon (or the token is unusable) — then restart from a fresh pin.
345
+ const resume = token !== undefined && token.asOfCommitSeq >= horizonSeq
346
+ ? token
347
+ : undefined;
348
+ const asOf = resume?.asOfCommitSeq ?? maxSeq;
349
+ const startCursor = resume?.rowCursor ?? null;
350
+ yield {
351
+ type: 'SUB_START',
352
+ id: sub.id,
353
+ status: 'active',
354
+ reasonCode: '',
355
+ effectiveScopes: plan.effective,
356
+ bootstrap: true,
357
+ };
358
+ const outcome = yield* bootstrapSegments(ctx, schema, limits, plan, asOf, startCursor, trace);
359
+ if (outcome.complete) {
360
+ yield { type: 'SUB_END', nextCursor: asOf };
299
361
  }
300
362
  else {
301
- activeSubscriptionCount += 1;
302
- }
303
- if (sub.bootstrap) {
304
- bootstrapSubscriptionCount += 1;
363
+ const nextToken = {
364
+ asOfCommitSeq: asOf,
365
+ tables: [sub.table],
366
+ tableIndex: 0,
367
+ rowCursor: outcome.rowCursor,
368
+ };
369
+ yield {
370
+ type: 'SUB_END',
371
+ nextCursor: asOf,
372
+ bootstrapState: JSON.stringify(nextToken),
373
+ };
305
374
  }
306
- const commits = sub.commits ?? [];
307
- commitCount += commits.length;
308
- for (const commit of commits) {
309
- changeCount += commit.changes?.length ?? 0;
310
- }
311
- snapshotPageCount += sub.snapshots?.length ?? 0;
375
+ return { nextCursor: asOf, active: true };
312
376
  }
313
- return {
314
- subscriptionCount: subscriptions.length,
315
- activeSubscriptionCount,
316
- revokedSubscriptionCount,
317
- bootstrapSubscriptionCount,
318
- commitCount,
319
- changeCount,
320
- snapshotPageCount,
377
+ // Incremental (§4.5): window cursor < commitSeq <= maxSeq, oldest first,
378
+ // cut off at limitCommits total changes, never splitting a commit.
379
+ yield {
380
+ type: 'SUB_START',
381
+ id: sub.id,
382
+ status: 'active',
383
+ reasonCode: '',
384
+ effectiveScopes: plan.effective,
385
+ bootstrap: false,
321
386
  };
322
- }
323
- function recordPullMetrics(args) {
324
- const { status, dedupeRows, durationMs, stats } = args;
325
- const attributes = {
326
- status,
327
- dedupe_rows: dedupeRows,
328
- };
329
- countSyncMetric('sync.server.pull.requests', 1, { attributes });
330
- distributionSyncMetric('sync.server.pull.duration_ms', durationMs, {
331
- unit: 'millisecond',
332
- attributes,
333
- });
334
- distributionSyncMetric('sync.server.pull.subscriptions', stats.subscriptionCount, { attributes });
335
- distributionSyncMetric('sync.server.pull.active_subscriptions', stats.activeSubscriptionCount, { attributes });
336
- distributionSyncMetric('sync.server.pull.revoked_subscriptions', stats.revokedSubscriptionCount, { attributes });
337
- distributionSyncMetric('sync.server.pull.bootstrap_subscriptions', stats.bootstrapSubscriptionCount, { attributes });
338
- distributionSyncMetric('sync.server.pull.commits', stats.commitCount, {
339
- attributes,
387
+ const commits = await ctx.storage.readCommitWindow(ctx.partition, {
388
+ table: sub.table,
389
+ scopeFilter: plan.effective,
390
+ afterSeq: sub.cursor,
391
+ throughSeq: maxSeq,
392
+ limitChanges: limits.limitCommits + 1,
340
393
  });
341
- distributionSyncMetric('sync.server.pull.changes', stats.changeCount, {
342
- attributes,
343
- });
344
- distributionSyncMetric('sync.server.pull.snapshot_pages', stats.snapshotPageCount, { attributes });
345
- }
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
- }
352
- const executor = trx;
353
- const rows = await executor
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')
366
- .execute();
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;
374
- }
375
- export async function pull(args) {
376
- const { request, dialect } = args;
377
- const db = args.db;
378
- const pullPlugins = sortServerPullPlugins(args.plugins);
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}`;
389
- const requestedSubscriptionCount = Array.isArray(request.subscriptions)
390
- ? request.subscriptions.length
391
- : 0;
392
- const startedAtMs = Date.now();
393
- return startSyncSpan({
394
- name: 'sync.server.pull',
395
- op: 'sync.pull',
396
- attributes: {
397
- requested_subscription_count: requestedSubscriptionCount,
398
- dedupe_rows: request.dedupeRows === true,
399
- },
400
- }, async (span) => {
401
- try {
402
- // Validate and sanitize request limits
403
- const limitCommits = sanitizeLimit(request.limitCommits, 1000, 1, 1000);
404
- const limitSnapshotRows = sanitizeLimit(request.limitSnapshotRows, 1000, 1, 50000);
405
- const maxSnapshotPages = sanitizeLimit(request.maxSnapshotPages, 4, 1, 50);
406
- const dedupeRows = request.dedupeRows === true;
407
- const snapshotChunkEncoding = SYNC_SNAPSHOT_CHUNK_ENCODING;
408
- const snapshotArtifactSelection = resolveSnapshotArtifactSelection(request.snapshotArtifacts, clientSchemaVersion);
409
- const snapshotArtifactSchemaVersion = snapshotArtifactSelection?.schemaVersion ?? null;
410
- // Resolve effective scopes for each subscription
411
- const resolved = await resolveEffectiveScopesForSubscriptions({
412
- db,
413
- auth: args.auth,
414
- subscriptions: request.subscriptions ?? [],
415
- handlers: args.handlers,
416
- scopeCache: args.scopeCache ?? defaultScopeCache,
417
- });
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,
426
- });
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
- }
446
- }
447
- const maxExternalCommitByTable = minSubCursor < Number.MAX_SAFE_INTEGER && minSubCursor >= 0
448
- ? await readLatestExternalCommitByTable(trx, {
449
- partitionId,
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: [],
551
- };
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,
636
- partitionId,
637
- scopeKey: cacheKey,
638
- scope: bundle.table,
639
- asOfCommitSeq: effectiveState.asOfCommitSeq,
640
- rowCursor: bundle.startCursor,
641
- rowLimit: bundleRowLimit,
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
- }),
675
- });
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
- }
834
- activeBundle.isLastPage = true;
835
- await flushSnapshotBundle(activeBundle);
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;
847
- }
848
- if (activeBundle) {
849
- await flushSnapshotBundle(activeBundle);
850
- }
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);
862
- continue;
863
- }
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);
879
- }
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}`);
907
- }
908
- const incrementalItems = incrementalRows.map((r) => ({
909
- commitSeq: r.commit_seq,
910
- createdAt: r.created_at,
911
- actorId: r.actor_id,
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,
1037
- });
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
- }
1051
- subResponses.push({
1052
- id: sub.id,
1053
- status: 'active',
1054
- scopes: effectiveScopes,
1055
- bootstrap: false,
1056
- nextCursor,
1057
- ...(integrity ? { integrity } : {}),
1058
- commits,
1059
- });
1060
- nextCursors.push(nextCursor);
1061
- }
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,
1073
- };
1074
- });
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, {
1080
- partitionId: pending.cacheLookup.partitionId,
1081
- scopeKey: pending.cacheLookup.scopeKey,
1082
- scope: pending.cacheLookup.scope,
1083
- asOfCommitSeq: pending.cacheLookup.asOfCommitSeq,
1084
- rowCursor: pending.cacheLookup.rowCursor,
1085
- rowLimit: pending.cacheLookup.rowLimit,
1086
- encoding: snapshotChunkEncoding,
1087
- compression: SYNC_SNAPSHOT_CHUNK_COMPRESSION,
1088
- });
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
- });
1152
- }
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;
1190
- }
1191
- }
1192
- throw new Error('Pull transaction retry loop exhausted unexpectedly');
394
+ let delivered = 0;
395
+ let deliveredCommits = 0;
396
+ let lastDeliveredSeq = sub.cursor;
397
+ for (const commit of commits) {
398
+ if (delivered > 0 &&
399
+ delivered + commit.changes.length > limits.limitCommits) {
400
+ break;
1193
401
  }
1194
- catch (error) {
1195
- const durationMs = Math.max(0, Date.now() - startedAtMs);
1196
- span.setAttribute('status', 'error');
1197
- span.setAttribute('duration_ms', durationMs);
1198
- span.setStatus('error');
1199
- recordPullMetrics({
1200
- status: 'error',
1201
- dedupeRows: request.dedupeRows === true,
1202
- durationMs,
1203
- stats: {
1204
- subscriptionCount: 0,
1205
- activeSubscriptionCount: 0,
1206
- revokedSubscriptionCount: 0,
1207
- bootstrapSubscriptionCount: 0,
1208
- commitCount: 0,
1209
- changeCount: 0,
1210
- snapshotPageCount: 0,
1211
- },
1212
- });
1213
- captureSyncException(error, {
1214
- event: 'sync.server.pull',
1215
- requestedSubscriptionCount,
1216
- dedupeRows: request.dedupeRows === true,
1217
- });
1218
- throw error;
1219
- }
1220
- });
402
+ yield commitFrame(sub.table, commit);
403
+ delivered += commit.changes.length;
404
+ deliveredCommits += 1;
405
+ lastDeliveredSeq = commit.commitSeq;
406
+ if (delivered >= limits.limitCommits)
407
+ break;
408
+ }
409
+ const totalReturned = commits.reduce((n, c) => n + c.changes.length, 0);
410
+ // The window is proven exhausted only when the storage lookahead came
411
+ // back under budget (it scanned to `throughSeq`) and every returned
412
+ // commit was delivered.
413
+ const exhausted = totalReturned <= limits.limitCommits && deliveredCommits === commits.length;
414
+ // §4.5: the cursor advances even when no matching changes exist; when
415
+ // the change limit truncated the window it stops at the last fully
416
+ // delivered commit.
417
+ const nextCursor = exhausted
418
+ ? Math.max(sub.cursor, maxSeq)
419
+ : Math.max(sub.cursor, lastDeliveredSeq);
420
+ yield { type: 'SUB_END', nextCursor };
421
+ return { nextCursor, active: true };
1221
422
  }
1222
- //# sourceMappingURL=pull.js.map