@syncular/server 0.0.6-96 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (601) hide show
  1. package/README.md +1 -2
  2. package/dist/auth-leases.d.ts +85 -0
  3. package/dist/auth-leases.d.ts.map +1 -0
  4. package/dist/auth-leases.js +368 -0
  5. package/dist/auth-leases.js.map +1 -0
  6. package/dist/better-sqlite3.d.ts +22 -0
  7. package/dist/better-sqlite3.d.ts.map +1 -0
  8. package/dist/better-sqlite3.js +16 -0
  9. package/dist/better-sqlite3.js.map +1 -0
  10. package/dist/blobs/access.d.ts +35 -0
  11. package/dist/blobs/access.d.ts.map +1 -0
  12. package/dist/blobs/access.js +149 -0
  13. package/dist/blobs/access.js.map +1 -0
  14. package/dist/blobs/adapters/database.d.ts +26 -9
  15. package/dist/blobs/adapters/database.d.ts.map +1 -1
  16. package/dist/blobs/adapters/database.js +68 -23
  17. package/dist/blobs/adapters/database.js.map +1 -1
  18. package/dist/blobs/index.d.ts +1 -0
  19. package/dist/blobs/index.d.ts.map +1 -1
  20. package/dist/blobs/index.js +5 -4
  21. package/dist/blobs/index.js.map +1 -1
  22. package/dist/blobs/manager.d.ts +69 -44
  23. package/dist/blobs/manager.d.ts.map +1 -1
  24. package/dist/blobs/manager.js +250 -65
  25. package/dist/blobs/manager.js.map +1 -1
  26. package/dist/blobs/migrate.d.ts.map +1 -1
  27. package/dist/blobs/migrate.js +16 -8
  28. package/dist/blobs/migrate.js.map +1 -1
  29. package/dist/blobs/types.d.ts +4 -0
  30. package/dist/blobs/types.d.ts.map +1 -1
  31. package/dist/bun-sqlite.d.ts +19 -0
  32. package/dist/bun-sqlite.d.ts.map +1 -0
  33. package/dist/bun-sqlite.js +19 -0
  34. package/dist/bun-sqlite.js.map +1 -0
  35. package/dist/clients.d.ts +1 -0
  36. package/dist/clients.d.ts.map +1 -1
  37. package/dist/clients.js.map +1 -1
  38. package/dist/cloudflare/durable-object.d.ts +93 -0
  39. package/dist/cloudflare/durable-object.d.ts.map +1 -0
  40. package/dist/cloudflare/durable-object.js +210 -0
  41. package/dist/cloudflare/durable-object.js.map +1 -0
  42. package/dist/cloudflare/index.d.ts +22 -0
  43. package/dist/cloudflare/index.d.ts.map +1 -0
  44. package/dist/cloudflare/index.js +22 -0
  45. package/dist/cloudflare/index.js.map +1 -0
  46. package/dist/cloudflare/r2.d.ts +180 -0
  47. package/dist/cloudflare/r2.d.ts.map +1 -0
  48. package/dist/cloudflare/r2.js +258 -0
  49. package/dist/cloudflare/r2.js.map +1 -0
  50. package/dist/cloudflare/scope-cache.d.ts +54 -0
  51. package/dist/cloudflare/scope-cache.d.ts.map +1 -0
  52. package/dist/cloudflare/scope-cache.js +223 -0
  53. package/dist/cloudflare/scope-cache.js.map +1 -0
  54. package/dist/cloudflare/sentry.d.ts +47 -0
  55. package/dist/cloudflare/sentry.d.ts.map +1 -0
  56. package/dist/cloudflare/sentry.js +163 -0
  57. package/dist/cloudflare/sentry.js.map +1 -0
  58. package/dist/cloudflare/worker.d.ts +46 -0
  59. package/dist/cloudflare/worker.d.ts.map +1 -0
  60. package/dist/cloudflare/worker.js +63 -0
  61. package/dist/cloudflare/worker.js.map +1 -0
  62. package/dist/commit-integrity.d.ts +38 -0
  63. package/dist/commit-integrity.d.ts.map +1 -0
  64. package/dist/commit-integrity.js +260 -0
  65. package/dist/commit-integrity.js.map +1 -0
  66. package/dist/compaction.js.map +1 -1
  67. package/dist/crdt-yjs/index.d.ts +99 -0
  68. package/dist/crdt-yjs/index.d.ts.map +1 -0
  69. package/dist/crdt-yjs/index.js +629 -0
  70. package/dist/crdt-yjs/index.js.map +1 -0
  71. package/dist/d1.d.ts +13 -0
  72. package/dist/d1.d.ts.map +1 -0
  73. package/dist/d1.js +14 -0
  74. package/dist/d1.js.map +1 -0
  75. package/dist/dialect/base.d.ts +11 -1
  76. package/dist/dialect/base.d.ts.map +1 -1
  77. package/dist/dialect/base.js +43 -6
  78. package/dist/dialect/base.js.map +1 -1
  79. package/dist/dialect/helpers.d.ts +6 -0
  80. package/dist/dialect/helpers.d.ts.map +1 -1
  81. package/dist/dialect/helpers.js +24 -0
  82. package/dist/dialect/helpers.js.map +1 -1
  83. package/dist/dialect/index.js +3 -3
  84. package/dist/dialect/types.d.ts +40 -0
  85. package/dist/dialect/types.d.ts.map +1 -1
  86. package/dist/encrypted-crdt.d.ts +67 -0
  87. package/dist/encrypted-crdt.d.ts.map +1 -0
  88. package/dist/encrypted-crdt.js +426 -0
  89. package/dist/encrypted-crdt.js.map +1 -0
  90. package/dist/errors.d.ts +10 -0
  91. package/dist/errors.d.ts.map +1 -0
  92. package/dist/errors.js +13 -0
  93. package/dist/errors.js.map +1 -0
  94. package/dist/filesystem/index.d.ts +44 -0
  95. package/dist/filesystem/index.d.ts.map +1 -0
  96. package/dist/filesystem/index.js +164 -0
  97. package/dist/filesystem/index.js.map +1 -0
  98. package/dist/handlers/collection.d.ts +11 -3
  99. package/dist/handlers/collection.d.ts.map +1 -1
  100. package/dist/handlers/collection.js +34 -56
  101. package/dist/handlers/collection.js.map +1 -1
  102. package/dist/handlers/create-handler.d.ts +31 -7
  103. package/dist/handlers/create-handler.d.ts.map +1 -1
  104. package/dist/handlers/create-handler.js +332 -115
  105. package/dist/handlers/create-handler.js.map +1 -1
  106. package/dist/handlers/index.js +3 -3
  107. package/dist/handlers/types.d.ts +86 -1
  108. package/dist/handlers/types.d.ts.map +1 -1
  109. package/dist/helpers/conflict.js.map +1 -1
  110. package/dist/helpers/emitted-change.js.map +1 -1
  111. package/dist/helpers/index.d.ts +2 -0
  112. package/dist/helpers/index.d.ts.map +1 -1
  113. package/dist/helpers/index.js +6 -4
  114. package/dist/helpers/index.js.map +1 -1
  115. package/dist/helpers/paginate.js.map +1 -1
  116. package/dist/helpers/scope-authorization.d.ts +7 -0
  117. package/dist/helpers/scope-authorization.d.ts.map +1 -0
  118. package/dist/helpers/scope-authorization.js +19 -0
  119. package/dist/helpers/scope-authorization.js.map +1 -0
  120. package/dist/helpers/scope-commit-index.d.ts +12 -0
  121. package/dist/helpers/scope-commit-index.d.ts.map +1 -0
  122. package/dist/helpers/scope-commit-index.js +35 -0
  123. package/dist/helpers/scope-commit-index.js.map +1 -0
  124. package/dist/helpers/scope-strings.js.map +1 -1
  125. package/dist/hono/api-key-auth.d.ts +49 -0
  126. package/dist/hono/api-key-auth.d.ts.map +1 -0
  127. package/dist/hono/api-key-auth.js +108 -0
  128. package/dist/hono/api-key-auth.js.map +1 -0
  129. package/dist/hono/audit-redaction.d.ts +20 -0
  130. package/dist/hono/audit-redaction.d.ts.map +1 -0
  131. package/dist/hono/audit-redaction.js +85 -0
  132. package/dist/hono/audit-redaction.js.map +1 -0
  133. package/dist/hono/blobs.d.ts +75 -0
  134. package/dist/hono/blobs.d.ts.map +1 -0
  135. package/dist/hono/blobs.js +586 -0
  136. package/dist/hono/blobs.js.map +1 -0
  137. package/dist/hono/console/gateway.d.ts +42 -0
  138. package/dist/hono/console/gateway.d.ts.map +1 -0
  139. package/dist/hono/console/gateway.js +2158 -0
  140. package/dist/hono/console/gateway.js.map +1 -0
  141. package/dist/hono/console/live-auth.d.ts +7 -0
  142. package/dist/hono/console/live-auth.d.ts.map +1 -0
  143. package/dist/hono/console/live-auth.js +39 -0
  144. package/dist/hono/console/live-auth.js.map +1 -0
  145. package/dist/hono/console/route-descriptor.d.ts +6 -0
  146. package/dist/hono/console/route-descriptor.d.ts.map +1 -0
  147. package/dist/hono/console/route-descriptor.js +16 -0
  148. package/dist/hono/console/route-descriptor.js.map +1 -0
  149. package/dist/hono/console/routes/api-keys.d.ts +8 -0
  150. package/dist/hono/console/routes/api-keys.d.ts.map +1 -0
  151. package/dist/hono/console/routes/api-keys.js +575 -0
  152. package/dist/hono/console/routes/api-keys.js.map +1 -0
  153. package/dist/hono/console/routes/clients.d.ts +8 -0
  154. package/dist/hono/console/routes/clients.d.ts.map +1 -0
  155. package/dist/hono/console/routes/clients.js +322 -0
  156. package/dist/hono/console/routes/clients.js.map +1 -0
  157. package/dist/hono/console/routes/commits.d.ts +8 -0
  158. package/dist/hono/console/routes/commits.d.ts.map +1 -0
  159. package/dist/hono/console/routes/commits.js +882 -0
  160. package/dist/hono/console/routes/commits.js.map +1 -0
  161. package/dist/hono/console/routes/context.d.ts +188 -0
  162. package/dist/hono/console/routes/context.d.ts.map +1 -0
  163. package/dist/hono/console/routes/context.js +633 -0
  164. package/dist/hono/console/routes/context.js.map +1 -0
  165. package/dist/hono/console/routes/events.d.ts +8 -0
  166. package/dist/hono/console/routes/events.d.ts.map +1 -0
  167. package/dist/hono/console/routes/events.js +501 -0
  168. package/dist/hono/console/routes/events.js.map +1 -0
  169. package/dist/hono/console/routes/maintenance.d.ts +8 -0
  170. package/dist/hono/console/routes/maintenance.d.ts.map +1 -0
  171. package/dist/hono/console/routes/maintenance.js +341 -0
  172. package/dist/hono/console/routes/maintenance.js.map +1 -0
  173. package/dist/hono/console/routes/shared.d.ts +174 -0
  174. package/dist/hono/console/routes/shared.d.ts.map +1 -0
  175. package/dist/hono/console/routes/shared.js +583 -0
  176. package/dist/hono/console/routes/shared.js.map +1 -0
  177. package/dist/hono/console/routes/stats.d.ts +8 -0
  178. package/dist/hono/console/routes/stats.d.ts.map +1 -0
  179. package/dist/hono/console/routes/stats.js +295 -0
  180. package/dist/hono/console/routes/stats.js.map +1 -0
  181. package/dist/hono/console/routes/storage.d.ts +8 -0
  182. package/dist/hono/console/routes/storage.d.ts.map +1 -0
  183. package/dist/hono/console/routes/storage.js +121 -0
  184. package/dist/hono/console/routes/storage.js.map +1 -0
  185. package/dist/hono/console/routes.d.ts +36 -0
  186. package/dist/hono/console/routes.d.ts.map +1 -0
  187. package/dist/hono/console/routes.js +112 -0
  188. package/dist/hono/console/routes.js.map +1 -0
  189. package/dist/hono/console/schema-errors.d.ts +2 -0
  190. package/dist/hono/console/schema-errors.d.ts.map +1 -0
  191. package/dist/hono/console/schema-errors.js +17 -0
  192. package/dist/hono/console/schema-errors.js.map +1 -0
  193. package/dist/hono/console/schemas.d.ts +1515 -0
  194. package/dist/hono/console/schemas.d.ts.map +1 -0
  195. package/dist/hono/console/schemas.js +661 -0
  196. package/dist/hono/console/schemas.js.map +1 -0
  197. package/dist/hono/console/types.d.ts +213 -0
  198. package/dist/hono/console/types.d.ts.map +1 -0
  199. package/dist/hono/console/types.js +2 -0
  200. package/dist/hono/console/types.js.map +1 -0
  201. package/dist/hono/console/ui.d.ts +38 -0
  202. package/dist/hono/console/ui.d.ts.map +1 -0
  203. package/dist/hono/console/ui.js +43 -0
  204. package/dist/hono/console/ui.js.map +1 -0
  205. package/dist/hono/create-server.d.ts +71 -0
  206. package/dist/hono/create-server.d.ts.map +1 -0
  207. package/dist/hono/create-server.js +121 -0
  208. package/dist/hono/create-server.js.map +1 -0
  209. package/dist/hono/errors.d.ts +13 -0
  210. package/dist/hono/errors.d.ts.map +1 -0
  211. package/dist/hono/errors.js +21 -0
  212. package/dist/hono/errors.js.map +1 -0
  213. package/dist/hono/index.d.ts +20 -0
  214. package/dist/hono/index.d.ts.map +1 -0
  215. package/dist/hono/index.js +31 -0
  216. package/dist/hono/index.js.map +1 -0
  217. package/dist/hono/openapi.d.ts +72 -0
  218. package/dist/hono/openapi.d.ts.map +1 -0
  219. package/dist/hono/openapi.js +99 -0
  220. package/dist/hono/openapi.js.map +1 -0
  221. package/dist/hono/proxy/connection-manager.d.ts +78 -0
  222. package/dist/hono/proxy/connection-manager.d.ts.map +1 -0
  223. package/dist/hono/proxy/connection-manager.js +251 -0
  224. package/dist/hono/proxy/connection-manager.js.map +1 -0
  225. package/dist/hono/proxy/index.d.ts +8 -0
  226. package/dist/hono/proxy/index.d.ts.map +1 -0
  227. package/dist/hono/proxy/index.js +8 -0
  228. package/dist/hono/proxy/index.js.map +1 -0
  229. package/dist/hono/proxy/routes.d.ts +86 -0
  230. package/dist/hono/proxy/routes.d.ts.map +1 -0
  231. package/dist/hono/proxy/routes.js +183 -0
  232. package/dist/hono/proxy/routes.js.map +1 -0
  233. package/dist/hono/rate-limit.d.ts +101 -0
  234. package/dist/hono/rate-limit.d.ts.map +1 -0
  235. package/dist/hono/rate-limit.js +184 -0
  236. package/dist/hono/rate-limit.js.map +1 -0
  237. package/dist/hono/realtime-sync-packs.d.ts +43 -0
  238. package/dist/hono/realtime-sync-packs.d.ts.map +1 -0
  239. package/dist/hono/realtime-sync-packs.js +219 -0
  240. package/dist/hono/realtime-sync-packs.js.map +1 -0
  241. package/dist/hono/routes/audit.d.ts +12 -0
  242. package/dist/hono/routes/audit.d.ts.map +1 -0
  243. package/dist/hono/routes/audit.js +385 -0
  244. package/dist/hono/routes/audit.js.map +1 -0
  245. package/dist/hono/routes/auth-leases.d.ts +8 -0
  246. package/dist/hono/routes/auth-leases.d.ts.map +1 -0
  247. package/dist/hono/routes/auth-leases.js +88 -0
  248. package/dist/hono/routes/auth-leases.js.map +1 -0
  249. package/dist/hono/routes/combined.d.ts +8 -0
  250. package/dist/hono/routes/combined.d.ts.map +1 -0
  251. package/dist/hono/routes/combined.js +392 -0
  252. package/dist/hono/routes/combined.js.map +1 -0
  253. package/dist/hono/routes/context.d.ts +314 -0
  254. package/dist/hono/routes/context.d.ts.map +1 -0
  255. package/dist/hono/routes/context.js +1147 -0
  256. package/dist/hono/routes/context.js.map +1 -0
  257. package/dist/hono/routes/health.d.ts +8 -0
  258. package/dist/hono/routes/health.d.ts.map +1 -0
  259. package/dist/hono/routes/health.js +16 -0
  260. package/dist/hono/routes/health.js.map +1 -0
  261. package/dist/hono/routes/realtime.d.ts +9 -0
  262. package/dist/hono/routes/realtime.d.ts.map +1 -0
  263. package/dist/hono/routes/realtime.js +626 -0
  264. package/dist/hono/routes/realtime.js.map +1 -0
  265. package/dist/hono/routes/shared.d.ts +689 -0
  266. package/dist/hono/routes/shared.d.ts.map +1 -0
  267. package/dist/hono/routes/shared.js +972 -0
  268. package/dist/hono/routes/shared.js.map +1 -0
  269. package/dist/hono/routes/snapshots.d.ts +10 -0
  270. package/dist/hono/routes/snapshots.d.ts.map +1 -0
  271. package/dist/hono/routes/snapshots.js +281 -0
  272. package/dist/hono/routes/snapshots.js.map +1 -0
  273. package/dist/hono/routes.d.ts +19 -0
  274. package/dist/hono/routes.d.ts.map +1 -0
  275. package/dist/hono/routes.js +37 -0
  276. package/dist/hono/routes.js.map +1 -0
  277. package/dist/hono/validation.d.ts +5 -0
  278. package/dist/hono/validation.d.ts.map +1 -0
  279. package/dist/hono/validation.js +47 -0
  280. package/dist/hono/validation.js.map +1 -0
  281. package/dist/hono/websocket-origin.d.ts +9 -0
  282. package/dist/hono/websocket-origin.d.ts.map +1 -0
  283. package/dist/hono/websocket-origin.js +96 -0
  284. package/dist/hono/websocket-origin.js.map +1 -0
  285. package/dist/hono/ws.d.ts +341 -0
  286. package/dist/hono/ws.d.ts.map +1 -0
  287. package/dist/hono/ws.js +711 -0
  288. package/dist/hono/ws.js.map +1 -0
  289. package/dist/index.d.ts +6 -0
  290. package/dist/index.d.ts.map +1 -1
  291. package/dist/index.js +24 -18
  292. package/dist/index.js.map +1 -1
  293. package/dist/libsql.d.ts +29 -0
  294. package/dist/libsql.d.ts.map +1 -0
  295. package/dist/libsql.js +25 -0
  296. package/dist/libsql.js.map +1 -0
  297. package/dist/migrate.js.map +1 -1
  298. package/dist/neon.d.ts +21 -0
  299. package/dist/neon.d.ts.map +1 -0
  300. package/dist/neon.js +22 -0
  301. package/dist/neon.js.map +1 -0
  302. package/dist/notify.d.ts +30 -0
  303. package/dist/notify.d.ts.map +1 -1
  304. package/dist/notify.js +160 -41
  305. package/dist/notify.js.map +1 -1
  306. package/dist/pglite.d.ts +37 -0
  307. package/dist/pglite.d.ts.map +1 -0
  308. package/dist/pglite.js +37 -0
  309. package/dist/pglite.js.map +1 -0
  310. package/dist/plugins/index.d.ts +2 -0
  311. package/dist/plugins/index.d.ts.map +1 -0
  312. package/dist/plugins/index.js +2 -0
  313. package/dist/plugins/index.js.map +1 -0
  314. package/dist/plugins/types.d.ts +73 -0
  315. package/dist/plugins/types.d.ts.map +1 -0
  316. package/dist/plugins/types.js +30 -0
  317. package/dist/plugins/types.js.map +1 -0
  318. package/dist/postgres/index.d.ts +68 -0
  319. package/dist/postgres/index.d.ts.map +1 -0
  320. package/dist/postgres/index.js +924 -0
  321. package/dist/postgres/index.js.map +1 -0
  322. package/dist/proxy/collection.d.ts +0 -2
  323. package/dist/proxy/collection.d.ts.map +1 -1
  324. package/dist/proxy/collection.js +2 -17
  325. package/dist/proxy/collection.js.map +1 -1
  326. package/dist/proxy/handler.d.ts +1 -1
  327. package/dist/proxy/handler.d.ts.map +1 -1
  328. package/dist/proxy/handler.js +3 -4
  329. package/dist/proxy/handler.js.map +1 -1
  330. package/dist/proxy/index.d.ts +1 -1
  331. package/dist/proxy/index.d.ts.map +1 -1
  332. package/dist/proxy/index.js +3 -3
  333. package/dist/proxy/index.js.map +1 -1
  334. package/dist/proxy/mutation-detector.js.map +1 -1
  335. package/dist/proxy/oplog.d.ts.map +1 -1
  336. package/dist/proxy/oplog.js +32 -8
  337. package/dist/proxy/oplog.js.map +1 -1
  338. package/dist/prune.d.ts +8 -1
  339. package/dist/prune.d.ts.map +1 -1
  340. package/dist/prune.js +68 -28
  341. package/dist/prune.js.map +1 -1
  342. package/dist/pull.d.ts +31 -0
  343. package/dist/pull.d.ts.map +1 -1
  344. package/dist/pull.js +1014 -474
  345. package/dist/pull.js.map +1 -1
  346. package/dist/push.d.ts +36 -2
  347. package/dist/push.d.ts.map +1 -1
  348. package/dist/push.js +603 -358
  349. package/dist/push.js.map +1 -1
  350. package/dist/realtime/in-memory.js.map +1 -1
  351. package/dist/realtime/index.d.ts +2 -1
  352. package/dist/realtime/index.d.ts.map +1 -1
  353. package/dist/realtime/index.js +2 -1
  354. package/dist/realtime/index.js.map +1 -1
  355. package/dist/realtime/types.d.ts +12 -0
  356. package/dist/realtime/types.d.ts.map +1 -1
  357. package/dist/realtime/types.js +13 -1
  358. package/dist/realtime/types.js.map +1 -1
  359. package/dist/relay/client-role/forward-engine.d.ts +63 -0
  360. package/dist/relay/client-role/forward-engine.d.ts.map +1 -0
  361. package/dist/relay/client-role/forward-engine.js +267 -0
  362. package/dist/relay/client-role/forward-engine.js.map +1 -0
  363. package/dist/relay/client-role/index.d.ts +9 -0
  364. package/dist/relay/client-role/index.d.ts.map +1 -0
  365. package/dist/relay/client-role/index.js +9 -0
  366. package/dist/relay/client-role/index.js.map +1 -0
  367. package/dist/relay/client-role/pull-engine.d.ts +72 -0
  368. package/dist/relay/client-role/pull-engine.d.ts.map +1 -0
  369. package/dist/relay/client-role/pull-engine.js +249 -0
  370. package/dist/relay/client-role/pull-engine.js.map +1 -0
  371. package/dist/relay/client-role/sequence-mapper.d.ts +65 -0
  372. package/dist/relay/client-role/sequence-mapper.d.ts.map +1 -0
  373. package/dist/relay/client-role/sequence-mapper.js +161 -0
  374. package/dist/relay/client-role/sequence-mapper.js.map +1 -0
  375. package/dist/relay/evaluation/relay-paths.d.ts +41 -0
  376. package/dist/relay/evaluation/relay-paths.d.ts.map +1 -0
  377. package/dist/relay/evaluation/relay-paths.js +504 -0
  378. package/dist/relay/evaluation/relay-paths.js.map +1 -0
  379. package/dist/relay/evaluation/rust-boundary.d.ts +47 -0
  380. package/dist/relay/evaluation/rust-boundary.d.ts.map +1 -0
  381. package/dist/relay/evaluation/rust-boundary.js +220 -0
  382. package/dist/relay/evaluation/rust-boundary.js.map +1 -0
  383. package/dist/relay/index.d.ts +37 -0
  384. package/dist/relay/index.d.ts.map +1 -0
  385. package/dist/relay/index.js +44 -0
  386. package/dist/relay/index.js.map +1 -0
  387. package/dist/relay/migrate.d.ts +18 -0
  388. package/dist/relay/migrate.d.ts.map +1 -0
  389. package/dist/relay/migrate.js +99 -0
  390. package/dist/relay/migrate.js.map +1 -0
  391. package/dist/relay/mode-manager.d.ts +60 -0
  392. package/dist/relay/mode-manager.d.ts.map +1 -0
  393. package/dist/relay/mode-manager.js +114 -0
  394. package/dist/relay/mode-manager.js.map +1 -0
  395. package/dist/relay/realtime.d.ts +90 -0
  396. package/dist/relay/realtime.d.ts.map +1 -0
  397. package/dist/relay/realtime.js +147 -0
  398. package/dist/relay/realtime.js.map +1 -0
  399. package/dist/relay/relay.d.ts +190 -0
  400. package/dist/relay/relay.d.ts.map +1 -0
  401. package/dist/relay/relay.js +320 -0
  402. package/dist/relay/relay.js.map +1 -0
  403. package/dist/relay/schema.d.ts +158 -0
  404. package/dist/relay/schema.d.ts.map +1 -0
  405. package/dist/relay/schema.js +7 -0
  406. package/dist/relay/schema.js.map +1 -0
  407. package/dist/relay/server-role/index.d.ts +56 -0
  408. package/dist/relay/server-role/index.d.ts.map +1 -0
  409. package/dist/relay/server-role/index.js +193 -0
  410. package/dist/relay/server-role/index.js.map +1 -0
  411. package/dist/relay/server-role/pull.d.ts +27 -0
  412. package/dist/relay/server-role/pull.d.ts.map +1 -0
  413. package/dist/relay/server-role/pull.js +24 -0
  414. package/dist/relay/server-role/pull.js.map +1 -0
  415. package/dist/relay/server-role/push.d.ts +29 -0
  416. package/dist/relay/server-role/push.d.ts.map +1 -0
  417. package/dist/relay/server-role/push.js +122 -0
  418. package/dist/relay/server-role/push.js.map +1 -0
  419. package/dist/s3/index.d.ts +83 -0
  420. package/dist/s3/index.d.ts.map +1 -0
  421. package/dist/s3/index.js +223 -0
  422. package/dist/s3/index.js.map +1 -0
  423. package/dist/schema.d.ts +137 -1
  424. package/dist/schema.d.ts.map +1 -1
  425. package/dist/schema.js +1 -0
  426. package/dist/schema.js.map +1 -1
  427. package/dist/service-worker/index.d.ts +114 -0
  428. package/dist/service-worker/index.d.ts.map +1 -0
  429. package/dist/service-worker/index.js +487 -0
  430. package/dist/service-worker/index.js.map +1 -0
  431. package/dist/snapshot-artifacts/sqlite-bun.d.ts +15 -0
  432. package/dist/snapshot-artifacts/sqlite-bun.d.ts.map +1 -0
  433. package/dist/snapshot-artifacts/sqlite-bun.js +128 -0
  434. package/dist/snapshot-artifacts/sqlite-bun.js.map +1 -0
  435. package/dist/snapshot-artifacts.d.ts +206 -0
  436. package/dist/snapshot-artifacts.d.ts.map +1 -0
  437. package/dist/snapshot-artifacts.js +545 -0
  438. package/dist/snapshot-artifacts.js.map +1 -0
  439. package/dist/snapshot-chunks/db-metadata.d.ts +18 -0
  440. package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
  441. package/dist/snapshot-chunks/db-metadata.js +91 -63
  442. package/dist/snapshot-chunks/db-metadata.js.map +1 -1
  443. package/dist/snapshot-chunks/index.js +2 -2
  444. package/dist/snapshot-chunks/types.d.ts +2 -0
  445. package/dist/snapshot-chunks/types.d.ts.map +1 -1
  446. package/dist/snapshot-chunks.d.ts +33 -3
  447. package/dist/snapshot-chunks.d.ts.map +1 -1
  448. package/dist/snapshot-chunks.js +136 -55
  449. package/dist/snapshot-chunks.js.map +1 -1
  450. package/dist/sqlite/index.d.ts +53 -0
  451. package/dist/sqlite/index.d.ts.map +1 -0
  452. package/dist/sqlite/index.js +795 -0
  453. package/dist/sqlite/index.js.map +1 -0
  454. package/dist/sqlite3.d.ts +22 -0
  455. package/dist/sqlite3.d.ts.map +1 -0
  456. package/dist/sqlite3.js +99 -0
  457. package/dist/sqlite3.js.map +1 -0
  458. package/dist/stats.d.ts +8 -0
  459. package/dist/stats.d.ts.map +1 -1
  460. package/dist/stats.js +45 -13
  461. package/dist/stats.js.map +1 -1
  462. package/dist/subscriptions/cache.d.ts +3 -0
  463. package/dist/subscriptions/cache.d.ts.map +1 -1
  464. package/dist/subscriptions/cache.js +44 -0
  465. package/dist/subscriptions/cache.js.map +1 -1
  466. package/dist/subscriptions/index.js +2 -2
  467. package/dist/subscriptions/resolve.d.ts +3 -1
  468. package/dist/subscriptions/resolve.d.ts.map +1 -1
  469. package/dist/subscriptions/resolve.js +91 -52
  470. package/dist/subscriptions/resolve.js.map +1 -1
  471. package/dist/sync.d.ts +3 -0
  472. package/dist/sync.d.ts.map +1 -1
  473. package/dist/sync.js +9 -5
  474. package/dist/sync.js.map +1 -1
  475. package/package.json +297 -8
  476. package/src/auth-leases.ts +649 -0
  477. package/src/better-sqlite3.ts +35 -0
  478. package/src/blobs/access.ts +244 -0
  479. package/src/blobs/adapters/database.ts +125 -43
  480. package/src/blobs/index.ts +1 -0
  481. package/src/blobs/manager.ts +366 -65
  482. package/src/blobs/migrate.ts +16 -8
  483. package/src/blobs/types.ts +4 -0
  484. package/src/bun-sqlite-ambient.d.ts +19 -0
  485. package/src/bun-sqlite.ts +27 -0
  486. package/src/clients.ts +1 -0
  487. package/src/cloudflare/durable-object.ts +289 -0
  488. package/src/cloudflare/index.ts +22 -0
  489. package/src/cloudflare/r2.ts +526 -0
  490. package/src/cloudflare/scope-cache.ts +341 -0
  491. package/src/cloudflare/sentry.ts +230 -0
  492. package/src/cloudflare/worker.ts +77 -0
  493. package/src/commit-integrity.ts +371 -0
  494. package/src/crdt-yjs/index.ts +931 -0
  495. package/src/d1.ts +16 -0
  496. package/src/dialect/base.ts +71 -5
  497. package/src/dialect/helpers.ts +31 -0
  498. package/src/dialect/types.ts +47 -0
  499. package/src/encrypted-crdt.ts +786 -0
  500. package/src/errors.ts +22 -0
  501. package/src/filesystem/index.ts +262 -0
  502. package/src/handlers/collection.ts +78 -78
  503. package/src/handlers/create-handler.ts +540 -147
  504. package/src/handlers/types.ts +116 -0
  505. package/src/helpers/index.ts +2 -0
  506. package/src/helpers/scope-authorization.ts +27 -0
  507. package/src/helpers/scope-commit-index.ts +52 -0
  508. package/src/hono/api-key-auth.ts +177 -0
  509. package/src/hono/audit-redaction.ts +135 -0
  510. package/src/hono/blobs.ts +851 -0
  511. package/src/hono/console/gateway.ts +3046 -0
  512. package/src/hono/console/live-auth.ts +46 -0
  513. package/src/hono/console/route-descriptor.ts +22 -0
  514. package/src/hono/console/routes/api-keys.ts +721 -0
  515. package/src/hono/console/routes/clients.ts +447 -0
  516. package/src/hono/console/routes/commits.ts +1137 -0
  517. package/src/hono/console/routes/context.ts +956 -0
  518. package/src/hono/console/routes/events.ts +669 -0
  519. package/src/hono/console/routes/maintenance.ts +461 -0
  520. package/src/hono/console/routes/shared.ts +816 -0
  521. package/src/hono/console/routes/stats.ts +392 -0
  522. package/src/hono/console/routes/storage.ts +159 -0
  523. package/src/hono/console/routes.ts +146 -0
  524. package/src/hono/console/schema-errors.ts +23 -0
  525. package/src/hono/console/schemas.ts +914 -0
  526. package/src/hono/console/types.ts +223 -0
  527. package/src/hono/console/ui.ts +100 -0
  528. package/src/hono/create-server.ts +230 -0
  529. package/src/hono/errors.ts +50 -0
  530. package/src/hono/index.ts +54 -0
  531. package/src/hono/openapi.ts +139 -0
  532. package/src/hono/proxy/connection-manager.ts +340 -0
  533. package/src/hono/proxy/index.ts +8 -0
  534. package/src/hono/proxy/routes.ts +272 -0
  535. package/src/hono/rate-limit.ts +319 -0
  536. package/src/hono/realtime-sync-packs.ts +354 -0
  537. package/src/hono/routes/audit.ts +499 -0
  538. package/src/hono/routes/auth-leases.ts +119 -0
  539. package/src/hono/routes/combined.ts +583 -0
  540. package/src/hono/routes/context.ts +1629 -0
  541. package/src/hono/routes/health.ts +26 -0
  542. package/src/hono/routes/realtime.ts +808 -0
  543. package/src/hono/routes/shared.ts +1626 -0
  544. package/src/hono/routes/snapshots.ts +345 -0
  545. package/src/hono/routes.ts +68 -0
  546. package/src/hono/validation.ts +81 -0
  547. package/src/hono/websocket-origin.ts +131 -0
  548. package/src/hono/ws.ts +1134 -0
  549. package/src/index.ts +6 -0
  550. package/src/libsql.ts +51 -0
  551. package/src/neon.ts +28 -0
  552. package/src/notify.ts +224 -44
  553. package/src/pglite.ts +68 -0
  554. package/src/plugins/index.ts +1 -0
  555. package/src/plugins/types.ts +144 -0
  556. package/src/postgres/index.ts +1291 -0
  557. package/src/proxy/collection.ts +5 -27
  558. package/src/proxy/handler.ts +2 -2
  559. package/src/proxy/index.ts +0 -2
  560. package/src/proxy/oplog.ts +41 -10
  561. package/src/prune.ts +137 -36
  562. package/src/pull.ts +1504 -591
  563. package/src/push.ts +881 -443
  564. package/src/realtime/index.ts +2 -0
  565. package/src/realtime/types.ts +35 -0
  566. package/src/relay/bun-types.d.ts +50 -0
  567. package/src/relay/client-role/forward-engine.ts +355 -0
  568. package/src/relay/client-role/index.ts +9 -0
  569. package/src/relay/client-role/pull-engine.ts +329 -0
  570. package/src/relay/client-role/sequence-mapper.ts +201 -0
  571. package/src/relay/evaluation/relay-paths.ts +699 -0
  572. package/src/relay/evaluation/rust-boundary.ts +464 -0
  573. package/src/relay/index.ts +50 -0
  574. package/src/relay/migrate.ts +113 -0
  575. package/src/relay/mode-manager.ts +142 -0
  576. package/src/relay/realtime.ts +207 -0
  577. package/src/relay/relay.ts +431 -0
  578. package/src/relay/schema.ts +171 -0
  579. package/src/relay/server-role/index.ts +338 -0
  580. package/src/relay/server-role/pull.ts +43 -0
  581. package/src/relay/server-role/push.ts +164 -0
  582. package/src/s3/index.ts +346 -0
  583. package/src/schema.ts +141 -1
  584. package/src/service-worker/index.ts +773 -0
  585. package/src/snapshot-artifacts/sqlite-bun.ts +168 -0
  586. package/src/snapshot-artifacts.ts +896 -0
  587. package/src/snapshot-chunks/db-metadata.ts +131 -72
  588. package/src/snapshot-chunks/types.ts +2 -0
  589. package/src/snapshot-chunks.ts +194 -70
  590. package/src/sqlite/index.ts +1064 -0
  591. package/src/sqlite3.ts +137 -0
  592. package/src/stats.ts +76 -13
  593. package/src/subscriptions/cache.ts +58 -0
  594. package/src/subscriptions/resolve.ts +99 -58
  595. package/src/sync.ts +16 -6
  596. package/src/blobs/adapters/database.test.ts +0 -67
  597. package/src/notify.test.ts +0 -520
  598. package/src/proxy/handler.test.ts +0 -126
  599. package/src/proxy/mutation-detector.test.ts +0 -71
  600. package/src/snapshot-chunks/db-metadata.test.ts +0 -172
  601. package/src/subscriptions/resolve.test.ts +0 -180
package/src/push.ts CHANGED
@@ -7,24 +7,33 @@ import {
7
7
  type SyncPushResponse,
8
8
  startSyncSpan,
9
9
  } from '@syncular/core';
10
- import type {
11
- Insertable,
12
- Kysely,
13
- SelectQueryBuilder,
14
- SqlBool,
15
- Updateable,
16
- } from 'kysely';
10
+ import type { Insertable, Kysely, SelectQueryBuilder, SqlBool } from 'kysely';
17
11
  import { sql } from 'kysely';
18
- import type { ServerSyncDialect } from './dialect/types';
12
+ import { finalizeCommitIntegrity } from './commit-integrity';
19
13
  import {
20
- getServerHandlerOrThrow,
21
- type ServerHandlerCollection,
22
- } from './handlers/collection';
14
+ coerceNumber,
15
+ parseJsonValue,
16
+ toDialectJsonValue,
17
+ } from './dialect/helpers';
18
+ import type { DbExecutor, ServerSyncDialect } from './dialect/types';
19
+ import type { ServerHandlerCollection } from './handlers/collection';
23
20
  import type { SyncServerAuth } from './handlers/types';
21
+ import {
22
+ createScopeCommitIndexEntries,
23
+ scopeKeysFromScopeValues,
24
+ } from './helpers/scope-commit-index';
25
+ import {
26
+ type SyncServerPushPlugin,
27
+ sortServerPushPlugins,
28
+ } from './plugins/types';
24
29
  import type { SyncCoreDb } from './schema';
25
30
 
26
31
  // biome-ignore lint/complexity/noBannedTypes: Kysely uses `{}` as the initial "no selected columns yet" marker.
27
32
  type EmptySelection = {};
33
+ type SyncMetadataTrx = Pick<
34
+ Kysely<SyncCoreDb>,
35
+ 'selectFrom' | 'insertInto' | 'updateTable' | 'deleteFrom'
36
+ >;
28
37
 
29
38
  export interface PushCommitResult {
30
39
  response: SyncPushResponse;
@@ -44,8 +53,41 @@ export interface PushCommitResult {
44
53
  * Empty for rejected/cached commits.
45
54
  */
46
55
  emittedChanges: SyncChange[];
56
+ /**
57
+ * Commit actor metadata for downstream notifications.
58
+ * Null when no commit row was persisted.
59
+ */
60
+ commitActorId: string | null;
61
+ /**
62
+ * Commit timestamp metadata for downstream notifications.
63
+ * Null when no commit row was persisted.
64
+ */
65
+ commitCreatedAt: string | null;
66
+ }
67
+
68
+ export interface PushCommitValidationContext<
69
+ DB extends SyncCoreDb = SyncCoreDb,
70
+ Auth extends SyncServerAuth = SyncServerAuth,
71
+ > {
72
+ trx: DbExecutor<DB>;
73
+ dialect: ServerSyncDialect;
74
+ auth: Auth;
75
+ request: SyncPushRequest;
76
+ partitionId: string;
77
+ actorId: string;
78
+ commitSeq: number;
47
79
  }
48
80
 
81
+ export type PushCommitValidator<
82
+ DB extends SyncCoreDb = SyncCoreDb,
83
+ Auth extends SyncServerAuth = SyncServerAuth,
84
+ > = (
85
+ ctx: PushCommitValidationContext<DB, Auth>
86
+ ) =>
87
+ | Promise<SyncPushResponse['results'][number] | null>
88
+ | SyncPushResponse['results'][number]
89
+ | null;
90
+
49
91
  class RejectCommitError extends Error {
50
92
  constructor(public readonly response: SyncPushResponse) {
51
93
  super('REJECT_COMMIT');
@@ -53,40 +95,6 @@ class RejectCommitError extends Error {
53
95
  }
54
96
  }
55
97
 
56
- function toDialectJsonValue(
57
- dialect: ServerSyncDialect,
58
- value: unknown
59
- ): unknown {
60
- if (value === null || value === undefined) return null;
61
- if (dialect.family === 'sqlite') return JSON.stringify(value);
62
- return value;
63
- }
64
-
65
- function parseJsonValue(value: unknown): unknown {
66
- if (typeof value !== 'string') return value;
67
- try {
68
- return JSON.parse(value);
69
- } catch {
70
- return value;
71
- }
72
- }
73
-
74
- function coerceNumber(value: unknown): number | null {
75
- if (value === null || value === undefined) return null;
76
- if (typeof value === 'number') {
77
- return Number.isFinite(value) ? value : null;
78
- }
79
- if (typeof value === 'bigint') {
80
- const coerced = Number(value);
81
- return Number.isFinite(coerced) ? coerced : null;
82
- }
83
- if (typeof value === 'string') {
84
- const coerced = Number(value);
85
- return Number.isFinite(coerced) ? coerced : null;
86
- }
87
- return null;
88
- }
89
-
90
98
  function isRecord(value: unknown): value is Record<string, unknown> {
91
99
  return typeof value === 'object' && value !== null;
92
100
  }
@@ -97,8 +105,30 @@ function isSyncPushResponse(value: unknown): value is SyncPushResponse {
97
105
  );
98
106
  }
99
107
 
108
+ function assertOperationIdentityUnchanged(
109
+ pluginName: string,
110
+ before: SyncPushRequest['operations'][number],
111
+ after: SyncPushRequest['operations'][number]
112
+ ): void {
113
+ if (before.table !== after.table) {
114
+ throw new Error(
115
+ `Server push plugin "${pluginName}" cannot change op.table (${before.table} -> ${after.table})`
116
+ );
117
+ }
118
+ if (before.row_id !== after.row_id) {
119
+ throw new Error(
120
+ `Server push plugin "${pluginName}" cannot change op.row_id (${before.row_id} -> ${after.row_id})`
121
+ );
122
+ }
123
+ if (before.op !== after.op) {
124
+ throw new Error(
125
+ `Server push plugin "${pluginName}" cannot change op.op (${before.op} -> ${after.op})`
126
+ );
127
+ }
128
+ }
129
+
100
130
  async function readCommitAffectedTables<DB extends SyncCoreDb>(
101
- db: Kysely<DB>,
131
+ db: DbExecutor<DB>,
102
132
  dialect: ServerSyncDialect,
103
133
  commitSeq: number,
104
134
  partitionId: string
@@ -131,10 +161,8 @@ function scopeKeysFromEmitted(
131
161
  ): string[] {
132
162
  const keys = new Set<string>();
133
163
  for (const c of emitted) {
134
- for (const [key, value] of Object.entries(c.scopes)) {
135
- if (!value) continue;
136
- const prefix = key.replace(/_id$/, '');
137
- keys.add(`${prefix}:${value}`);
164
+ for (const scopeKey of scopeKeysFromScopeValues(c.scopes)) {
165
+ keys.add(scopeKey);
138
166
  }
139
167
  }
140
168
  return Array.from(keys);
@@ -181,6 +209,650 @@ function recordPushMetrics(args: {
181
209
  );
182
210
  }
183
211
 
212
+ function createRejectedPushResult(
213
+ error: SyncPushResponse['results'][number]
214
+ ): PushCommitResult {
215
+ return {
216
+ response: {
217
+ ok: true,
218
+ status: 'rejected',
219
+ results: [error],
220
+ },
221
+ affectedTables: [],
222
+ scopeKeys: [],
223
+ emittedChanges: [],
224
+ commitActorId: null,
225
+ commitCreatedAt: null,
226
+ };
227
+ }
228
+
229
+ function createRejectedPushResponse(
230
+ error: SyncPushResponse['results'][number],
231
+ commitSeq?: number
232
+ ): SyncPushResponse {
233
+ return {
234
+ ok: true,
235
+ status: 'rejected',
236
+ ...(commitSeq !== undefined ? { commitSeq } : {}),
237
+ results: [error],
238
+ };
239
+ }
240
+
241
+ function validatePushRequest(
242
+ request: SyncPushRequest
243
+ ): SyncPushResponse['results'][number] | null {
244
+ if (!request.clientId || !request.clientCommitId) {
245
+ return {
246
+ opIndex: 0,
247
+ status: 'error',
248
+ error: 'Invalid push request',
249
+ code: 'sync.invalid_request',
250
+ retriable: false,
251
+ };
252
+ }
253
+
254
+ const ops = request.operations ?? [];
255
+ if (!Array.isArray(ops) || ops.length === 0) {
256
+ return {
257
+ opIndex: 0,
258
+ status: 'error',
259
+ error: 'Empty commit',
260
+ code: 'sync.empty_commit',
261
+ retriable: false,
262
+ };
263
+ }
264
+
265
+ return null;
266
+ }
267
+
268
+ function shouldUseSavepoints<
269
+ DB extends SyncCoreDb,
270
+ Auth extends SyncServerAuth,
271
+ >(args: {
272
+ dialect: ServerSyncDialect;
273
+ handlers: ServerHandlerCollection<DB, Auth>;
274
+ operations: SyncPushRequest['operations'];
275
+ }): boolean {
276
+ if (!args.dialect.supportsSavepoints) {
277
+ return false;
278
+ }
279
+
280
+ if ((args.operations?.length ?? 0) !== 1) {
281
+ return true;
282
+ }
283
+
284
+ const singleOp = args.operations?.[0];
285
+ if (!singleOp) {
286
+ return true;
287
+ }
288
+
289
+ const singleOpHandler = args.handlers.byTable.get(singleOp.table);
290
+ if (!singleOpHandler) {
291
+ throw new Error(`Unknown table: ${singleOp.table}`);
292
+ }
293
+
294
+ return !singleOpHandler.canRejectSingleOperationWithoutSavepoint;
295
+ }
296
+
297
+ async function persistEmittedChanges<DB extends SyncCoreDb>(args: {
298
+ trx: DbExecutor<DB>;
299
+ dialect: ServerSyncDialect;
300
+ partitionId: string;
301
+ commitSeq: number;
302
+ emittedChanges: PushCommitResult['emittedChanges'];
303
+ }): Promise<void> {
304
+ if (args.emittedChanges.length === 0) {
305
+ return;
306
+ }
307
+
308
+ const syncTrx = args.trx as Pick<Kysely<SyncCoreDb>, 'insertInto'>;
309
+ const changeRows: Array<Insertable<SyncCoreDb['sync_changes']>> =
310
+ args.emittedChanges.map((change) => ({
311
+ partition_id: args.partitionId,
312
+ commit_seq: args.commitSeq,
313
+ table: change.table,
314
+ row_id: change.row_id,
315
+ op: change.op,
316
+ row_json: toDialectJsonValue(args.dialect, change.row_json),
317
+ row_version: change.row_version,
318
+ scopes: args.dialect.scopesToDb(change.scopes),
319
+ }));
320
+
321
+ await syncTrx.insertInto('sync_changes').values(changeRows).execute();
322
+
323
+ const scopeEntries = createScopeCommitIndexEntries(args.emittedChanges);
324
+ if (scopeEntries.length > 0) {
325
+ await syncTrx
326
+ .insertInto('sync_scope_commits')
327
+ .values(
328
+ scopeEntries.map((entry) => ({
329
+ partition_id: args.partitionId,
330
+ table: entry.table,
331
+ scope_key: entry.scopeKey,
332
+ commit_seq: args.commitSeq,
333
+ }))
334
+ )
335
+ .onConflict((oc) =>
336
+ oc
337
+ .columns(['partition_id', 'table', 'scope_key', 'commit_seq'])
338
+ .doNothing()
339
+ )
340
+ .execute();
341
+ }
342
+ }
343
+
344
+ async function persistCommitOutcome<DB extends SyncCoreDb>(args: {
345
+ trx: DbExecutor<DB>;
346
+ dialect: ServerSyncDialect;
347
+ partitionId: string;
348
+ commitSeq: number;
349
+ response: SyncPushResponse;
350
+ affectedTables: string[];
351
+ emittedChangeCount: number;
352
+ }): Promise<void> {
353
+ const syncTrx = args.trx as Pick<
354
+ Kysely<SyncCoreDb>,
355
+ 'insertInto' | 'updateTable'
356
+ >;
357
+
358
+ await syncTrx
359
+ .updateTable('sync_commits')
360
+ .set({
361
+ result_json: toDialectJsonValue(args.dialect, args.response),
362
+ change_count: args.emittedChangeCount,
363
+ affected_tables: args.dialect.arrayToDb(args.affectedTables) as string[],
364
+ })
365
+ .where('commit_seq', '=', args.commitSeq)
366
+ .execute();
367
+
368
+ if (args.affectedTables.length > 0) {
369
+ await syncTrx
370
+ .insertInto('sync_table_commits')
371
+ .values(
372
+ args.affectedTables.map((table) => ({
373
+ partition_id: args.partitionId,
374
+ table,
375
+ commit_seq: args.commitSeq,
376
+ }))
377
+ )
378
+ .onConflict((oc) =>
379
+ oc.columns(['partition_id', 'table', 'commit_seq']).doNothing()
380
+ )
381
+ .execute();
382
+ }
383
+
384
+ await finalizeCommitIntegrity({
385
+ db: args.trx,
386
+ dialect: args.dialect,
387
+ partitionId: args.partitionId,
388
+ commitSeq: args.commitSeq,
389
+ });
390
+ }
391
+
392
+ async function loadExistingCommitResult<DB extends SyncCoreDb>(args: {
393
+ trx: DbExecutor<DB>;
394
+ syncTrx: SyncMetadataTrx;
395
+ dialect: ServerSyncDialect;
396
+ request: SyncPushRequest;
397
+ partitionId: string;
398
+ }): Promise<PushCommitResult> {
399
+ let query = (
400
+ args.syncTrx.selectFrom('sync_commits') as SelectQueryBuilder<
401
+ SyncCoreDb,
402
+ 'sync_commits',
403
+ EmptySelection
404
+ >
405
+ )
406
+ .selectAll()
407
+ .where('partition_id', '=', args.partitionId)
408
+ .where('client_id', '=', args.request.clientId)
409
+ .where('client_commit_id', '=', args.request.clientCommitId);
410
+
411
+ if (args.dialect.supportsForUpdate) {
412
+ query = query.forUpdate();
413
+ }
414
+
415
+ const existing = await query.executeTakeFirstOrThrow();
416
+ const parsedCached = parseJsonValue(existing.result_json);
417
+
418
+ if (!isSyncPushResponse(parsedCached)) {
419
+ return createRejectedPushResult({
420
+ opIndex: 0,
421
+ status: 'error',
422
+ error: 'Idempotency cache miss',
423
+ code: 'sync.idempotency_cache_miss',
424
+ retriable: true,
425
+ });
426
+ }
427
+
428
+ const base: SyncPushResponse = {
429
+ ...parsedCached,
430
+ commitSeq: Number(existing.commit_seq),
431
+ };
432
+
433
+ if (parsedCached.status === 'applied') {
434
+ const tablesFromDb = args.dialect.dbToArray(existing.affected_tables);
435
+ return {
436
+ response: { ...base, status: 'cached' },
437
+ affectedTables:
438
+ tablesFromDb.length > 0
439
+ ? tablesFromDb
440
+ : await readCommitAffectedTables(
441
+ args.trx,
442
+ args.dialect,
443
+ Number(existing.commit_seq),
444
+ args.partitionId
445
+ ),
446
+ scopeKeys: [],
447
+ emittedChanges: [],
448
+ commitActorId:
449
+ typeof existing.actor_id === 'string' ? existing.actor_id : null,
450
+ commitCreatedAt:
451
+ typeof existing.created_at === 'string' ? existing.created_at : null,
452
+ };
453
+ }
454
+
455
+ return {
456
+ response: base,
457
+ affectedTables: [],
458
+ scopeKeys: [],
459
+ emittedChanges: [],
460
+ commitActorId:
461
+ typeof existing.actor_id === 'string' ? existing.actor_id : null,
462
+ commitCreatedAt:
463
+ typeof existing.created_at === 'string' ? existing.created_at : null,
464
+ };
465
+ }
466
+
467
+ async function insertPendingCommit(args: {
468
+ syncTrx: SyncMetadataTrx;
469
+ dialect: ServerSyncDialect;
470
+ commitRow: Insertable<SyncCoreDb['sync_commits']>;
471
+ }): Promise<number | null> {
472
+ if (args.dialect.supportsInsertReturning) {
473
+ const insertedCommit = await args.syncTrx
474
+ .insertInto('sync_commits')
475
+ .values(args.commitRow)
476
+ .onConflict((oc) =>
477
+ oc
478
+ .columns(['partition_id', 'client_id', 'client_commit_id'])
479
+ .doNothing()
480
+ )
481
+ .returning(['commit_seq'])
482
+ .executeTakeFirst();
483
+
484
+ if (!insertedCommit) {
485
+ return null;
486
+ }
487
+
488
+ return coerceNumber(insertedCommit.commit_seq) ?? 0;
489
+ }
490
+
491
+ const insertResult = await args.syncTrx
492
+ .insertInto('sync_commits')
493
+ .values(args.commitRow)
494
+ .onConflict((oc) =>
495
+ oc.columns(['partition_id', 'client_id', 'client_commit_id']).doNothing()
496
+ )
497
+ .executeTakeFirstOrThrow();
498
+
499
+ const insertedRows = Number(insertResult.numInsertedOrUpdatedRows ?? 0);
500
+ if (insertedRows === 0) {
501
+ return null;
502
+ }
503
+
504
+ return coerceNumber(insertResult.insertId) ?? 0;
505
+ }
506
+
507
+ async function applyCommitOperations<
508
+ DB extends SyncCoreDb,
509
+ Auth extends SyncServerAuth,
510
+ >(args: {
511
+ trx: DbExecutor<DB>;
512
+ handlers: ServerHandlerCollection<DB, Auth>;
513
+ pushPlugins: readonly SyncServerPushPlugin<DB, Auth>[];
514
+ auth: Auth;
515
+ request: SyncPushRequest;
516
+ actorId: string;
517
+ commitId: string;
518
+ commitSeq: number;
519
+ }): Promise<{
520
+ results: SyncPushResponse['results'];
521
+ emittedChanges: PushCommitResult['emittedChanges'];
522
+ affectedTables: string[];
523
+ }> {
524
+ const ops = args.request.operations ?? [];
525
+ const allEmitted: PushCommitResult['emittedChanges'] = [];
526
+ const results: SyncPushResponse['results'] = [];
527
+ const affectedTablesSet = new Set<string>();
528
+
529
+ for (let i = 0; i < ops.length; ) {
530
+ const op = ops[i]!;
531
+ const handler = args.handlers.byTable.get(op.table);
532
+ if (!handler) {
533
+ throw new Error(`Unknown table: ${op.table}`);
534
+ }
535
+
536
+ const operationCtx = {
537
+ db: args.trx,
538
+ trx: args.trx,
539
+ actorId: args.actorId,
540
+ auth: args.auth,
541
+ clientId: args.request.clientId,
542
+ commitId: args.commitId,
543
+ schemaVersion: args.request.schemaVersion,
544
+ authLease: args.request.authLease,
545
+ };
546
+
547
+ let transformedOp = op;
548
+ for (const plugin of args.pushPlugins) {
549
+ if (!plugin.beforeApplyOperation) continue;
550
+ const nextOp = await plugin.beforeApplyOperation({
551
+ ctx: operationCtx,
552
+ tableHandler: handler,
553
+ op: transformedOp,
554
+ opIndex: i,
555
+ });
556
+ assertOperationIdentityUnchanged(plugin.name, op, nextOp);
557
+ transformedOp = nextOp;
558
+ }
559
+
560
+ let appliedBatch:
561
+ | Awaited<ReturnType<typeof handler.applyOperation>>[]
562
+ | null = null;
563
+ let consumed = 1;
564
+
565
+ if (args.pushPlugins.length === 0 && handler.applyOperationBatch) {
566
+ const batchInput = [];
567
+ for (let j = i; j < ops.length; j++) {
568
+ const nextOp = ops[j]!;
569
+ if (nextOp.table !== op.table) break;
570
+ batchInput.push({ op: nextOp, opIndex: j });
571
+ }
572
+
573
+ if (batchInput.length > 1) {
574
+ appliedBatch = await handler.applyOperationBatch(
575
+ operationCtx,
576
+ batchInput
577
+ );
578
+ consumed = Math.max(1, appliedBatch.length);
579
+ }
580
+ }
581
+
582
+ if (!appliedBatch) {
583
+ let appliedSingle = await handler.applyOperation(
584
+ operationCtx,
585
+ transformedOp,
586
+ i
587
+ );
588
+
589
+ for (const plugin of args.pushPlugins) {
590
+ if (!plugin.afterApplyOperation) continue;
591
+ appliedSingle = await plugin.afterApplyOperation({
592
+ ctx: operationCtx,
593
+ tableHandler: handler,
594
+ op: transformedOp,
595
+ opIndex: i,
596
+ applied: appliedSingle,
597
+ });
598
+ }
599
+
600
+ appliedBatch = [appliedSingle];
601
+ }
602
+
603
+ if (appliedBatch.length === 0) {
604
+ throw new Error(
605
+ `Handler "${op.table}" returned no results from applyOperationBatch`
606
+ );
607
+ }
608
+
609
+ for (const applied of appliedBatch) {
610
+ if (applied.result.status !== 'applied') {
611
+ results.push(applied.result);
612
+ throw new RejectCommitError(
613
+ createRejectedPushResponse(applied.result, args.commitSeq)
614
+ );
615
+ }
616
+
617
+ for (const change of applied.emittedChanges ?? []) {
618
+ const scopes = change?.scopes;
619
+ if (!scopes || typeof scopes !== 'object') {
620
+ const error: SyncPushResponse['results'][number] = {
621
+ opIndex: applied.result.opIndex,
622
+ status: 'error',
623
+ error: 'Missing scopes',
624
+ code: 'sync.missing_scopes',
625
+ retriable: false,
626
+ };
627
+ results.push(error);
628
+ throw new RejectCommitError(
629
+ createRejectedPushResponse(error, args.commitSeq)
630
+ );
631
+ }
632
+ }
633
+
634
+ results.push(applied.result);
635
+ allEmitted.push(...applied.emittedChanges);
636
+ for (const change of applied.emittedChanges) {
637
+ affectedTablesSet.add(change.table);
638
+ }
639
+ }
640
+
641
+ i += consumed;
642
+ }
643
+
644
+ return {
645
+ results,
646
+ emittedChanges: allEmitted,
647
+ affectedTables: Array.from(affectedTablesSet).sort(),
648
+ };
649
+ }
650
+
651
+ async function executePushCommitInExecutor<
652
+ DB extends SyncCoreDb,
653
+ Auth extends SyncServerAuth,
654
+ >(args: {
655
+ trx: DbExecutor<DB>;
656
+ dialect: ServerSyncDialect;
657
+ handlers: ServerHandlerCollection<DB, Auth>;
658
+ pushPlugins: readonly SyncServerPushPlugin<DB, Auth>[];
659
+ auth: Auth;
660
+ request: SyncPushRequest;
661
+ validateCommit?: PushCommitValidator<DB, Auth>;
662
+ }): Promise<PushCommitResult> {
663
+ const { trx, dialect, handlers, request, pushPlugins } = args;
664
+ const actorId = args.auth.actorId;
665
+ const partitionId = args.auth.partitionId ?? 'default';
666
+ const ops = request.operations ?? [];
667
+ const syncTrx = trx as SyncMetadataTrx;
668
+
669
+ if (!dialect.supportsSavepoints) {
670
+ await syncTrx
671
+ .deleteFrom('sync_commits')
672
+ .where('partition_id', '=', partitionId)
673
+ .where('client_id', '=', request.clientId)
674
+ .where('client_commit_id', '=', request.clientCommitId)
675
+ .where('result_json', 'is', null)
676
+ .execute();
677
+ }
678
+
679
+ const commitCreatedAt = new Date().toISOString();
680
+ const commitRow: Insertable<SyncCoreDb['sync_commits']> = {
681
+ partition_id: partitionId,
682
+ actor_id: actorId,
683
+ client_id: request.clientId,
684
+ client_commit_id: request.clientCommitId,
685
+ created_at: commitCreatedAt,
686
+ meta: request.authLease
687
+ ? toDialectJsonValue(dialect, {
688
+ authLease: request.authLease,
689
+ })
690
+ : null,
691
+ result_json: null,
692
+ };
693
+ let commitSeq =
694
+ (await insertPendingCommit({
695
+ syncTrx,
696
+ dialect,
697
+ commitRow,
698
+ })) ?? 0;
699
+ if (commitSeq === 0) {
700
+ return loadExistingCommitResult({
701
+ trx,
702
+ syncTrx,
703
+ dialect,
704
+ request,
705
+ partitionId,
706
+ });
707
+ }
708
+
709
+ if (commitSeq <= 0) {
710
+ const insertedCommitRow = await (
711
+ syncTrx.selectFrom('sync_commits') as SelectQueryBuilder<
712
+ SyncCoreDb,
713
+ 'sync_commits',
714
+ EmptySelection
715
+ >
716
+ )
717
+ .selectAll()
718
+ .where('partition_id', '=', partitionId)
719
+ .where('client_id', '=', request.clientId)
720
+ .where('client_commit_id', '=', request.clientCommitId)
721
+ .executeTakeFirstOrThrow();
722
+ commitSeq = Number(insertedCommitRow.commit_seq);
723
+ }
724
+
725
+ const commitId = `${request.clientId}:${request.clientCommitId}`;
726
+ const validationResult = await args.validateCommit?.({
727
+ trx,
728
+ dialect,
729
+ auth: args.auth,
730
+ request,
731
+ partitionId,
732
+ actorId,
733
+ commitSeq,
734
+ });
735
+ if (validationResult) {
736
+ const response = createRejectedPushResponse(validationResult, commitSeq);
737
+ await persistCommitOutcome({
738
+ trx,
739
+ dialect,
740
+ partitionId,
741
+ commitSeq,
742
+ response,
743
+ affectedTables: [],
744
+ emittedChangeCount: 0,
745
+ });
746
+
747
+ return {
748
+ response,
749
+ affectedTables: [],
750
+ scopeKeys: [],
751
+ emittedChanges: [],
752
+ commitActorId: actorId,
753
+ commitCreatedAt,
754
+ };
755
+ }
756
+
757
+ const savepointName = `sync_apply_${commitSeq}`;
758
+ const useSavepoints = shouldUseSavepoints({
759
+ dialect,
760
+ handlers,
761
+ operations: ops,
762
+ });
763
+ let savepointCreated = false;
764
+
765
+ try {
766
+ if (useSavepoints) {
767
+ await sql.raw(`SAVEPOINT ${savepointName}`).execute(trx);
768
+ savepointCreated = true;
769
+ }
770
+
771
+ const applied = await applyCommitOperations({
772
+ trx,
773
+ handlers,
774
+ pushPlugins,
775
+ auth: args.auth,
776
+ request,
777
+ actorId,
778
+ commitId,
779
+ commitSeq,
780
+ });
781
+
782
+ const appliedResponse: SyncPushResponse = {
783
+ ok: true,
784
+ status: 'applied',
785
+ commitSeq,
786
+ results: applied.results,
787
+ };
788
+ await persistEmittedChanges({
789
+ trx,
790
+ dialect,
791
+ partitionId,
792
+ commitSeq,
793
+ emittedChanges: applied.emittedChanges,
794
+ });
795
+ await persistCommitOutcome({
796
+ trx,
797
+ dialect,
798
+ partitionId,
799
+ commitSeq,
800
+ response: appliedResponse,
801
+ affectedTables: applied.affectedTables,
802
+ emittedChangeCount: applied.emittedChanges.length,
803
+ });
804
+
805
+ if (useSavepoints) {
806
+ await sql.raw(`RELEASE SAVEPOINT ${savepointName}`).execute(trx);
807
+ }
808
+
809
+ return {
810
+ response: appliedResponse,
811
+ affectedTables: applied.affectedTables,
812
+ scopeKeys: scopeKeysFromEmitted(applied.emittedChanges),
813
+ emittedChanges: applied.emittedChanges,
814
+ commitActorId: actorId,
815
+ commitCreatedAt,
816
+ };
817
+ } catch (error) {
818
+ if (savepointCreated) {
819
+ try {
820
+ await sql.raw(`ROLLBACK TO SAVEPOINT ${savepointName}`).execute(trx);
821
+ await sql.raw(`RELEASE SAVEPOINT ${savepointName}`).execute(trx);
822
+ } catch (savepointError) {
823
+ console.error(
824
+ '[pushCommit] Savepoint rollback failed:',
825
+ savepointError
826
+ );
827
+ throw savepointError;
828
+ }
829
+ }
830
+
831
+ if (!(error instanceof RejectCommitError)) {
832
+ throw error;
833
+ }
834
+
835
+ await persistCommitOutcome({
836
+ trx,
837
+ dialect,
838
+ partitionId,
839
+ commitSeq,
840
+ response: error.response,
841
+ affectedTables: [],
842
+ emittedChangeCount: 0,
843
+ });
844
+
845
+ return {
846
+ response: error.response,
847
+ affectedTables: [],
848
+ scopeKeys: [],
849
+ emittedChanges: [],
850
+ commitActorId: actorId,
851
+ commitCreatedAt,
852
+ };
853
+ }
854
+ }
855
+
184
856
  export async function pushCommit<
185
857
  DB extends SyncCoreDb,
186
858
  Auth extends SyncServerAuth,
@@ -188,17 +860,20 @@ export async function pushCommit<
188
860
  db: Kysely<DB>;
189
861
  dialect: ServerSyncDialect;
190
862
  handlers: ServerHandlerCollection<DB, Auth>;
863
+ plugins?: readonly SyncServerPushPlugin<DB, Auth>[];
191
864
  auth: Auth;
192
865
  request: SyncPushRequest;
866
+ validateCommit?: PushCommitValidator<DB, Auth>;
867
+ suppressTelemetry?: boolean;
193
868
  }): Promise<PushCommitResult> {
194
869
  const { db, dialect, handlers, request } = args;
195
- const actorId = args.auth.actorId;
196
- const partitionId = args.auth.partitionId ?? 'default';
870
+ const pushPlugins = sortServerPushPlugins(args.plugins);
197
871
  const requestedOps = Array.isArray(request.operations)
198
872
  ? request.operations
199
873
  : [];
200
874
  const operationCount = requestedOps.length;
201
875
  const startedAtMs = Date.now();
876
+ const suppressTelemetry = args.suppressTelemetry === true;
202
877
 
203
878
  return startSyncSpan(
204
879
  {
@@ -219,418 +894,181 @@ export async function pushCommit<
219
894
  span.setAttribute('affected_table_count', result.affectedTables.length);
220
895
  span.setStatus('ok');
221
896
 
222
- recordPushMetrics({
223
- status,
224
- durationMs,
225
- operationCount,
226
- emittedChangeCount: result.emittedChanges.length,
227
- affectedTableCount: result.affectedTables.length,
228
- });
897
+ if (!suppressTelemetry) {
898
+ recordPushMetrics({
899
+ status,
900
+ durationMs,
901
+ operationCount,
902
+ emittedChangeCount: result.emittedChanges.length,
903
+ affectedTableCount: result.affectedTables.length,
904
+ });
905
+ }
229
906
 
230
907
  return result;
231
908
  };
232
909
 
233
910
  try {
234
- if (!request.clientId || !request.clientCommitId) {
235
- return finalizeResult({
236
- response: {
237
- ok: true,
238
- status: 'rejected',
239
- results: [
240
- {
241
- opIndex: 0,
242
- status: 'error',
243
- error: 'INVALID_REQUEST',
244
- code: 'INVALID_REQUEST',
245
- retriable: false,
246
- },
247
- ],
248
- },
249
- affectedTables: [],
250
- scopeKeys: [],
251
- emittedChanges: [],
252
- });
911
+ const validationError = validatePushRequest(request);
912
+ if (validationError) {
913
+ return finalizeResult(createRejectedPushResult(validationError));
253
914
  }
254
915
 
255
- const ops = request.operations ?? [];
256
- if (!Array.isArray(ops) || ops.length === 0) {
257
- return finalizeResult({
258
- response: {
259
- ok: true,
260
- status: 'rejected',
261
- results: [
262
- {
263
- opIndex: 0,
264
- status: 'error',
265
- error: 'EMPTY_COMMIT',
266
- code: 'EMPTY_COMMIT',
267
- retriable: false,
268
- },
269
- ],
270
- },
271
- affectedTables: [],
272
- scopeKeys: [],
273
- emittedChanges: [],
916
+ return finalizeResult(
917
+ await dialect.executeInTransaction(db, async (trx) =>
918
+ executePushCommitInExecutor({
919
+ trx,
920
+ dialect,
921
+ handlers,
922
+ pushPlugins,
923
+ auth: args.auth,
924
+ request,
925
+ validateCommit: args.validateCommit,
926
+ })
927
+ )
928
+ );
929
+ } catch (error) {
930
+ const durationMs = Math.max(0, Date.now() - startedAtMs);
931
+ span.setAttribute('status', 'error');
932
+ span.setAttribute('duration_ms', durationMs);
933
+ span.setStatus('error');
934
+
935
+ if (!suppressTelemetry) {
936
+ recordPushMetrics({
937
+ status: 'error',
938
+ durationMs,
939
+ operationCount,
940
+ emittedChangeCount: 0,
941
+ affectedTableCount: 0,
942
+ });
943
+ captureSyncException(error, {
944
+ event: 'sync.server.push',
945
+ operationCount,
274
946
  });
275
947
  }
948
+ throw error;
949
+ }
950
+ }
951
+ );
952
+ }
276
953
 
277
- return finalizeResult(
278
- await dialect.executeInTransaction(db, async (trx) => {
279
- type SyncTrx = Pick<
280
- Kysely<SyncCoreDb>,
281
- 'selectFrom' | 'insertInto' | 'updateTable' | 'deleteFrom'
282
- >;
283
-
284
- const syncTrx = trx as SyncTrx;
285
-
286
- // Clean up any stale commit row with null result_json.
287
- // This can happen when a previous push inserted the commit row but crashed
288
- // before writing the result (e.g., on D1 without transaction support).
289
- if (!dialect.supportsSavepoints) {
290
- await syncTrx
291
- .deleteFrom('sync_commits')
292
- .where('partition_id', '=', partitionId)
293
- .where('client_id', '=', request.clientId)
294
- .where('client_commit_id', '=', request.clientCommitId)
295
- .where('result_json', 'is', null)
296
- .execute();
297
- }
954
+ export async function pushCommitBatch<
955
+ DB extends SyncCoreDb,
956
+ Auth extends SyncServerAuth,
957
+ >(args: {
958
+ db: Kysely<DB>;
959
+ dialect: ServerSyncDialect;
960
+ handlers: ServerHandlerCollection<DB, Auth>;
961
+ plugins?: readonly SyncServerPushPlugin<DB, Auth>[];
962
+ auth: Auth;
963
+ requests: SyncPushRequest[];
964
+ validateCommit?: PushCommitValidator<DB, Auth>;
965
+ suppressTelemetry?: boolean;
966
+ }): Promise<PushCommitResult[]> {
967
+ const { db, dialect, handlers, requests } = args;
968
+ const pushPlugins = sortServerPushPlugins(args.plugins);
969
+ const startedAtMs = Date.now();
970
+ const suppressTelemetry = args.suppressTelemetry === true;
971
+ const totalOperationCount = requests.reduce((count, request) => {
972
+ const operations = Array.isArray(request.operations)
973
+ ? request.operations
974
+ : [];
975
+ return count + operations.length;
976
+ }, 0);
298
977
 
299
- // Insert commit row (idempotency key)
300
- const commitRow: Insertable<SyncCoreDb['sync_commits']> = {
301
- partition_id: partitionId,
302
- actor_id: actorId,
303
- client_id: request.clientId,
304
- client_commit_id: request.clientCommitId,
305
- meta: null,
306
- result_json: null,
307
- };
308
-
309
- const insertResult = await syncTrx
310
- .insertInto('sync_commits')
311
- .values(commitRow)
312
- .onConflict((oc) =>
313
- oc
314
- .columns(['partition_id', 'client_id', 'client_commit_id'])
315
- .doNothing()
316
- )
317
- .executeTakeFirstOrThrow();
978
+ return startSyncSpan(
979
+ {
980
+ name: 'sync.server.push_batch',
981
+ op: 'sync.push.batch',
982
+ attributes: {
983
+ commit_count: requests.length,
984
+ operation_count: totalOperationCount,
985
+ },
986
+ },
987
+ async (span) => {
988
+ try {
989
+ const results = await dialect.executeInTransaction(db, async (trx) => {
990
+ const executed: PushCommitResult[] = [];
991
+ for (const request of requests) {
992
+ const validationError = validatePushRequest(request);
993
+ if (validationError) {
994
+ executed.push(createRejectedPushResult(validationError));
995
+ continue;
996
+ }
318
997
 
319
- const insertedRows = Number(
320
- insertResult.numInsertedOrUpdatedRows ?? 0
998
+ executed.push(
999
+ await executePushCommitInExecutor({
1000
+ trx,
1001
+ dialect,
1002
+ handlers,
1003
+ pushPlugins,
1004
+ auth: args.auth,
1005
+ request,
1006
+ validateCommit: args.validateCommit,
1007
+ })
321
1008
  );
322
- if (insertedRows === 0) {
323
- // Existing commit: return cached response (applied or rejected)
324
- // Use forUpdate() for row locking on databases that support it
325
- let query = (
326
- syncTrx.selectFrom('sync_commits') as SelectQueryBuilder<
327
- SyncCoreDb,
328
- 'sync_commits',
329
- EmptySelection
330
- >
331
- )
332
- .selectAll()
333
- .where('partition_id', '=', partitionId)
334
- .where('client_id', '=', request.clientId)
335
- .where('client_commit_id', '=', request.clientCommitId);
336
-
337
- if (dialect.supportsForUpdate) {
338
- query = query.forUpdate();
339
- }
340
-
341
- const existing = await query.executeTakeFirstOrThrow();
342
-
343
- const parsedCached = parseJsonValue(existing.result_json);
344
- if (!isSyncPushResponse(parsedCached)) {
345
- return {
346
- response: {
347
- ok: true,
348
- status: 'rejected',
349
- results: [
350
- {
351
- opIndex: 0,
352
- status: 'error',
353
- error: 'IDEMPOTENCY_CACHE_MISS',
354
- code: 'INTERNAL',
355
- retriable: true,
356
- },
357
- ],
358
- },
359
- affectedTables: [],
360
- scopeKeys: [],
361
- emittedChanges: [],
362
- };
363
- }
364
-
365
- const base: SyncPushResponse = {
366
- ...parsedCached,
367
- commitSeq: Number(existing.commit_seq),
368
- };
369
-
370
- if (parsedCached.status === 'applied') {
371
- const tablesFromDb = dialect.dbToArray(
372
- existing.affected_tables
373
- );
374
- return {
375
- response: { ...base, status: 'cached' },
376
- affectedTables:
377
- tablesFromDb.length > 0
378
- ? tablesFromDb
379
- : await readCommitAffectedTables(
380
- trx,
381
- dialect,
382
- Number(existing.commit_seq),
383
- partitionId
384
- ),
385
- scopeKeys: [],
386
- emittedChanges: [],
387
- };
388
- }
389
-
390
- return {
391
- response: base,
392
- affectedTables: [],
393
- scopeKeys: [],
394
- emittedChanges: [],
395
- };
396
- }
1009
+ }
1010
+ return executed;
1011
+ });
397
1012
 
398
- let commitSeq = coerceNumber(insertResult.insertId) ?? 0;
399
- if (commitSeq <= 0) {
400
- const insertedCommit = await (
401
- syncTrx.selectFrom('sync_commits') as SelectQueryBuilder<
402
- SyncCoreDb,
403
- 'sync_commits',
404
- EmptySelection
405
- >
406
- )
407
- .selectAll()
408
- .where('partition_id', '=', partitionId)
409
- .where('client_id', '=', request.clientId)
410
- .where('client_commit_id', '=', request.clientCommitId)
411
- .executeTakeFirstOrThrow();
412
- commitSeq = Number(insertedCommit.commit_seq);
413
- }
414
- const commitId = `${request.clientId}:${request.clientCommitId}`;
415
-
416
- const savepointName = 'sync_apply';
417
- const useSavepoints = dialect.supportsSavepoints;
418
- let savepointCreated = false;
419
-
420
- try {
421
- // Apply the commit under a savepoint so we can roll back app writes on conflict
422
- // while still persisting the commit-level cached response.
423
- if (useSavepoints) {
424
- await sql.raw(`SAVEPOINT ${savepointName}`).execute(trx);
425
- savepointCreated = true;
426
- }
427
-
428
- const allEmitted = [];
429
- const results = [];
430
- const affectedTablesSet = new Set<string>();
431
-
432
- for (let i = 0; i < ops.length; i++) {
433
- const op = ops[i]!;
434
- const handler = getServerHandlerOrThrow(handlers, op.table);
435
- const applied = await handler.applyOperation(
436
- {
437
- db: trx,
438
- trx,
439
- actorId,
440
- auth: args.auth,
441
- clientId: request.clientId,
442
- commitId,
443
- schemaVersion: request.schemaVersion,
444
- },
445
- op,
446
- i
447
- );
448
-
449
- if (applied.result.status !== 'applied') {
450
- results.push(applied.result);
451
- throw new RejectCommitError({
452
- ok: true,
453
- status: 'rejected',
454
- commitSeq,
455
- results,
456
- });
457
- }
458
-
459
- // Framework-level enforcement: emitted changes must have scopes
460
- for (const c of applied.emittedChanges ?? []) {
461
- const scopes = c?.scopes;
462
- if (!scopes || typeof scopes !== 'object') {
463
- results.push({
464
- opIndex: i,
465
- status: 'error' as const,
466
- error: 'MISSING_SCOPES',
467
- code: 'INVALID_SCOPE',
468
- retriable: false,
469
- });
470
- throw new RejectCommitError({
471
- ok: true,
472
- status: 'rejected',
473
- commitSeq,
474
- results,
475
- });
476
- }
477
- }
478
-
479
- results.push(applied.result);
480
- allEmitted.push(...applied.emittedChanges);
481
- for (const c of applied.emittedChanges) {
482
- affectedTablesSet.add(c.table);
483
- }
484
- }
485
-
486
- if (allEmitted.length > 0) {
487
- const changeRows: Array<
488
- Insertable<SyncCoreDb['sync_changes']>
489
- > = allEmitted.map((c) => ({
490
- partition_id: partitionId,
491
- commit_seq: commitSeq,
492
- table: c.table,
493
- row_id: c.row_id,
494
- op: c.op,
495
- row_json: toDialectJsonValue(dialect, c.row_json),
496
- row_version: c.row_version,
497
- scopes: dialect.scopesToDb(c.scopes),
498
- }));
499
-
500
- await syncTrx
501
- .insertInto('sync_changes')
502
- .values(changeRows)
503
- .execute();
504
- }
505
-
506
- const appliedResponse: SyncPushResponse = {
507
- ok: true,
508
- status: 'applied',
509
- commitSeq,
510
- results,
511
- };
512
-
513
- const affectedTables = Array.from(affectedTablesSet).sort();
514
-
515
- const appliedCommitUpdate: Updateable<
516
- SyncCoreDb['sync_commits']
517
- > = {
518
- result_json: toDialectJsonValue(dialect, appliedResponse),
519
- change_count: allEmitted.length,
520
- affected_tables: dialect.arrayToDb(affectedTables) as string[],
521
- };
522
-
523
- await syncTrx
524
- .updateTable('sync_commits')
525
- .set(appliedCommitUpdate)
526
- .where('commit_seq', '=', commitSeq)
527
- .execute();
528
-
529
- // Insert table commits for subscription filtering
530
- if (affectedTables.length > 0) {
531
- const tableCommits: Array<
532
- Insertable<SyncCoreDb['sync_table_commits']>
533
- > = affectedTables.map((table) => ({
534
- partition_id: partitionId,
535
- table,
536
- commit_seq: commitSeq,
537
- }));
538
-
539
- await syncTrx
540
- .insertInto('sync_table_commits')
541
- .values(tableCommits)
542
- .onConflict((oc) =>
543
- oc
544
- .columns(['partition_id', 'table', 'commit_seq'])
545
- .doNothing()
546
- )
547
- .execute();
548
- }
549
-
550
- if (useSavepoints) {
551
- await sql
552
- .raw(`RELEASE SAVEPOINT ${savepointName}`)
553
- .execute(trx);
554
- }
555
-
556
- return {
557
- response: appliedResponse,
558
- affectedTables,
559
- scopeKeys: scopeKeysFromEmitted(allEmitted),
560
- emittedChanges: allEmitted.map((c) => ({
561
- table: c.table,
562
- row_id: c.row_id,
563
- op: c.op,
564
- row_json: c.row_json,
565
- row_version: c.row_version,
566
- scopes: c.scopes,
567
- })),
568
- };
569
- } catch (err) {
570
- // Roll back app writes but keep the commit row.
571
- if (savepointCreated) {
572
- try {
573
- await sql
574
- .raw(`ROLLBACK TO SAVEPOINT ${savepointName}`)
575
- .execute(trx);
576
- await sql
577
- .raw(`RELEASE SAVEPOINT ${savepointName}`)
578
- .execute(trx);
579
- } catch (savepointErr) {
580
- // If savepoint rollback fails, the transaction may be in an
581
- // inconsistent state. Log and rethrow to fail the entire commit
582
- // rather than risk data corruption.
583
- console.error(
584
- '[pushCommit] Savepoint rollback failed:',
585
- savepointErr
586
- );
587
- throw savepointErr;
588
- }
589
- }
590
-
591
- if (!(err instanceof RejectCommitError)) throw err;
592
-
593
- const rejectedCommitUpdate: Updateable<
594
- SyncCoreDb['sync_commits']
595
- > = {
596
- result_json: toDialectJsonValue(dialect, err.response),
597
- change_count: 0,
598
- affected_tables: dialect.arrayToDb([]) as string[],
599
- };
600
-
601
- // Persist the rejected response for commit-level idempotency.
602
- await syncTrx
603
- .updateTable('sync_commits')
604
- .set(rejectedCommitUpdate)
605
- .where('commit_seq', '=', commitSeq)
606
- .execute();
607
-
608
- return {
609
- response: err.response,
610
- affectedTables: [],
611
- scopeKeys: [],
612
- emittedChanges: [],
613
- };
614
- }
615
- })
1013
+ const durationMs = Math.max(0, Date.now() - startedAtMs);
1014
+ const emittedChangeCount = results.reduce(
1015
+ (count, result) => count + result.emittedChanges.length,
1016
+ 0
616
1017
  );
1018
+ const affectedTableCount = results.reduce(
1019
+ (count, result) => count + result.affectedTables.length,
1020
+ 0
1021
+ );
1022
+ const status = results.every(
1023
+ (result) => result.response.status === 'cached'
1024
+ )
1025
+ ? 'cached'
1026
+ : results.every(
1027
+ (result) =>
1028
+ result.response.status === 'applied' ||
1029
+ result.response.status === 'cached'
1030
+ )
1031
+ ? 'applied'
1032
+ : 'rejected';
1033
+
1034
+ span.setAttribute('status', status);
1035
+ span.setAttribute('duration_ms', durationMs);
1036
+ span.setAttribute('commit_count', results.length);
1037
+ span.setAttribute('emitted_change_count', emittedChangeCount);
1038
+ span.setAttribute('affected_table_count', affectedTableCount);
1039
+ span.setStatus('ok');
1040
+
1041
+ if (!suppressTelemetry) {
1042
+ recordPushMetrics({
1043
+ status,
1044
+ durationMs,
1045
+ operationCount: totalOperationCount,
1046
+ emittedChangeCount,
1047
+ affectedTableCount,
1048
+ });
1049
+ }
1050
+
1051
+ return results;
617
1052
  } catch (error) {
618
1053
  const durationMs = Math.max(0, Date.now() - startedAtMs);
619
1054
  span.setAttribute('status', 'error');
620
1055
  span.setAttribute('duration_ms', durationMs);
621
1056
  span.setStatus('error');
622
1057
 
623
- recordPushMetrics({
624
- status: 'error',
625
- durationMs,
626
- operationCount,
627
- emittedChangeCount: 0,
628
- affectedTableCount: 0,
629
- });
630
- captureSyncException(error, {
631
- event: 'sync.server.push',
632
- operationCount,
633
- });
1058
+ if (!suppressTelemetry) {
1059
+ recordPushMetrics({
1060
+ status: 'error',
1061
+ durationMs,
1062
+ operationCount: totalOperationCount,
1063
+ emittedChangeCount: 0,
1064
+ affectedTableCount: 0,
1065
+ });
1066
+ captureSyncException(error, {
1067
+ event: 'sync.server.push_batch',
1068
+ commitCount: requests.length,
1069
+ operationCount: totalOperationCount,
1070
+ });
1071
+ }
634
1072
  throw error;
635
1073
  }
636
1074
  }