@syncular/server 0.1.2 → 0.2.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 (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
@@ -0,0 +1,1082 @@
1
+ /**
2
+ * Realtime channel (SPEC.md §8) — transport-agnostic.
3
+ *
4
+ * A `RealtimeSession` wraps a connected socket's send/receive callbacks:
5
+ * the host feeds inbound text frames to `handleMessage`, binary frames to
6
+ * `handleBinary`, and wires `session.close()` to socket close. Initial
7
+ * subscription registration comes from the client's most recent pull
8
+ * (§8.1, loaded from the client record); a sync round completed on the
9
+ * connection replaces it at round end (§8.7). Deltas are complete SSP2
10
+ * response messages pushed as `0x00`-tagged binary (§8.2/§8.7); sync
11
+ * rounds ride the same socket as `0x01`-tagged byte-stream chunks driven
12
+ * through the SAME `createSyncResponseStream` as `POST /sync` (§8.7 —
13
+ * one handler, two framings); the only JSON data-plane server event is
14
+ * the `sync` wake-up (§8.3).
15
+ */
16
+ import {
17
+ type CommitChange,
18
+ DecodeError,
19
+ decodeMessage,
20
+ encodeMessage,
21
+ encodePresenceError,
22
+ encodePresenceFanout,
23
+ MessageStreamScanner,
24
+ PROTOCOL_WIRE_VERSION,
25
+ type PresenceKind,
26
+ parseRealtimePresencePublish,
27
+ REALTIME_TAG_DELTA,
28
+ REALTIME_TAG_ROUND,
29
+ type ResponseFrame,
30
+ type ScopeMap,
31
+ type WakeReason,
32
+ } from '@syncular/core';
33
+ import type {
34
+ LeaseConfig,
35
+ ResolveScopes,
36
+ ServerLimits,
37
+ SyncRequestContext,
38
+ } from './context';
39
+ import { RESOLVER_OUTAGE } from './context';
40
+ import { SyncError, syncError } from './errors';
41
+ import { emitEvent, type SyncularServerEvents } from './events';
42
+ import { createSyncResponseStream } from './handler';
43
+ import type { ServerSchema } from './schema';
44
+ import { type CompiledSchema, compileSchema } from './schema';
45
+ import {
46
+ computeEffective,
47
+ matchesEffective,
48
+ type ResolvedScopes,
49
+ } from './scopes';
50
+ import type { SegmentStore } from './segment-store';
51
+ import type { SegmentUrlConfig } from './signed-url';
52
+ import type { ServerStorage, StoredCommit } from './storage';
53
+
54
+ export interface RealtimeHubConfig {
55
+ readonly schema: ServerSchema;
56
+ readonly storage: ServerStorage;
57
+ readonly resolveScopes: ResolveScopes;
58
+ readonly clock?: () => number;
59
+ /** Deltas larger than this become `delta-too-large` wake-ups (§8.2). */
60
+ readonly maxDeltaBytes?: number;
61
+ /** Optional structured-events sink (`realtime.*` events). */
62
+ readonly events?: SyncularServerEvents;
63
+ /**
64
+ * Segment store for sync rounds over the socket (§8.7). Without it a
65
+ * socket round fails loudly with an in-band ERROR — provide the same
66
+ * store the HTTP binding uses (one handler, two framings).
67
+ */
68
+ readonly segments?: SegmentStore;
69
+ /** Request limits for socket rounds; defaults match the HTTP binding. */
70
+ readonly limits?: Partial<ServerLimits>;
71
+ readonly signedUrls?: SegmentUrlConfig;
72
+ /** §7.3 auth leases for socket sync rounds (§8.7) — same config the
73
+ * HTTP binding uses, so rounds over the socket are lease-aware too. */
74
+ readonly leases?: LeaseConfig;
75
+ /**
76
+ * §8.6 presence: cap on the serialized size (bytes) of a published
77
+ * presence document. An over-cap publish is rejected loudly to the
78
+ * publisher with `presence.too_large` and fans out nothing. Default 16 KiB.
79
+ */
80
+ readonly maxPresenceBytes?: number;
81
+ /**
82
+ * §8.6.4 presence rate cap: minimum ms between fanned-out publishes per
83
+ * connection per scope key. Absent ⇒ off (the reference default): every
84
+ * publish fans out immediately. When set, publishes exceeding the cap
85
+ * coalesce latest-wins into at most one `update` per window (never an
86
+ * error, never a stale or lost latest document).
87
+ */
88
+ readonly presenceMinIntervalMs?: number;
89
+ }
90
+
91
+ export interface RealtimeConnectOptions {
92
+ readonly partition: string;
93
+ readonly actorId: string;
94
+ readonly clientId: string;
95
+ /**
96
+ * Socket send: JSON control messages as text, tagged binary otherwise
97
+ * (§8.7). A host MAY return a promise that resolves when the socket
98
+ * has drained — the session awaits it between round-response chunks
99
+ * (§8.7 backpressure: bounded buffering, mechanics host-owned).
100
+ */
101
+ readonly send: (data: string | Uint8Array) => void | Promise<void>;
102
+ /**
103
+ * Close the underlying socket — invoked on §8.7 protocol violations
104
+ * (pipelined rounds, unframed streams). The host must still call
105
+ * `session.close()` from its socket-close handler as usual.
106
+ */
107
+ readonly closeSocket?: () => void;
108
+ }
109
+
110
+ interface Registration {
111
+ readonly id: string;
112
+ readonly table: string;
113
+ readonly effective: ScopeMap;
114
+ }
115
+
116
+ const DEFAULT_MAX_DELTA_BYTES = 1024 * 1024;
117
+ const DEFAULT_MAX_PRESENCE_BYTES = 16 * 1024;
118
+
119
+ type PresenceDoc = Record<string, unknown>;
120
+
121
+ /**
122
+ * §8.6 presence registry — pure in-memory ephemeral state, keyed per
123
+ * `(partition, scopeKey)` → the set of present sessions and their
124
+ * documents. It never touches `ServerStorage`; a server restart loses all
125
+ * presence (§8.6.1). Fanout is scoped to registered peers only — the
126
+ * privacy floor (§8.6.3).
127
+ */
128
+ class PresenceRegistry {
129
+ /** partition → scopeKey → session → document. */
130
+ readonly #byKey = new Map<
131
+ string,
132
+ Map<string, Map<RealtimeSession, PresenceDoc>>
133
+ >();
134
+
135
+ #keyMap(
136
+ partition: string,
137
+ scopeKey: string,
138
+ ): Map<RealtimeSession, PresenceDoc> {
139
+ let byScope = this.#byKey.get(partition);
140
+ if (byScope === undefined) {
141
+ byScope = new Map();
142
+ this.#byKey.set(partition, byScope);
143
+ }
144
+ let sessions = byScope.get(scopeKey);
145
+ if (sessions === undefined) {
146
+ sessions = new Map();
147
+ byScope.set(scopeKey, sessions);
148
+ }
149
+ return sessions;
150
+ }
151
+
152
+ /** Current documents on a key, excluding one session (the snapshot,
153
+ * §8.6.4). */
154
+ snapshot(
155
+ partition: string,
156
+ scopeKey: string,
157
+ exclude: RealtimeSession,
158
+ ): Array<{ session: RealtimeSession; doc: PresenceDoc }> {
159
+ const sessions = this.#byKey.get(partition)?.get(scopeKey);
160
+ if (sessions === undefined) return [];
161
+ const out: Array<{ session: RealtimeSession; doc: PresenceDoc }> = [];
162
+ for (const [session, doc] of sessions) {
163
+ if (session !== exclude) out.push({ session, doc });
164
+ }
165
+ return out;
166
+ }
167
+
168
+ /** Store/replace a session's document for a key. Returns whether this is
169
+ * the session's FIRST document on the key (join) or a replacement
170
+ * (update). */
171
+ set(
172
+ partition: string,
173
+ scopeKey: string,
174
+ session: RealtimeSession,
175
+ doc: PresenceDoc,
176
+ ): 'join' | 'update' {
177
+ const sessions = this.#keyMap(partition, scopeKey);
178
+ const kind = sessions.has(session) ? 'update' : 'join';
179
+ sessions.set(session, doc);
180
+ return kind;
181
+ }
182
+
183
+ /** Remove a session's document for a key. Returns true if one existed. */
184
+ clear(
185
+ partition: string,
186
+ scopeKey: string,
187
+ session: RealtimeSession,
188
+ ): boolean {
189
+ const sessions = this.#byKey.get(partition)?.get(scopeKey);
190
+ if (sessions === undefined) return false;
191
+ const existed = sessions.delete(session);
192
+ if (sessions.size === 0) this.#byKey.get(partition)?.delete(scopeKey);
193
+ return existed;
194
+ }
195
+
196
+ /** Every key a session currently holds a document on (for leave-on-drop
197
+ * and registration-change re-derivation). */
198
+ keysOf(partition: string, session: RealtimeSession): string[] {
199
+ const byScope = this.#byKey.get(partition);
200
+ if (byScope === undefined) return [];
201
+ const keys: string[] = [];
202
+ for (const [scopeKey, sessions] of byScope) {
203
+ if (sessions.has(session)) keys.push(scopeKey);
204
+ }
205
+ return keys;
206
+ }
207
+
208
+ /** Sessions currently registered as peers on a key (for fanout). */
209
+ peers(partition: string, scopeKey: string): RealtimeSession[] {
210
+ const sessions = this.#byKey.get(partition)?.get(scopeKey);
211
+ return sessions === undefined ? [] : [...sessions.keys()];
212
+ }
213
+ }
214
+
215
+ /** The scope keys a set of registrations covers (§3.1: `prefix:value`). A
216
+ * registration's effective scopes are variable → values; a scope key is
217
+ * `prefix:value` for each declared value. */
218
+ function registrationScopeKeys(
219
+ registrations: readonly Registration[],
220
+ schema: CompiledSchema,
221
+ ): Set<string> {
222
+ const keys = new Set<string>();
223
+ for (const registration of registrations) {
224
+ const table = schema.tables.get(registration.table);
225
+ if (table === undefined) continue;
226
+ for (const [variable, values] of Object.entries(registration.effective)) {
227
+ const pattern = table.scopePatterns.find((p) => p.variable === variable);
228
+ if (pattern === undefined) continue;
229
+ for (const value of values) keys.add(`${pattern.prefix}:${value}`);
230
+ }
231
+ }
232
+ return keys;
233
+ }
234
+
235
+ export class RealtimeSession {
236
+ readonly sessionId: string;
237
+ readonly partition: string;
238
+ readonly actorId: string;
239
+ readonly clientId: string;
240
+ /** Highest contiguously applied commitSeq acknowledged by the client. */
241
+ cursor: number;
242
+ /** Suppress deltas until the client catches up via pull + ack (§8.2). */
243
+ wakePending: boolean;
244
+ lastKnownSeq: number;
245
+ /** Replaced at socket-round completion (§8.7); initial set from §8.1. */
246
+ registrations: readonly Registration[];
247
+ /** Epoch-ms (hub clock) at registration, for `realtime.closed`. */
248
+ readonly openedAtMs: number;
249
+ #send: (data: string | Uint8Array) => void | Promise<void>;
250
+ #closeSocket: (() => void) | undefined;
251
+ #hub: RealtimeHub;
252
+ #clock: () => number;
253
+ #maxDeltaBytes: number;
254
+ #storage: ServerStorage;
255
+ #events: SyncularServerEvents | undefined;
256
+ /** §8.7 round state: request assembly + one-round-in-flight. The
257
+ * token identifies the owning round so a finished round's cleanup
258
+ * never clobbers a follow-up round's in-flight marker. */
259
+ #requestScanner: MessageStreamScanner | undefined;
260
+ #activeRound: symbol | undefined;
261
+ /** §8.6.4 rate cap: per-scope-key last-fanout time + a pending latest
262
+ * document coalesced while over the cap. */
263
+ #presenceRate = new Map<
264
+ string,
265
+ {
266
+ lastFanoutMs: number;
267
+ pending?: PresenceDoc;
268
+ timer?: ReturnType<typeof setTimeout>;
269
+ }
270
+ >();
271
+
272
+ constructor(
273
+ hub: RealtimeHub,
274
+ options: RealtimeConnectOptions,
275
+ registrations: readonly Registration[],
276
+ cursor: number,
277
+ latestSeq: number,
278
+ clock: () => number,
279
+ maxDeltaBytes: number,
280
+ storage: ServerStorage,
281
+ events: SyncularServerEvents | undefined,
282
+ ) {
283
+ this.sessionId = crypto.randomUUID();
284
+ this.partition = options.partition;
285
+ this.actorId = options.actorId;
286
+ this.clientId = options.clientId;
287
+ this.cursor = cursor;
288
+ this.lastKnownSeq = latestSeq;
289
+ this.wakePending = cursor < latestSeq;
290
+ this.registrations = registrations;
291
+ this.openedAtMs = clock();
292
+ this.#send = options.send;
293
+ this.#closeSocket = options.closeSocket;
294
+ this.#hub = hub;
295
+ this.#clock = clock;
296
+ this.#maxDeltaBytes = maxDeltaBytes;
297
+ this.#storage = storage;
298
+ this.#events = events;
299
+ }
300
+
301
+ /** Feed an inbound text frame (client → server control message, §8.2
302
+ * ack, §8.6.2 presence). */
303
+ handleMessage(text: string): void {
304
+ let parsed: unknown;
305
+ try {
306
+ parsed = JSON.parse(text);
307
+ } catch {
308
+ return; // tolerate unknown/garbled control messages
309
+ }
310
+ if (typeof parsed !== 'object' || parsed === null) return;
311
+ // §8.6.2: client→server presence carries `event: 'presence'` (server
312
+ // control messages carry `event`; the ack carries `type`, §8.1).
313
+ if ((parsed as { event?: unknown }).event === 'presence') {
314
+ this.#handlePresence(text);
315
+ return;
316
+ }
317
+ if ((parsed as { type?: unknown }).type !== 'ack') return;
318
+ const cursor = (parsed as { cursor?: unknown }).cursor;
319
+ if (typeof cursor !== 'number' || !Number.isSafeInteger(cursor)) return;
320
+ this.cursor = Math.max(this.cursor, cursor);
321
+ if (this.cursor >= this.lastKnownSeq) this.wakePending = false;
322
+ // §8.2: acks update the client cursor record without an HTTP pull.
323
+ void this.#persistCursor();
324
+ }
325
+
326
+ /** §8.6.2 inbound presence publish/leave from the client. */
327
+ #handlePresence(text: string): void {
328
+ let publish: ReturnType<typeof parseRealtimePresencePublish>;
329
+ try {
330
+ publish = parseRealtimePresencePublish(text);
331
+ } catch {
332
+ // A malformed known event is a parse error (§8.1) — a broken client;
333
+ // drop the connection loudly rather than silently ignore.
334
+ this.#violation('malformed presence control message (§8.6.2)');
335
+ return;
336
+ }
337
+ const { scopeKey, doc } = publish.data;
338
+ // §8.6.3 authorization: the publish key must be in the connection's
339
+ // registered effective scopes.
340
+ if (!this.#currentScopeKeys().has(scopeKey)) {
341
+ this.#sendSafe(
342
+ encodePresenceError(scopeKey, 'presence.forbidden', this.#clock()),
343
+ );
344
+ return;
345
+ }
346
+ if (doc === null) {
347
+ this.#leavePresence(scopeKey);
348
+ return;
349
+ }
350
+ // §8.6.2 size cap: fail loud to the publisher, fan out nothing.
351
+ const serialized = JSON.stringify(doc);
352
+ if (serialized.length > this.#hub.maxPresenceBytes) {
353
+ this.#sendSafe(
354
+ encodePresenceError(scopeKey, 'presence.too_large', this.#clock()),
355
+ );
356
+ return;
357
+ }
358
+ this.#publishPresence(scopeKey, doc);
359
+ }
360
+
361
+ /** §8.6.4: store + fan out a publish, honoring the rate cap (latest-wins
362
+ * coalesce). */
363
+ #publishPresence(scopeKey: string, doc: PresenceDoc): void {
364
+ const minInterval = this.#hub.presenceMinIntervalMs;
365
+ if (minInterval > 0) {
366
+ const now = this.#clock();
367
+ const rate = this.#presenceRate.get(scopeKey);
368
+ if (rate !== undefined && now - rate.lastFanoutMs < minInterval) {
369
+ // Over the cap: keep only the latest document, fan it out at the
370
+ // window edge (never an error, never a stale/lost latest, §8.6.4).
371
+ rate.pending = doc;
372
+ if (rate.timer === undefined) {
373
+ const delay = minInterval - (now - rate.lastFanoutMs);
374
+ rate.timer = setTimeout(() => {
375
+ delete rate.timer;
376
+ const pending = rate.pending;
377
+ delete rate.pending;
378
+ if (pending !== undefined) this.#fanoutPublish(scopeKey, pending);
379
+ }, delay);
380
+ }
381
+ return;
382
+ }
383
+ }
384
+ this.#fanoutPublish(scopeKey, doc);
385
+ }
386
+
387
+ #fanoutPublish(scopeKey: string, doc: PresenceDoc): void {
388
+ const kind = this.#hub.presence.set(this.partition, scopeKey, this, doc);
389
+ let rate = this.#presenceRate.get(scopeKey);
390
+ if (rate === undefined) {
391
+ rate = { lastFanoutMs: this.#clock() };
392
+ this.#presenceRate.set(scopeKey, rate);
393
+ } else {
394
+ rate.lastFanoutMs = this.#clock();
395
+ }
396
+ this.#hub.fanoutPresence(this, scopeKey, kind, doc);
397
+ }
398
+
399
+ /** §8.6.1/§8.6.3: clear this session's document for a key and fan a
400
+ * leave to the remaining peers. */
401
+ #leavePresence(scopeKey: string): void {
402
+ const existed = this.#hub.presence.clear(this.partition, scopeKey, this);
403
+ const rate = this.#presenceRate.get(scopeKey);
404
+ if (rate?.timer !== undefined) clearTimeout(rate.timer);
405
+ this.#presenceRate.delete(scopeKey);
406
+ if (existed) this.#hub.fanoutPresence(this, scopeKey, 'leave', null);
407
+ }
408
+
409
+ /** Deliver a fanout event to this session (called by the hub for peers,
410
+ * §8.6.3 receive authorization checked by the caller). */
411
+ receivePresence(
412
+ scopeKey: string,
413
+ kind: PresenceKind,
414
+ actorId: string,
415
+ clientId: string,
416
+ doc: PresenceDoc | null,
417
+ ): void {
418
+ this.#sendSafe(
419
+ encodePresenceFanout(
420
+ scopeKey,
421
+ kind,
422
+ actorId,
423
+ clientId,
424
+ doc,
425
+ this.#clock(),
426
+ ),
427
+ );
428
+ }
429
+
430
+ /** The scope keys this connection currently holds (§8.6.3). */
431
+ #currentScopeKeys(): Set<string> {
432
+ return this.#hub.scopeKeysOf(this.registrations);
433
+ }
434
+
435
+ /** §8.6.3: drop presence on keys the connection no longer holds and
436
+ * deliver the snapshot on keys it newly holds. Called after a
437
+ * registration change (§8.7 round end, §8.1 reconnect handled by
438
+ * connect's snapshot). */
439
+ reconcilePresence(previousKeys: ReadonlySet<string>): void {
440
+ const currentKeys = this.#currentScopeKeys();
441
+ // Keys lost: leave my own published docs; presence delivery stops
442
+ // naturally (peers no longer fan to me — I am not registered).
443
+ for (const key of previousKeys) {
444
+ if (!currentKeys.has(key)) this.#leavePresence(key);
445
+ }
446
+ // Keys gained: deliver the snapshot of already-present peers.
447
+ for (const key of currentKeys) {
448
+ if (!previousKeys.has(key)) this.#deliverSnapshot(key);
449
+ }
450
+ }
451
+
452
+ /** §8.6.4 snapshot: a burst of `join`s for peers already present on a
453
+ * key, delivered to this session only. */
454
+ #deliverSnapshot(scopeKey: string): void {
455
+ for (const peer of this.#hub.presence.snapshot(
456
+ this.partition,
457
+ scopeKey,
458
+ this,
459
+ )) {
460
+ this.receivePresence(
461
+ scopeKey,
462
+ 'join',
463
+ peer.session.actorId,
464
+ peer.session.clientId,
465
+ peer.doc,
466
+ );
467
+ }
468
+ }
469
+
470
+ /** §8.6.1: on disconnect, leave every key this session was present on. */
471
+ dropAllPresence(): void {
472
+ for (const key of this.#hub.presence.keysOf(this.partition, this)) {
473
+ this.#leavePresence(key);
474
+ }
475
+ for (const rate of this.#presenceRate.values()) {
476
+ if (rate.timer !== undefined) clearTimeout(rate.timer);
477
+ }
478
+ this.#presenceRate.clear();
479
+ }
480
+
481
+ /** Snapshot delivery on connect (§8.6.4) — the newly-registered
482
+ * connection sees who is already present on each of its keys. */
483
+ deliverInitialPresence(): void {
484
+ for (const key of this.#currentScopeKeys()) this.#deliverSnapshot(key);
485
+ }
486
+
487
+ /**
488
+ * Feed an inbound binary frame: a `0x01`-tagged chunk of the sync
489
+ * round's request byte stream (§8.7). Synchronous entry — assembly and
490
+ * violation detection happen inline so a pipelined chunk arriving
491
+ * while a response streams is caught deterministically; the round
492
+ * itself runs async once the request is complete.
493
+ */
494
+ handleBinary(bytes: Uint8Array): void {
495
+ if (bytes.length === 0) return;
496
+ if (bytes[0] !== REALTIME_TAG_ROUND) {
497
+ // §8.7: client→server tags other than 0x01 — a broken client.
498
+ this.#violation(`unexpected channel tag 0x${bytes[0]?.toString(16)}`);
499
+ return;
500
+ }
501
+ if (this.#activeRound !== undefined) {
502
+ // §8.7 one round in flight: MUST NOT process, drop the connection.
503
+ this.#violation('sync round begun while a response stream is in flight');
504
+ return;
505
+ }
506
+ const chunk = bytes.subarray(1);
507
+ this.#requestScanner ??= new MessageStreamScanner();
508
+ let done: ReturnType<MessageStreamScanner['push']>;
509
+ try {
510
+ done = this.#requestScanner.push(chunk);
511
+ } catch (error) {
512
+ // §8.7: an unframed stream has no findable end — connection-fatal.
513
+ this.#requestScanner = undefined;
514
+ this.#violation(
515
+ error instanceof DecodeError
516
+ ? error.message
517
+ : 'malformed request stream',
518
+ );
519
+ return;
520
+ }
521
+ if (done === undefined) return;
522
+ this.#requestScanner = undefined;
523
+ if (done.excess > 0) {
524
+ this.#violation('request bytes past END (pipelining, §8.7)');
525
+ return;
526
+ }
527
+ const token = Symbol('round');
528
+ this.#activeRound = token;
529
+ void this.#runRound(done.message.slice(), token);
530
+ }
531
+
532
+ /** Drive the shared handler and stream the response back (§8.7). */
533
+ async #runRound(requestBytes: Uint8Array, token: symbol): Promise<void> {
534
+ const finishRound = (): void => {
535
+ if (this.#activeRound === token) this.#activeRound = undefined;
536
+ };
537
+ try {
538
+ let stream: AsyncIterable<Uint8Array> | undefined;
539
+ try {
540
+ // §8.7: the round's clientId must match the connection's —
541
+ // registration identity would otherwise be ambiguous.
542
+ const decoded = decodeMessage(requestBytes);
543
+ const header = decoded.frames[0];
544
+ if (decoded.msgKind !== 'request' || header?.type !== 'REQ_HEADER') {
545
+ throw syncError('sync.invalid_request', 'expected a request message');
546
+ }
547
+ if (header.clientId !== this.clientId) {
548
+ throw syncError(
549
+ 'sync.invalid_client_id',
550
+ 'socket round clientId must match the connection (§8.7)',
551
+ );
552
+ }
553
+ stream = await createSyncResponseStream(
554
+ requestBytes,
555
+ this.#hub.requestContext(this),
556
+ );
557
+ } catch (error) {
558
+ // §8.7 failures: what HTTP reports as status+JSON becomes a
559
+ // minimal RESP_HEADER/ERROR/END response stream on the socket.
560
+ if (error instanceof DecodeError || error instanceof SyncError) {
561
+ const sync =
562
+ error instanceof SyncError
563
+ ? error
564
+ : syncError(error.code, error.message);
565
+ finishRound(); // END is in this one chunk
566
+ await this.#sendRoundChunk(errorResponseBytes(sync));
567
+ return;
568
+ }
569
+ throw error;
570
+ }
571
+ // Fetch-ahead so the round stops being "in flight" the moment the
572
+ // chunk carrying END is handed to send: the client may legally
573
+ // begin its next round as soon as END *arrives* (§8.7 rule 3), so
574
+ // in-flight must end with the send, not with generator cleanup.
575
+ const iterator = stream[Symbol.asyncIterator]();
576
+ let step = await iterator.next();
577
+ while (!step.done) {
578
+ const chunk = step.value;
579
+ step = await iterator.next();
580
+ if (step.done) finishRound();
581
+ await this.#sendRoundChunk(chunk);
582
+ }
583
+ } catch {
584
+ // A host failure mid-stream leaves the byte stream unfinishable —
585
+ // fail loud, drop the connection (§8.7 / §1.4 abort rule).
586
+ this.#violation('sync round failed mid-stream');
587
+ } finally {
588
+ finishRound();
589
+ // §8.7 registration at round end: reload the persisted list — it
590
+ // only advances on success, so failed rounds change nothing.
591
+ await this.#refreshRegistrations();
592
+ }
593
+ }
594
+
595
+ async #sendRoundChunk(chunk: Uint8Array): Promise<void> {
596
+ const tagged = new Uint8Array(chunk.length + 1);
597
+ tagged[0] = REALTIME_TAG_ROUND;
598
+ tagged.set(chunk, 1);
599
+ await this.#send(tagged);
600
+ }
601
+
602
+ async #refreshRegistrations(): Promise<void> {
603
+ // §8.6.3: a registration change re-derives the presence grant — capture
604
+ // the keys before, reconcile after (leaves on lost keys, snapshots on
605
+ // gained keys).
606
+ const previousKeys = this.#currentScopeKeys();
607
+ try {
608
+ this.registrations = await this.#hub.loadRegistrations(
609
+ this.partition,
610
+ this.actorId,
611
+ this.clientId,
612
+ );
613
+ } catch {
614
+ // Fail closed: an unreadable record or resolver failure leaves the
615
+ // previous registrations in place; the next round repairs it.
616
+ return;
617
+ }
618
+ this.reconcilePresence(previousKeys);
619
+ }
620
+
621
+ /** §8.7 protocol violation: drop the connection, fail loud. The
622
+ * message is intentionally unsent — there is no error channel for an
623
+ * unframed stream; `realtime.closed` fires via `disconnect`. */
624
+ #violation(_message: string): void {
625
+ this.#closeSocket?.();
626
+ this.#hub.disconnect(this);
627
+ }
628
+
629
+ async #persistCursor(): Promise<void> {
630
+ try {
631
+ const record = await this.#storage.getClientRecord(
632
+ this.partition,
633
+ this.clientId,
634
+ );
635
+ if (record === undefined) return;
636
+ await this.#storage.putClientRecord(this.partition, {
637
+ ...record,
638
+ cursor: Math.max(record.cursor, this.cursor),
639
+ updatedAtMs: this.#clock(),
640
+ });
641
+ } catch {
642
+ // Cursor persistence is best-effort; the next pull repairs it.
643
+ }
644
+ }
645
+
646
+ /** Fire-and-forget send for control text and deltas; a host send
647
+ * failure surfaces on the socket, not here. */
648
+ #sendSafe(data: string | Uint8Array): void {
649
+ try {
650
+ const result = this.#send(data);
651
+ if (result instanceof Promise) result.catch(() => {});
652
+ } catch {
653
+ // socket already gone; close handling is the host's job
654
+ }
655
+ }
656
+
657
+ sendHeartbeat(): void {
658
+ this.#sendSafe(
659
+ JSON.stringify({
660
+ event: 'heartbeat',
661
+ data: { timestamp: this.#clock() },
662
+ }),
663
+ );
664
+ }
665
+
666
+ sendWake(reason: WakeReason): void {
667
+ this.wakePending = true;
668
+ this.#sendSafe(
669
+ JSON.stringify({
670
+ event: 'sync',
671
+ data: {
672
+ cursor: this.lastKnownSeq,
673
+ requiresPull: true,
674
+ reason,
675
+ timestamp: this.#clock(),
676
+ },
677
+ }),
678
+ );
679
+ const events = this.#events;
680
+ if (events !== undefined) {
681
+ emitEvent(events, {
682
+ type: 'realtime.wake',
683
+ atMs: this.#clock(),
684
+ partition: this.partition,
685
+ actorId: this.actorId,
686
+ clientId: this.clientId,
687
+ sessionId: this.sessionId,
688
+ reason,
689
+ });
690
+ }
691
+ }
692
+
693
+ /** Called by the hub for every applied commit, in commitSeq order. */
694
+ deliverCommit(commit: StoredCommit): void {
695
+ this.lastKnownSeq = Math.max(this.lastKnownSeq, commit.commitSeq);
696
+ const sections: Array<{
697
+ registration: Registration;
698
+ changes: CommitChange[];
699
+ }> = [];
700
+ for (const registration of this.registrations) {
701
+ const changes = commit.changes
702
+ .filter(
703
+ (change) =>
704
+ change.table === registration.table &&
705
+ matchesEffective(change.scopes, registration.effective),
706
+ )
707
+ .map(
708
+ (change): CommitChange => ({
709
+ tableIndex: 0,
710
+ rowId: change.rowId,
711
+ op: change.op,
712
+ ...(change.rowVersion !== undefined
713
+ ? { rowVersion: change.rowVersion }
714
+ : {}),
715
+ scopes: change.scopes,
716
+ ...(change.payload !== undefined ? { row: change.payload } : {}),
717
+ }),
718
+ );
719
+ if (changes.length > 0) sections.push({ registration, changes });
720
+ }
721
+ if (sections.length === 0) return;
722
+ if (this.#activeRound !== undefined) {
723
+ // §8.7 interleaving: no 0x00 messages while a response stream is
724
+ // in flight — the §8.2 suppression path takes over (the client's
725
+ // post-round ack lifts it when the round covered this commit).
726
+ this.sendWake('catchup-required');
727
+ return;
728
+ }
729
+ if (this.wakePending) {
730
+ // §8.2: deltas must be cursor-contiguous; while the client is behind
731
+ // we send (coalescible) wake-ups instead of a delta past its cursor.
732
+ this.sendWake('catchup-required');
733
+ return;
734
+ }
735
+ const frames: ResponseFrame[] = [{ type: 'RESP_HEADER' }];
736
+ for (const section of sections) {
737
+ frames.push({
738
+ type: 'SUB_START',
739
+ id: section.registration.id,
740
+ status: 'active',
741
+ reasonCode: '',
742
+ effectiveScopes: section.registration.effective,
743
+ bootstrap: false,
744
+ });
745
+ frames.push({
746
+ type: 'COMMIT',
747
+ commitSeq: commit.commitSeq,
748
+ createdAtMs: commit.createdAtMs,
749
+ actorId: commit.actorId,
750
+ tables: [section.registration.table],
751
+ changes: section.changes,
752
+ });
753
+ frames.push({ type: 'SUB_END', nextCursor: commit.commitSeq });
754
+ }
755
+ const bytes = encodeMessage({
756
+ wireVersion: PROTOCOL_WIRE_VERSION,
757
+ msgKind: 'response',
758
+ frames,
759
+ });
760
+ if (bytes.length > this.#maxDeltaBytes) {
761
+ this.sendWake('delta-too-large');
762
+ return;
763
+ }
764
+ // §8.7: standalone deltas carry channel tag 0x00.
765
+ const tagged = new Uint8Array(bytes.length + 1);
766
+ tagged[0] = REALTIME_TAG_DELTA;
767
+ tagged.set(bytes, 1);
768
+ this.#sendSafe(tagged);
769
+ this.cursor = commit.commitSeq;
770
+ const events = this.#events;
771
+ if (events !== undefined) {
772
+ emitEvent(events, {
773
+ type: 'realtime.delta',
774
+ atMs: this.#clock(),
775
+ partition: this.partition,
776
+ actorId: this.actorId,
777
+ clientId: this.clientId,
778
+ sessionId: this.sessionId,
779
+ commitSeq: commit.commitSeq,
780
+ bytes: bytes.length,
781
+ changes: sections.reduce((n, s) => n + s.changes.length, 0),
782
+ });
783
+ }
784
+ }
785
+
786
+ close(): void {
787
+ this.#hub.disconnect(this);
788
+ }
789
+ }
790
+
791
+ export class RealtimeHub {
792
+ readonly #config: RealtimeHubConfig;
793
+ readonly #schema: CompiledSchema;
794
+ readonly #sessions = new Set<RealtimeSession>();
795
+ /** §8.6 presence registry — ephemeral, in-memory, never persisted. */
796
+ readonly presence = new PresenceRegistry();
797
+
798
+ constructor(config: RealtimeHubConfig) {
799
+ this.#config = config;
800
+ this.#schema = compileSchema(config.schema);
801
+ }
802
+
803
+ get sessionCount(): number {
804
+ return this.#sessions.size;
805
+ }
806
+
807
+ /** §8.6.2 published-document size cap (bytes). */
808
+ get maxPresenceBytes(): number {
809
+ return this.#config.maxPresenceBytes ?? DEFAULT_MAX_PRESENCE_BYTES;
810
+ }
811
+
812
+ /** §8.6.4 presence rate cap (ms); 0 = off (reference default). */
813
+ get presenceMinIntervalMs(): number {
814
+ return this.#config.presenceMinIntervalMs ?? 0;
815
+ }
816
+
817
+ /** §8.6.3: the scope keys a set of registrations covers. */
818
+ scopeKeysOf(registrations: readonly Registration[]): Set<string> {
819
+ return registrationScopeKeys(registrations, this.#schema);
820
+ }
821
+
822
+ /**
823
+ * §8.6.3 fanout: deliver a presence change to every OTHER session
824
+ * registered on the key (the privacy floor — only current scope-mates).
825
+ * The publisher does not receive its own fanout.
826
+ */
827
+ fanoutPresence(
828
+ origin: RealtimeSession,
829
+ scopeKey: string,
830
+ kind: PresenceKind,
831
+ doc: PresenceDoc | null,
832
+ ): void {
833
+ for (const session of this.#sessions) {
834
+ if (session === origin) continue;
835
+ if (session.partition !== origin.partition) continue;
836
+ // Receive authorization (§8.6.3): the receiver must itself hold the key.
837
+ if (!this.scopeKeysOf(session.registrations).has(scopeKey)) continue;
838
+ session.receivePresence(
839
+ scopeKey,
840
+ kind,
841
+ origin.actorId,
842
+ origin.clientId,
843
+ doc,
844
+ );
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Resolve the client record's subscription list into per-connection
850
+ * registrations (§8.1 at upgrade; §8.7 at socket-round end).
851
+ */
852
+ async loadRegistrations(
853
+ partition: string,
854
+ actorId: string,
855
+ clientId: string,
856
+ ): Promise<Registration[]> {
857
+ const record = await this.#config.storage.getClientRecord(
858
+ partition,
859
+ clientId,
860
+ );
861
+ let resolved: ResolvedScopes;
862
+ try {
863
+ const allowed = await this.#config.resolveScopes({
864
+ partition,
865
+ actorId,
866
+ clientId,
867
+ });
868
+ // §7.3.3: delta-fanout registration needs live scopes — an outage
869
+ // registers nothing (the client's next socket round, §8.7, is
870
+ // lease-aware and re-registers at round end, §8.1).
871
+ resolved =
872
+ allowed === RESOLVER_OUTAGE ? { ok: false } : { ok: true, allowed };
873
+ } catch (error) {
874
+ const events = this.#config.events;
875
+ if (events !== undefined) {
876
+ const clock = this.#config.clock ?? Date.now;
877
+ emitEvent(events, {
878
+ type: 'scopes.resolve_failed',
879
+ atMs: clock(),
880
+ partition,
881
+ actorId,
882
+ phase: 'realtime',
883
+ message: error instanceof Error ? error.message : String(error),
884
+ });
885
+ }
886
+ resolved = { ok: false };
887
+ }
888
+ const registrations: Registration[] = [];
889
+ for (const subscription of record?.subscriptions ?? []) {
890
+ const table = this.#schema.tables.get(subscription.table);
891
+ if (table === undefined) continue;
892
+ const keysValid = Object.keys(subscription.scopes).every((key) =>
893
+ table.declaredVariables.has(key),
894
+ );
895
+ if (!keysValid) continue;
896
+ const outcome = computeEffective(subscription.scopes, resolved);
897
+ if (outcome.status !== 'active') continue;
898
+ registrations.push({
899
+ id: subscription.id,
900
+ table: subscription.table,
901
+ effective: outcome.effective,
902
+ });
903
+ }
904
+ return registrations;
905
+ }
906
+
907
+ /**
908
+ * Build the per-round request context for a socket sync round (§8.7):
909
+ * the same shape the HTTP adapter builds, so the round drives the
910
+ * SAME handler with zero semantic divergence.
911
+ */
912
+ requestContext(session: RealtimeSession): SyncRequestContext {
913
+ const segments = this.#config.segments;
914
+ if (segments === undefined) {
915
+ // Fail loud (§8.7): a hub serving socket rounds needs the same
916
+ // segment store the HTTP binding uses — never a degraded round.
917
+ throw syncError(
918
+ 'sync.invalid_request',
919
+ 'socket sync rounds require a segment store on the realtime hub (§8.7)',
920
+ );
921
+ }
922
+ return {
923
+ partition: session.partition,
924
+ actorId: session.actorId,
925
+ schema: this.#config.schema,
926
+ storage: this.#config.storage,
927
+ segments,
928
+ resolveScopes: this.#config.resolveScopes,
929
+ ...(this.#config.clock !== undefined
930
+ ? { clock: this.#config.clock }
931
+ : {}),
932
+ ...(this.#config.limits !== undefined
933
+ ? { limits: this.#config.limits }
934
+ : {}),
935
+ ...(this.#config.signedUrls !== undefined
936
+ ? { signedUrls: this.#config.signedUrls }
937
+ : {}),
938
+ ...(this.#config.leases !== undefined
939
+ ? { leases: this.#config.leases }
940
+ : {}),
941
+ ...(this.#config.events !== undefined
942
+ ? { events: this.#config.events }
943
+ : {}),
944
+ realtime: this,
945
+ };
946
+ }
947
+
948
+ /**
949
+ * Register a connected socket (§8.1): load the client's last pull's
950
+ * subscription list, resolve + intersect scopes, send `hello`.
951
+ */
952
+ async connect(options: RealtimeConnectOptions): Promise<RealtimeSession> {
953
+ const { storage } = this.#config;
954
+ const clock = this.#config.clock ?? Date.now;
955
+ const record = await storage.getClientRecord(
956
+ options.partition,
957
+ options.clientId,
958
+ );
959
+ if (record !== undefined && record.actorId !== options.actorId) {
960
+ throw syncError(
961
+ 'sync.invalid_client_id',
962
+ 'clientId is bound to a different actor in this partition (§1.5)',
963
+ );
964
+ }
965
+ const registrations = await this.loadRegistrations(
966
+ options.partition,
967
+ options.actorId,
968
+ options.clientId,
969
+ );
970
+ const latestSeq = await storage.getMaxCommitSeq(options.partition);
971
+ const cursor = record?.cursor ?? -1;
972
+ const session = new RealtimeSession(
973
+ this,
974
+ options,
975
+ registrations,
976
+ cursor,
977
+ latestSeq,
978
+ clock,
979
+ this.#config.maxDeltaBytes ?? DEFAULT_MAX_DELTA_BYTES,
980
+ storage,
981
+ this.#config.events,
982
+ );
983
+ this.#sessions.add(session);
984
+ const helloResult = options.send(
985
+ JSON.stringify({
986
+ event: 'hello',
987
+ data: {
988
+ protocolVersion: 1,
989
+ sessionId: session.sessionId,
990
+ actorId: options.actorId,
991
+ clientId: options.clientId,
992
+ cursor,
993
+ latestCursor: latestSeq,
994
+ requiresSync: record === undefined || cursor < latestSeq,
995
+ timestamp: clock(),
996
+ },
997
+ }),
998
+ );
999
+ if (helloResult instanceof Promise) helloResult.catch(() => {});
1000
+ // §8.6.4: the newly-registered connection sees who is already present
1001
+ // on each key it holds (a burst of joins, after hello).
1002
+ session.deliverInitialPresence();
1003
+ const events = this.#config.events;
1004
+ if (events !== undefined) {
1005
+ emitEvent(events, {
1006
+ type: 'realtime.opened',
1007
+ atMs: clock(),
1008
+ partition: options.partition,
1009
+ actorId: options.actorId,
1010
+ clientId: options.clientId,
1011
+ sessionId: session.sessionId,
1012
+ registrations: registrations.length,
1013
+ cursor,
1014
+ latestSeq,
1015
+ });
1016
+ }
1017
+ return session;
1018
+ }
1019
+
1020
+ disconnect(session: RealtimeSession): void {
1021
+ if (!this.#sessions.delete(session)) return;
1022
+ // §8.6.1: lost on disconnect ⇒ leave emitted for every held key. Done
1023
+ // AFTER removing from the set so the leaving session is not fanned to.
1024
+ session.dropAllPresence();
1025
+ const events = this.#config.events;
1026
+ if (events !== undefined) {
1027
+ const clock = this.#config.clock ?? Date.now;
1028
+ const now = clock();
1029
+ emitEvent(events, {
1030
+ type: 'realtime.closed',
1031
+ atMs: now,
1032
+ partition: session.partition,
1033
+ actorId: session.actorId,
1034
+ clientId: session.clientId,
1035
+ sessionId: session.sessionId,
1036
+ durationMs: now - session.openedAtMs,
1037
+ });
1038
+ }
1039
+ }
1040
+
1041
+ /** RealtimeNotifier: fan an applied commit out to matching sessions. */
1042
+ async notifyCommit(partition: string, commit: StoredCommit): Promise<void> {
1043
+ for (const session of this.#sessions) {
1044
+ if (session.partition !== partition) continue;
1045
+ session.deliverCommit(commit);
1046
+ }
1047
+ }
1048
+
1049
+ /** Broadcast a wake-up (host-initiated resync, schema rollover, §8.3). */
1050
+ wake(partition: string, reason: WakeReason): void {
1051
+ for (const session of this.#sessions) {
1052
+ if (session.partition !== partition) continue;
1053
+ session.sendWake(reason);
1054
+ }
1055
+ }
1056
+ }
1057
+
1058
+ export function createRealtimeHub(config: RealtimeHubConfig): RealtimeHub {
1059
+ return new RealtimeHub(config);
1060
+ }
1061
+
1062
+ /** §8.7 failures: the socket has no HTTP status surface, so a
1063
+ * request-level failure becomes a minimal RESP_HEADER/ERROR/END
1064
+ * response message delivered as the round's response stream. */
1065
+ function errorResponseBytes(error: SyncError): Uint8Array {
1066
+ return encodeMessage({
1067
+ wireVersion: PROTOCOL_WIRE_VERSION,
1068
+ msgKind: 'response',
1069
+ frames: [
1070
+ { type: 'RESP_HEADER' },
1071
+ {
1072
+ type: 'ERROR',
1073
+ code: error.code,
1074
+ message: error.message,
1075
+ category: error.category,
1076
+ retryable: error.retryable,
1077
+ recommendedAction: error.recommendedAction,
1078
+ ...(error.details !== undefined ? { details: error.details } : {}),
1079
+ },
1080
+ ],
1081
+ });
1082
+ }