@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/src/push.ts CHANGED
@@ -1,1076 +1,693 @@
1
+ /**
2
+ * Push apply (SPEC.md §6) with §3.4 write-path authorization.
3
+ *
4
+ * Security-critical rules implemented here:
5
+ * - authorization runs against the STORED row when it exists, never the
6
+ * pushed payload (§3.4 step 2);
7
+ * - declared scope columns are stripped from every update path (§3.4
8
+ * rule 5) — updates keep the stored row's scope column values;
9
+ * - a lost `baseVersion = 0` insert race re-authorizes the winner's row
10
+ * before disclosing it in a conflict record (§6.2).
11
+ *
12
+ * Per-commit atomicity (§6.4): one storage transaction per commit; the
13
+ * idempotency record persists in the same transaction as the writes.
14
+ *
15
+ * Optional per-table write validation (§6.7) runs after decode + the §3.4
16
+ * scope check, on the row that will persist (post scope-strip, post CRDT
17
+ * merge); a validator throw rejects the whole commit atomically with a
18
+ * host code.
19
+ */
1
20
  import {
2
- captureSyncException,
3
- countSyncMetric,
4
- distributionSyncMetric,
5
- type SyncChange,
6
- type SyncPushRequest,
7
- type SyncPushResponse,
8
- startSyncSpan,
21
+ decodeRow,
22
+ encodeRow,
23
+ type PushCommitFrame,
24
+ type PushOperation,
25
+ type PushOperationResult,
26
+ type PushResultFrame,
27
+ parseBlobRef,
28
+ type RowValue,
9
29
  } from '@syncular/core';
10
- import type { Insertable, Kysely, SelectQueryBuilder, SqlBool } from 'kysely';
11
- import { sql } from 'kysely';
12
- import { finalizeCommitIntegrity } from './commit-integrity';
13
- import {
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';
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';
29
- import type { SyncCoreDb } from './schema';
30
-
31
- // biome-ignore lint/complexity/noBannedTypes: Kysely uses `{}` as the initial "no selected columns yet" marker.
32
- type EmptySelection = {};
33
- type SyncMetadataTrx = Pick<
34
- Kysely<SyncCoreDb>,
35
- 'selectFrom' | 'insertInto' | 'updateTable' | 'deleteFrom'
36
- >;
37
-
38
- export interface PushCommitResult {
39
- response: SyncPushResponse;
40
- /**
41
- * Distinct tables affected by this commit.
42
- * Empty for rejected commits and for commits that emit no changes.
43
- */
44
- affectedTables: string[];
45
- /**
46
- * Scope keys derived from emitted changes (e.g. "org:abc", "team:xyz").
47
- * Computed in-transaction so callers don't need an extra DB query.
48
- * Empty for rejected/cached commits.
49
- */
50
- scopeKeys: string[];
51
- /**
52
- * Changes emitted by this commit. Available for WS data delivery.
53
- * Empty for rejected/cached commits.
54
- */
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;
79
- }
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
-
91
- class RejectCommitError extends Error {
92
- constructor(public readonly response: SyncPushResponse) {
93
- super('REJECT_COMMIT');
94
- this.name = 'RejectCommitError';
95
- }
96
- }
97
-
98
- function isRecord(value: unknown): value is Record<string, unknown> {
99
- return typeof value === 'object' && value !== null;
100
- }
101
-
102
- function isSyncPushResponse(value: unknown): value is SyncPushResponse {
103
- return (
104
- isRecord(value) && value.ok === true && typeof value.status === 'string'
105
- );
106
- }
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
-
130
- async function readCommitAffectedTables<DB extends SyncCoreDb>(
131
- db: DbExecutor<DB>,
132
- dialect: ServerSyncDialect,
133
- commitSeq: number,
134
- partitionId: string
135
- ): Promise<string[]> {
136
- try {
137
- const commitsQ = db.selectFrom('sync_commits') as SelectQueryBuilder<
138
- DB,
139
- 'sync_commits',
140
- EmptySelection
141
- >;
142
-
143
- const row = await commitsQ
144
- .selectAll()
145
- .where(sql<SqlBool>`commit_seq = ${commitSeq}`)
146
- .where(sql<SqlBool>`partition_id = ${partitionId}`)
147
- .executeTakeFirst();
148
-
149
- const raw = row?.affected_tables;
150
- return dialect.dbToArray(raw);
151
- } catch {
152
- // ignore and fall back to scanning changes (best-effort)
153
- }
154
-
155
- // Fallback: read from changes using dialect-specific implementation
156
- return dialect.readAffectedTablesFromChanges(db, commitSeq, { partitionId });
157
- }
158
-
159
- function scopeKeysFromEmitted(
160
- emitted: Array<{ scopes: Record<string, string> }>
30
+ import type { BlobStore } from './blob-store';
31
+ import type { SyncRequestContext } from './context';
32
+ import { clockOf } from './context';
33
+ import type { CrdtMergerRegistry } from './crdt-merger';
34
+ import { SyncError } from './errors';
35
+ import type { CompiledSchema, CompiledTable } from './schema';
36
+ import type { ResolvedScopes } from './scopes';
37
+ import { authorizeWrite, renderScopeValue, storedScopesForRow } from './scopes';
38
+ import type {
39
+ NewChange,
40
+ StorageTransaction,
41
+ StoredCommit,
42
+ StoredPushResult,
43
+ } from './storage';
44
+ import type { ValidateOpKind, ValidatorRegistry } from './validate';
45
+ import { toValidateRow, ValidationRejection } from './validate';
46
+
47
+ /**
48
+ * Extract the blobIds a decoded row references through its `blob_ref`
49
+ * columns (§5.9.4), skipping NULLs. Malformed BlobRefs already failed at
50
+ * row-codec decode (§5.9.1), so `parseBlobRef` here is total.
51
+ */
52
+ function blobIdsInRow(
53
+ table: CompiledTable,
54
+ values: readonly RowValue[],
161
55
  ): string[] {
162
- const keys = new Set<string>();
163
- for (const c of emitted) {
164
- for (const scopeKey of scopeKeysFromScopeValues(c.scopes)) {
165
- keys.add(scopeKey);
166
- }
56
+ const ids: string[] = [];
57
+ for (const index of table.blobRefColumnIndices) {
58
+ const value = values[index];
59
+ if (typeof value === 'string') ids.push(parseBlobRef(value).blobId);
167
60
  }
168
- return Array.from(keys);
61
+ return ids;
169
62
  }
170
63
 
171
- function recordPushMetrics(args: {
172
- status: string;
173
- durationMs: number;
174
- operationCount: number;
175
- emittedChangeCount: number;
176
- affectedTableCount: number;
177
- }): void {
178
- const {
179
- status,
180
- durationMs,
181
- operationCount,
182
- emittedChangeCount,
183
- affectedTableCount,
184
- } = args;
64
+ type OperationOutcome =
65
+ | { readonly kind: 'applied'; readonly change: NewChange | undefined }
66
+ | { readonly kind: 'terminate'; readonly record: PushOperationResult };
185
67
 
186
- countSyncMetric('sync.server.push.requests', 1, {
187
- attributes: { status },
188
- });
189
- countSyncMetric('sync.server.push.operations', operationCount, {
190
- attributes: { status },
191
- });
192
- distributionSyncMetric('sync.server.push.duration_ms', durationMs, {
193
- unit: 'millisecond',
194
- attributes: { status },
195
- });
196
- distributionSyncMetric(
197
- 'sync.server.push.emitted_changes',
198
- emittedChangeCount,
199
- {
200
- attributes: { status },
201
- }
202
- );
203
- distributionSyncMetric(
204
- 'sync.server.push.affected_tables',
205
- affectedTableCount,
206
- {
207
- attributes: { status },
208
- }
209
- );
210
- }
211
-
212
- function createRejectedPushResult(
213
- error: SyncPushResponse['results'][number]
214
- ): PushCommitResult {
68
+ function errorRecord(
69
+ opIndex: number,
70
+ code: string,
71
+ message: string,
72
+ retryable = false,
73
+ ): OperationOutcome {
215
74
  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,
75
+ kind: 'terminate',
76
+ record: { opIndex, status: 'error', code, message, retryable },
226
77
  };
227
78
  }
228
79
 
229
- function createRejectedPushResponse(
230
- error: SyncPushResponse['results'][number],
231
- commitSeq?: number
232
- ): SyncPushResponse {
80
+ function conflictRecord(
81
+ opIndex: number,
82
+ serverVersion: number,
83
+ serverRow: Uint8Array,
84
+ ): OperationOutcome {
233
85
  return {
234
- ok: true,
235
- status: 'rejected',
236
- ...(commitSeq !== undefined ? { commitSeq } : {}),
237
- results: [error],
86
+ kind: 'terminate',
87
+ record: {
88
+ opIndex,
89
+ status: 'conflict',
90
+ code: 'sync.version_conflict',
91
+ message: 'row version does not match baseVersion (§6.2)',
92
+ serverVersion,
93
+ serverRow,
94
+ },
238
95
  };
239
96
  }
240
97
 
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;
98
+ interface BlobApplyContext {
99
+ readonly store: BlobStore | undefined;
100
+ readonly partition: string;
295
101
  }
296
102
 
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();
103
+ /**
104
+ * §6.7: run the table's write-validation hook, if configured, on the row
105
+ * that WILL persist (post scope-strip, post CRDT-merge — the values the
106
+ * store receives). Returns a terminating outcome iff the validator rejects
107
+ * (its `ValidationRejection` code, or `sync.constraint_violation` for a
108
+ * non-`ValidationRejection` throw), else `undefined` (accept / no hook).
109
+ * A no-op for tables with no validator — the `undefined` short-circuit
110
+ * keeps the feature zero-cost when off.
111
+ */
112
+ async function runValidator(
113
+ validators: ValidatorRegistry | undefined,
114
+ table: CompiledTable,
115
+ op: ValidateOpKind,
116
+ rowId: string,
117
+ values: readonly RowValue[] | undefined,
118
+ storedValues: readonly RowValue[] | undefined,
119
+ opIndex: number,
120
+ partition: string,
121
+ actorId: string,
122
+ ): Promise<OperationOutcome | undefined> {
123
+ const validator = validators?.[table.name];
124
+ if (validator === undefined) return undefined;
125
+ try {
126
+ await validator(
127
+ {
128
+ op,
129
+ table: table.name,
130
+ rowId,
131
+ row:
132
+ values !== undefined
133
+ ? toValidateRow(table.columns, values)
134
+ : undefined,
135
+ stored:
136
+ storedValues !== undefined
137
+ ? toValidateRow(table.columns, storedValues)
138
+ : undefined,
139
+ },
140
+ { actorId, partition },
141
+ );
142
+ } catch (error) {
143
+ if (error instanceof ValidationRejection) {
144
+ return errorRecord(opIndex, error.code, error.message);
145
+ }
146
+ // §6.7: a non-ValidationRejection throw is still a rejection, mapped to
147
+ // the generic server-side constraint code (§10.2) — the validator's
148
+ // failure never crashes the request or leaks its message as a code.
149
+ return errorRecord(
150
+ opIndex,
151
+ 'sync.constraint_violation',
152
+ `write validator for table ${JSON.stringify(table.name)} threw: ${error instanceof Error ? error.message : String(error)}`,
153
+ );
341
154
  }
155
+ return undefined;
342
156
  }
343
157
 
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();
158
+ /**
159
+ * §5.10.3: merge the row's `crdt` columns in place. For each crdt column,
160
+ * replace the incoming value with `merge(stored, incoming)` (§5.10.2) —
161
+ * never the raw pushed bytes. `storedValues` is undefined on insert (the
162
+ * stored value is `null` — the empty document). Returns `true` iff any crdt
163
+ * column value changed (so the caller re-encodes), or a terminating
164
+ * `sync.crdt_merge_failed` outcome if a merger is missing or throws.
165
+ *
166
+ * A NULL incoming crdt value is a semantic clear, not a merge — it passes
167
+ * through untouched (the app is nulling the column, the same as any other
168
+ * type). Merging only runs for a non-NULL incoming crdt value.
169
+ */
170
+ async function mergeCrdtColumns(
171
+ table: CompiledTable,
172
+ values: RowValue[],
173
+ storedValues: readonly RowValue[] | undefined,
174
+ opIndex: number,
175
+ mergers: CrdtMergerRegistry | undefined,
176
+ ): Promise<OperationOutcome | { readonly changed: boolean }> {
177
+ if (table.crdtColumns.length === 0) return { changed: false };
178
+ let changed = false;
179
+ for (const { index, crdtType } of table.crdtColumns) {
180
+ const incoming = values[index];
181
+ if (!(incoming instanceof Uint8Array)) continue; // NULL clear or absent
182
+ const merger = mergers?.[crdtType];
183
+ if (merger === undefined) {
184
+ return errorRecord(
185
+ opIndex,
186
+ 'sync.crdt_merge_failed',
187
+ `no CRDT merger registered for crdtType ${JSON.stringify(crdtType)} (§5.10.2)`,
188
+ );
189
+ }
190
+ const storedRaw = storedValues?.[index];
191
+ const stored = storedRaw instanceof Uint8Array ? storedRaw : null;
192
+ let merged: Uint8Array;
193
+ try {
194
+ merged = await merger(stored, incoming);
195
+ } catch (error) {
196
+ return errorRecord(
197
+ opIndex,
198
+ 'sync.crdt_merge_failed',
199
+ `CRDT merger for ${JSON.stringify(crdtType)} threw: ${error instanceof Error ? error.message : String(error)}`,
200
+ );
201
+ }
202
+ values[index] = merged;
203
+ changed = true;
382
204
  }
383
-
384
- await finalizeCommitIntegrity({
385
- db: args.trx,
386
- dialect: args.dialect,
387
- partitionId: args.partitionId,
388
- commitSeq: args.commitSeq,
389
- });
205
+ return { changed };
390
206
  }
391
207
 
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();
208
+ async function applyOperation(
209
+ tx: StorageTransaction,
210
+ schema: CompiledSchema,
211
+ resolved: ResolvedScopes,
212
+ op: PushOperation,
213
+ opIndex: number,
214
+ blobCtx: BlobApplyContext,
215
+ crdtMergers: CrdtMergerRegistry | undefined,
216
+ validators: ValidatorRegistry | undefined,
217
+ partition: string,
218
+ actorId: string,
219
+ ): Promise<OperationOutcome> {
220
+ const table = schema.tables.get(op.table);
221
+ if (table === undefined) {
222
+ return errorRecord(
223
+ opIndex,
224
+ 'sync.unknown_table',
225
+ `table ${JSON.stringify(op.table)} is not handled by this server`,
226
+ );
413
227
  }
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
- });
228
+ if (!resolved.ok) {
229
+ return errorRecord(
230
+ opIndex,
231
+ 'sync.forbidden',
232
+ 'scope resolution failed (§3.4 step 4)',
233
+ );
426
234
  }
235
+ const stored = await tx.getRow(op.table, op.rowId);
427
236
 
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);
237
+ if (op.op === 'delete') {
238
+ if (stored === undefined) {
239
+ // Deleting an absent row is applied (idempotent, §6.2); no change.
240
+ return { kind: 'applied', change: undefined };
241
+ }
242
+ if (!authorizeWrite(table, stored.scopes, resolved)) {
243
+ return errorRecord(
244
+ opIndex,
245
+ 'sync.forbidden',
246
+ 'delete denied by scope authorization (§3.4)',
247
+ );
248
+ }
249
+ const missing = missingScopeVariable(table, stored.scopes);
250
+ if (missing !== undefined) {
251
+ return errorRecord(
252
+ opIndex,
253
+ 'sync.missing_scopes',
254
+ `stored row lacks scope variable ${JSON.stringify(missing)} (§3.1)`,
255
+ );
256
+ }
257
+ // §6.7: validate the delete against the stored row (row = undefined,
258
+ // stored = the row about to be removed). Only reached for an existing
259
+ // row — an absent-row delete is an idempotent no-op above.
260
+ const deleteReject = await runValidator(
261
+ validators,
262
+ table,
263
+ 'delete',
264
+ op.rowId,
265
+ undefined,
266
+ decodeRow(table.columns, stored.payload),
267
+ opIndex,
268
+ partition,
269
+ actorId,
270
+ );
271
+ if (deleteReject !== undefined) return deleteReject;
272
+ await tx.deleteRow(op.table, op.rowId);
435
273
  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,
274
+ kind: 'applied',
275
+ change: {
276
+ table: op.table,
277
+ rowId: op.rowId,
278
+ op: 'delete',
279
+ scopes: stored.scopes,
280
+ },
452
281
  };
453
282
  }
454
283
 
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;
284
+ // upsert — payload presence is enforced by the envelope codec (§6.1).
285
+ const payload = op.payload;
286
+ if (payload === undefined) {
287
+ return errorRecord(
288
+ opIndex,
289
+ 'sync.invalid_request',
290
+ 'upsert without payload',
291
+ );
489
292
  }
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;
293
+ let values: RowValue[];
294
+ try {
295
+ values = decodeRow(table.columns, payload);
296
+ } catch (error) {
297
+ return errorRecord(
298
+ opIndex,
299
+ 'sync.invalid_request',
300
+ `row payload failed row-codec decode (§1.7): ${error instanceof Error ? error.message : String(error)}`,
301
+ );
302
+ }
303
+ const pkValue = renderScopeValue(values[table.primaryKeyIndex]);
304
+ if (pkValue !== op.rowId) {
305
+ return errorRecord(
306
+ opIndex,
307
+ 'sync.invalid_request',
308
+ 'payload primary key does not match rowId',
309
+ );
502
310
  }
503
311
 
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}`);
312
+ if (stored !== undefined) {
313
+ // §3.4 step 2: authorize against the STORED row, never the payload.
314
+ if (!authorizeWrite(table, stored.scopes, resolved)) {
315
+ return errorRecord(
316
+ opIndex,
317
+ 'sync.forbidden',
318
+ 'write denied by scope authorization (§3.4)',
319
+ );
534
320
  }
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;
321
+ if (op.baseVersion === 0) {
322
+ // Lost insert race (§6.2); the stored row was authorized above,
323
+ // so disclosure of the winner is permitted.
324
+ return conflictRecord(opIndex, stored.serverVersion, stored.payload);
558
325
  }
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);
326
+ if (
327
+ op.baseVersion !== undefined &&
328
+ op.baseVersion !== stored.serverVersion
329
+ ) {
330
+ return conflictRecord(opIndex, stored.serverVersion, stored.payload);
331
+ }
332
+ // §3.4 rule 5: scope columns are immutable on update — keep the
333
+ // stored row's scope column values on both the baseVersion and
334
+ // last-write-wins paths.
335
+ const storedValues = decodeRow(table.columns, stored.payload);
336
+ let mutated = false;
337
+ for (const pattern of table.scopePatterns) {
338
+ const storedValue = storedValues[pattern.columnIndex] ?? null;
339
+ if (values[pattern.columnIndex] !== storedValue) {
340
+ values[pattern.columnIndex] = storedValue;
341
+ mutated = true;
579
342
  }
580
343
  }
344
+ // §5.10.3: crdt columns merge (stored ⊕ incoming) — never LWW, never
345
+ // baseVersion-conflict (they were excluded from the checks above).
346
+ const mergeOutcome = await mergeCrdtColumns(
347
+ table,
348
+ values,
349
+ storedValues,
350
+ opIndex,
351
+ crdtMergers,
352
+ );
353
+ if ('kind' in mergeOutcome) return mergeOutcome;
354
+ if (mergeOutcome.changed) mutated = true;
355
+ const newPayload = mutated ? encodeRow(table.columns, values) : payload;
356
+ const newVersion = stored.serverVersion + 1;
357
+ // §6.6 / §5.9.6: verify referenced blobs exist before writing.
358
+ const blobCheck = await checkAndRecordBlobs(
359
+ tx,
360
+ table,
361
+ op.rowId,
362
+ values,
363
+ opIndex,
364
+ blobCtx,
365
+ );
366
+ if (blobCheck !== undefined) return blobCheck;
367
+ // §6.7: validate the merged, scope-stripped row that will persist —
368
+ // for a crdt column the validator sees the MERGED value (§5.10.3), the
369
+ // state the store holds, not the raw pushed update.
370
+ const updateReject = await runValidator(
371
+ validators,
372
+ table,
373
+ 'upsert',
374
+ op.rowId,
375
+ values,
376
+ storedValues,
377
+ opIndex,
378
+ partition,
379
+ actorId,
380
+ );
381
+ if (updateReject !== undefined) return updateReject;
382
+ const newRow = {
383
+ rowId: op.rowId,
384
+ serverVersion: newVersion,
385
+ scopes: stored.scopes,
386
+ payload: newPayload,
387
+ };
388
+ await tx.upsertRow(op.table, newRow);
389
+ return {
390
+ kind: 'applied',
391
+ change: {
392
+ table: op.table,
393
+ rowId: op.rowId,
394
+ op: 'upsert',
395
+ rowVersion: newVersion,
396
+ scopes: stored.scopes,
397
+ payload: newPayload,
398
+ },
399
+ };
400
+ }
581
401
 
582
- if (!appliedBatch) {
583
- let appliedSingle = await handler.applyOperation(
584
- operationCtx,
585
- transformedOp,
586
- i
402
+ // Insert path: no stored row.
403
+ if (op.baseVersion !== undefined && op.baseVersion !== 0) {
404
+ // Authorize the payload first so absence is not disclosed to actors
405
+ // without the scope; then §6.2: baseVersion ≠ 0, row absent.
406
+ const extractedFirst = storedScopesForRow(table, values);
407
+ if (
408
+ 'missing' in extractedFirst ||
409
+ !authorizeWrite(table, extractedFirst.scopes, resolved)
410
+ ) {
411
+ return errorRecord(
412
+ opIndex,
413
+ 'sync.forbidden',
414
+ 'write denied by scope authorization (§3.4)',
587
415
  );
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
416
  }
417
+ return errorRecord(
418
+ opIndex,
419
+ 'sync.row_missing',
420
+ 'upsert with baseVersion targets an absent row (§6.2)',
421
+ );
422
+ }
423
+ const extracted = storedScopesForRow(table, values);
424
+ if ('missing' in extracted) {
425
+ // §3.4 step 2: a missing or empty scope column value ⇒ deny.
426
+ return errorRecord(
427
+ opIndex,
428
+ 'sync.forbidden',
429
+ `insert missing scope column value for ${JSON.stringify(extracted.missing)} (§3.4)`,
430
+ );
431
+ }
432
+ if (!authorizeWrite(table, extracted.scopes, resolved)) {
433
+ return errorRecord(
434
+ opIndex,
435
+ 'sync.forbidden',
436
+ 'insert denied by scope authorization (§3.4)',
437
+ );
438
+ }
439
+ // §5.10.3: on insert a crdt column merges against the empty document
440
+ // (stored = null) — normalizes the initial state through the merger.
441
+ const insertMerge = await mergeCrdtColumns(
442
+ table,
443
+ values,
444
+ undefined,
445
+ opIndex,
446
+ crdtMergers,
447
+ );
448
+ if ('kind' in insertMerge) return insertMerge;
449
+ const insertPayload = insertMerge.changed
450
+ ? encodeRow(table.columns, values)
451
+ : payload;
452
+ // §6.6 / §5.9.6: verify referenced blobs exist before writing.
453
+ const blobCheck = await checkAndRecordBlobs(
454
+ tx,
455
+ table,
456
+ op.rowId,
457
+ values,
458
+ opIndex,
459
+ blobCtx,
460
+ );
461
+ if (blobCheck !== undefined) return blobCheck;
462
+ // §6.7: validate the insert row (stored = undefined, so a validator can
463
+ // distinguish create from update); crdt columns are already merged
464
+ // against the empty document.
465
+ const insertReject = await runValidator(
466
+ validators,
467
+ table,
468
+ 'upsert',
469
+ op.rowId,
470
+ values,
471
+ undefined,
472
+ opIndex,
473
+ partition,
474
+ actorId,
475
+ );
476
+ if (insertReject !== undefined) return insertReject;
477
+ const newRow = {
478
+ rowId: op.rowId,
479
+ serverVersion: 1,
480
+ scopes: extracted.scopes,
481
+ payload: insertPayload,
482
+ };
483
+ await tx.upsertRow(op.table, newRow);
484
+ return {
485
+ kind: 'applied',
486
+ change: {
487
+ table: op.table,
488
+ rowId: op.rowId,
489
+ op: 'upsert',
490
+ rowVersion: 1,
491
+ scopes: extracted.scopes,
492
+ payload: insertPayload,
493
+ },
494
+ };
495
+ }
602
496
 
603
- if (appliedBatch.length === 0) {
604
- throw new Error(
605
- `Handler "${op.table}" returned no results from applyOperationBatch`
497
+ /**
498
+ * §5.9.6/§6.6: for a row's `blob_ref` columns, verify every referenced blob
499
+ * exists, then record the row's reference set in the index (§5.9.4). Returns
500
+ * a terminating `blob.not_found` outcome if any blob is absent (or the store
501
+ * is unconfigured while a ref exists), else `undefined` (proceed). No-op for
502
+ * tables with no `blob_ref` columns.
503
+ */
504
+ async function checkAndRecordBlobs(
505
+ tx: StorageTransaction,
506
+ table: CompiledTable,
507
+ rowId: string,
508
+ values: readonly RowValue[],
509
+ opIndex: number,
510
+ blobCtx: BlobApplyContext,
511
+ ): Promise<OperationOutcome | undefined> {
512
+ if (table.blobRefColumnIndices.length === 0) return undefined;
513
+ const blobIds = blobIdsInRow(table, values);
514
+ if (blobIds.length > 0) {
515
+ if (blobCtx.store === undefined) {
516
+ return errorRecord(
517
+ opIndex,
518
+ 'blob.not_found',
519
+ 'row references a blob but the server has no blob store (§5.9.6)',
606
520
  );
607
521
  }
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)
522
+ for (const blobId of blobIds) {
523
+ if (!(await blobCtx.store.has(blobCtx.partition, blobId))) {
524
+ return errorRecord(
525
+ opIndex,
526
+ 'blob.not_found',
527
+ `push references blob ${blobId} which has not been uploaded (§5.9.6)`,
614
528
  );
615
529
  }
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
530
  }
531
+ }
532
+ // Update the reference index for this row (empty set clears it, §5.9.4).
533
+ if (tx.setBlobRefs !== undefined) {
534
+ await tx.setBlobRefs(table.name, rowId, blobIds);
535
+ }
536
+ return undefined;
537
+ }
640
538
 
641
- i += consumed;
539
+ function missingScopeVariable(
540
+ table: CompiledTable,
541
+ scopes: Record<string, string>,
542
+ ): string | undefined {
543
+ for (const pattern of table.scopePatterns) {
544
+ const value = scopes[pattern.variable];
545
+ if (value === undefined || value.length === 0) return pattern.variable;
642
546
  }
547
+ return undefined;
548
+ }
643
549
 
550
+ function resultFrame(
551
+ clientCommitId: string,
552
+ stored: StoredPushResult,
553
+ replay: boolean,
554
+ ): PushResultFrame {
555
+ const status =
556
+ stored.status === 'applied' ? (replay ? 'cached' : 'applied') : 'rejected';
644
557
  return {
645
- results,
646
- emittedChanges: allEmitted,
647
- affectedTables: Array.from(affectedTablesSet).sort(),
558
+ type: 'PUSH_RESULT',
559
+ clientCommitId,
560
+ status,
561
+ ...(stored.status === 'applied' && stored.commitSeq !== undefined
562
+ ? { commitSeq: stored.commitSeq }
563
+ : {}),
564
+ results: [...stored.results],
648
565
  };
649
566
  }
650
567
 
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
- }
568
+ export interface AppliedCommitEvent {
569
+ readonly commit: StoredCommit;
570
+ }
708
571
 
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);
572
+ /**
573
+ * Process one `PUSH_COMMIT` frame: idempotency replay (§2.3), sequential
574
+ * atomic apply (§6.4), realtime notification for applied commits.
575
+ */
576
+ export async function processPushCommit(
577
+ ctx: SyncRequestContext,
578
+ schema: CompiledSchema,
579
+ resolved: ResolvedScopes,
580
+ clientId: string,
581
+ frame: PushCommitFrame,
582
+ ): Promise<PushResultFrame> {
583
+ const { storage, partition } = ctx;
584
+ let persisted: StoredPushResult | undefined;
585
+ try {
586
+ persisted = await storage.getPushResult(
587
+ partition,
588
+ clientId,
589
+ frame.clientCommitId,
590
+ );
591
+ } catch (error) {
592
+ if (
593
+ error instanceof SyncError &&
594
+ error.code === 'sync.idempotency_cache_miss'
595
+ ) {
596
+ // §6.3: answer the retryable cache-miss for this commit rather than
597
+ // re-applying. Not persisted — a retry may find a readable record.
598
+ return {
599
+ type: 'PUSH_RESULT',
600
+ clientCommitId: frame.clientCommitId,
601
+ status: 'rejected',
602
+ results: [
603
+ {
604
+ opIndex: 0,
605
+ status: 'error',
606
+ code: 'sync.idempotency_cache_miss',
607
+ message: error.message,
608
+ retryable: true,
609
+ },
610
+ ],
611
+ };
612
+ }
613
+ throw error;
723
614
  }
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
- };
615
+ if (persisted !== undefined) {
616
+ return resultFrame(frame.clientCommitId, persisted, true);
755
617
  }
756
618
 
757
- const savepointName = `sync_apply_${commitSeq}`;
758
- const useSavepoints = shouldUseSavepoints({
759
- dialect,
760
- handlers,
761
- operations: ops,
762
- });
763
- let savepointCreated = false;
764
-
619
+ const createdAtMs = clockOf(ctx)();
620
+ const blobCtx: BlobApplyContext = { store: ctx.blobs, partition };
621
+ const crdtMergers = ctx.crdtMergers;
622
+ const validators = ctx.validators;
623
+ const tx = await storage.begin(partition);
765
624
  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;
625
+ const results: PushOperationResult[] = [];
626
+ const changes: NewChange[] = [];
627
+ let terminated: PushOperationResult | undefined;
628
+ for (let opIndex = 0; opIndex < frame.operations.length; opIndex++) {
629
+ const op = frame.operations[opIndex];
630
+ if (op === undefined) continue;
631
+ const outcome = await applyOperation(
632
+ tx,
633
+ schema,
634
+ resolved,
635
+ op,
636
+ opIndex,
637
+ blobCtx,
638
+ crdtMergers,
639
+ validators,
640
+ partition,
641
+ ctx.actorId,
642
+ );
643
+ if (outcome.kind === 'terminate') {
644
+ terminated = outcome.record;
645
+ break;
828
646
  }
647
+ results.push({ opIndex, status: 'applied' });
648
+ if (outcome.change !== undefined) changes.push(outcome.change);
829
649
  }
830
650
 
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
-
856
- export async function pushCommit<
857
- DB extends SyncCoreDb,
858
- Auth extends SyncServerAuth,
859
- >(args: {
860
- db: Kysely<DB>;
861
- dialect: ServerSyncDialect;
862
- handlers: ServerHandlerCollection<DB, Auth>;
863
- plugins?: readonly SyncServerPushPlugin<DB, Auth>[];
864
- auth: Auth;
865
- request: SyncPushRequest;
866
- validateCommit?: PushCommitValidator<DB, Auth>;
867
- suppressTelemetry?: boolean;
868
- }): Promise<PushCommitResult> {
869
- const { db, dialect, handlers, request } = args;
870
- const pushPlugins = sortServerPushPlugins(args.plugins);
871
- const requestedOps = Array.isArray(request.operations)
872
- ? request.operations
873
- : [];
874
- const operationCount = requestedOps.length;
875
- const startedAtMs = Date.now();
876
- const suppressTelemetry = args.suppressTelemetry === true;
877
-
878
- return startSyncSpan(
879
- {
880
- name: 'sync.server.push',
881
- op: 'sync.push',
882
- attributes: {
883
- operation_count: operationCount,
884
- },
885
- },
886
- async (span) => {
887
- const finalizeResult = (result: PushCommitResult): PushCommitResult => {
888
- const durationMs = Math.max(0, Date.now() - startedAtMs);
889
- const status = result.response.status;
890
-
891
- span.setAttribute('status', status);
892
- span.setAttribute('duration_ms', durationMs);
893
- span.setAttribute('emitted_change_count', result.emittedChanges.length);
894
- span.setAttribute('affected_table_count', result.affectedTables.length);
895
- span.setStatus('ok');
896
-
897
- if (!suppressTelemetry) {
898
- recordPushMetrics({
899
- status,
900
- durationMs,
901
- operationCount,
902
- emittedChangeCount: result.emittedChanges.length,
903
- affectedTableCount: result.affectedTables.length,
904
- });
905
- }
906
-
907
- return result;
651
+ if (terminated !== undefined) {
652
+ // §6.3 rejected: only the terminating operation's record; §6.4:
653
+ // every write of the commit rolls back.
654
+ await tx.rollback();
655
+ const stored: StoredPushResult = {
656
+ status: 'rejected',
657
+ results: [terminated],
908
658
  };
909
-
659
+ const rejectionTx = await storage.begin(partition);
910
660
  try {
911
- const validationError = validatePushRequest(request);
912
- if (validationError) {
913
- return finalizeResult(createRejectedPushResult(validationError));
914
- }
915
-
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
- );
661
+ await rejectionTx.putPushResult(clientId, frame.clientCommitId, stored);
662
+ await rejectionTx.commit();
929
663
  } 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,
946
- });
947
- }
664
+ await rejectionTx.rollback();
948
665
  throw error;
949
666
  }
667
+ return resultFrame(frame.clientCommitId, stored, false);
950
668
  }
951
- );
952
- }
953
-
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);
977
-
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
- }
997
-
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
- })
1008
- );
1009
- }
1010
- return executed;
1011
- });
1012
669
 
1013
- const durationMs = Math.max(0, Date.now() - startedAtMs);
1014
- const emittedChangeCount = results.reduce(
1015
- (count, result) => count + result.emittedChanges.length,
1016
- 0
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;
1052
- } catch (error) {
1053
- const durationMs = Math.max(0, Date.now() - startedAtMs);
1054
- span.setAttribute('status', 'error');
1055
- span.setAttribute('duration_ms', durationMs);
1056
- span.setStatus('error');
1057
-
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
- }
1072
- throw error;
1073
- }
670
+ const commitSeq = await tx.appendCommit({
671
+ clientId,
672
+ clientCommitId: frame.clientCommitId,
673
+ actorId: ctx.actorId,
674
+ createdAtMs,
675
+ changes,
676
+ });
677
+ const stored: StoredPushResult = { status: 'applied', commitSeq, results };
678
+ await tx.putPushResult(clientId, frame.clientCommitId, stored);
679
+ await tx.commit();
680
+ if (ctx.realtime !== undefined && changes.length > 0) {
681
+ await ctx.realtime.notifyCommit(partition, {
682
+ commitSeq,
683
+ createdAtMs,
684
+ actorId: ctx.actorId,
685
+ changes,
686
+ });
1074
687
  }
1075
- );
688
+ return resultFrame(frame.clientCommitId, stored, false);
689
+ } catch (error) {
690
+ await tx.rollback();
691
+ throw error;
692
+ }
1076
693
  }