@livestore/common 0.4.0-dev.8 → 0.4.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 (518) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts +27 -12
  3. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
  4. package/dist/ClientSessionLeaderThreadProxy.js.map +1 -1
  5. package/dist/WorkerTransportError.d.ts +11 -0
  6. package/dist/WorkerTransportError.d.ts.map +1 -0
  7. package/dist/WorkerTransportError.js +11 -0
  8. package/dist/WorkerTransportError.js.map +1 -0
  9. package/dist/adapter-types.d.ts +37 -7
  10. package/dist/adapter-types.d.ts.map +1 -1
  11. package/dist/adapter-types.js +27 -1
  12. package/dist/adapter-types.js.map +1 -1
  13. package/dist/bounded-collections.d.ts.map +1 -1
  14. package/dist/bounded-collections.js +6 -4
  15. package/dist/bounded-collections.js.map +1 -1
  16. package/dist/debug-info.d.ts.map +1 -1
  17. package/dist/debug-info.js +33 -6
  18. package/dist/debug-info.js.map +1 -1
  19. package/dist/devtools/devtools-compatibility.test.d.ts +2 -0
  20. package/dist/devtools/devtools-compatibility.test.d.ts.map +1 -0
  21. package/dist/devtools/devtools-compatibility.test.js +15 -0
  22. package/dist/devtools/devtools-compatibility.test.js.map +1 -0
  23. package/dist/devtools/devtools-messages-client-session.d.ts +55 -24
  24. package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
  25. package/dist/devtools/devtools-messages-client-session.js +22 -5
  26. package/dist/devtools/devtools-messages-client-session.js.map +1 -1
  27. package/dist/devtools/devtools-messages-common.d.ts +11 -14
  28. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  29. package/dist/devtools/devtools-messages-common.js +7 -9
  30. package/dist/devtools/devtools-messages-common.js.map +1 -1
  31. package/dist/devtools/devtools-messages-leader.d.ts +65 -30
  32. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  33. package/dist/devtools/devtools-messages-leader.js +29 -11
  34. package/dist/devtools/devtools-messages-leader.js.map +1 -1
  35. package/dist/devtools/devtools-sessioninfo.d.ts +14 -2
  36. package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -1
  37. package/dist/devtools/devtools-sessioninfo.js +7 -4
  38. package/dist/devtools/devtools-sessioninfo.js.map +1 -1
  39. package/dist/devtools/mod.d.ts +13 -2
  40. package/dist/devtools/mod.d.ts.map +1 -1
  41. package/dist/devtools/mod.js +10 -3
  42. package/dist/devtools/mod.js.map +1 -1
  43. package/dist/errors.d.ts +48 -18
  44. package/dist/errors.d.ts.map +1 -1
  45. package/dist/errors.js +20 -12
  46. package/dist/errors.js.map +1 -1
  47. package/dist/index.d.ts +4 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/leader-thread/LeaderSyncProcessor.d.ts +53 -6
  52. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  53. package/dist/leader-thread/LeaderSyncProcessor.js +325 -257
  54. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  55. package/dist/leader-thread/RejectedPushError.d.ts +107 -0
  56. package/dist/leader-thread/RejectedPushError.d.ts.map +1 -0
  57. package/dist/leader-thread/RejectedPushError.js +78 -0
  58. package/dist/leader-thread/RejectedPushError.js.map +1 -0
  59. package/dist/leader-thread/connection.js +1 -1
  60. package/dist/leader-thread/connection.js.map +1 -1
  61. package/dist/leader-thread/eventlog.d.ts +19 -14
  62. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  63. package/dist/leader-thread/eventlog.js +78 -18
  64. package/dist/leader-thread/eventlog.js.map +1 -1
  65. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -2
  66. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  67. package/dist/leader-thread/leader-worker-devtools.js +90 -58
  68. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  69. package/dist/leader-thread/make-leader-thread-layer.d.ts +15 -7
  70. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  71. package/dist/leader-thread/make-leader-thread-layer.js +49 -17
  72. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  73. package/dist/leader-thread/make-leader-thread-layer.test.d.ts +2 -0
  74. package/dist/leader-thread/make-leader-thread-layer.test.d.ts.map +1 -0
  75. package/dist/leader-thread/make-leader-thread-layer.test.js +32 -0
  76. package/dist/leader-thread/make-leader-thread-layer.test.js.map +1 -0
  77. package/dist/leader-thread/materialize-event.d.ts +1 -1
  78. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  79. package/dist/leader-thread/materialize-event.js +28 -9
  80. package/dist/leader-thread/materialize-event.js.map +1 -1
  81. package/dist/leader-thread/mod.d.ts +1 -0
  82. package/dist/leader-thread/mod.d.ts.map +1 -1
  83. package/dist/leader-thread/mod.js +1 -0
  84. package/dist/leader-thread/mod.js.map +1 -1
  85. package/dist/leader-thread/recreate-db.d.ts +2 -2
  86. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  87. package/dist/leader-thread/recreate-db.js +6 -6
  88. package/dist/leader-thread/recreate-db.js.map +1 -1
  89. package/dist/leader-thread/shutdown-channel.d.ts +2 -2
  90. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  91. package/dist/leader-thread/shutdown-channel.js +2 -2
  92. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  93. package/dist/leader-thread/stream-events.d.ts +56 -0
  94. package/dist/leader-thread/stream-events.d.ts.map +1 -0
  95. package/dist/leader-thread/stream-events.js +167 -0
  96. package/dist/leader-thread/stream-events.js.map +1 -0
  97. package/dist/leader-thread/types.d.ts +95 -17
  98. package/dist/leader-thread/types.d.ts.map +1 -1
  99. package/dist/leader-thread/types.js +13 -0
  100. package/dist/leader-thread/types.js.map +1 -1
  101. package/dist/logging.d.ts +40 -0
  102. package/dist/logging.d.ts.map +1 -0
  103. package/dist/logging.js +33 -0
  104. package/dist/logging.js.map +1 -0
  105. package/dist/make-client-session.d.ts +5 -3
  106. package/dist/make-client-session.d.ts.map +1 -1
  107. package/dist/make-client-session.js +7 -4
  108. package/dist/make-client-session.js.map +1 -1
  109. package/dist/materializer-helper.d.ts +6 -6
  110. package/dist/materializer-helper.d.ts.map +1 -1
  111. package/dist/materializer-helper.js +18 -8
  112. package/dist/materializer-helper.js.map +1 -1
  113. package/dist/otel.d.ts +2 -1
  114. package/dist/otel.d.ts.map +1 -1
  115. package/dist/otel.js +7 -2
  116. package/dist/otel.js.map +1 -1
  117. package/dist/rematerialize-from-eventlog.d.ts +3 -3
  118. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  119. package/dist/rematerialize-from-eventlog.js +40 -29
  120. package/dist/rematerialize-from-eventlog.js.map +1 -1
  121. package/dist/schema/EventDef/define.d.ts +161 -0
  122. package/dist/schema/EventDef/define.d.ts.map +1 -0
  123. package/dist/schema/EventDef/define.js +140 -0
  124. package/dist/schema/EventDef/define.js.map +1 -0
  125. package/dist/schema/EventDef/deprecated.d.ts +99 -0
  126. package/dist/schema/EventDef/deprecated.d.ts.map +1 -0
  127. package/dist/schema/EventDef/deprecated.js +144 -0
  128. package/dist/schema/EventDef/deprecated.js.map +1 -0
  129. package/dist/schema/EventDef/deprecated.test.d.ts +2 -0
  130. package/dist/schema/EventDef/deprecated.test.d.ts.map +1 -0
  131. package/dist/schema/EventDef/deprecated.test.js +95 -0
  132. package/dist/schema/EventDef/deprecated.test.js.map +1 -0
  133. package/dist/schema/EventDef/event-def.d.ts +110 -0
  134. package/dist/schema/EventDef/event-def.d.ts.map +1 -0
  135. package/dist/schema/EventDef/event-def.js +2 -0
  136. package/dist/schema/EventDef/event-def.js.map +1 -0
  137. package/dist/schema/EventDef/facts.d.ts +118 -0
  138. package/dist/schema/EventDef/facts.d.ts.map +1 -0
  139. package/dist/schema/EventDef/facts.js +53 -0
  140. package/dist/schema/EventDef/facts.js.map +1 -0
  141. package/dist/schema/EventDef/materializer.d.ts +155 -0
  142. package/dist/schema/EventDef/materializer.d.ts.map +1 -0
  143. package/dist/schema/EventDef/materializer.js +83 -0
  144. package/dist/schema/EventDef/materializer.js.map +1 -0
  145. package/dist/schema/EventDef/mod.d.ts +6 -0
  146. package/dist/schema/EventDef/mod.d.ts.map +1 -0
  147. package/dist/schema/EventDef/mod.js +6 -0
  148. package/dist/schema/EventDef/mod.js.map +1 -0
  149. package/dist/schema/EventSequenceNumber/client.d.ts +136 -0
  150. package/dist/schema/EventSequenceNumber/client.d.ts.map +1 -0
  151. package/dist/schema/EventSequenceNumber/client.js +193 -0
  152. package/dist/schema/EventSequenceNumber/client.js.map +1 -0
  153. package/dist/schema/EventSequenceNumber/global.d.ts +15 -0
  154. package/dist/schema/EventSequenceNumber/global.d.ts.map +1 -0
  155. package/dist/schema/EventSequenceNumber/global.js +14 -0
  156. package/dist/schema/EventSequenceNumber/global.js.map +1 -0
  157. package/dist/schema/EventSequenceNumber/mod.d.ts +37 -0
  158. package/dist/schema/EventSequenceNumber/mod.d.ts.map +1 -0
  159. package/dist/schema/EventSequenceNumber/mod.js +37 -0
  160. package/dist/schema/EventSequenceNumber/mod.js.map +1 -0
  161. package/dist/schema/EventSequenceNumber.test.js +44 -44
  162. package/dist/schema/EventSequenceNumber.test.js.map +1 -1
  163. package/dist/schema/{LiveStoreEvent.d.ts → LiveStoreEvent/client.d.ts} +102 -111
  164. package/dist/schema/LiveStoreEvent/client.d.ts.map +1 -0
  165. package/dist/schema/LiveStoreEvent/client.js +176 -0
  166. package/dist/schema/LiveStoreEvent/client.js.map +1 -0
  167. package/dist/schema/LiveStoreEvent/client.test.d.ts +2 -0
  168. package/dist/schema/LiveStoreEvent/client.test.d.ts.map +1 -0
  169. package/dist/schema/LiveStoreEvent/client.test.js +111 -0
  170. package/dist/schema/LiveStoreEvent/client.test.js.map +1 -0
  171. package/dist/schema/LiveStoreEvent/for-event-def.d.ts +52 -0
  172. package/dist/schema/LiveStoreEvent/for-event-def.d.ts.map +1 -0
  173. package/dist/schema/LiveStoreEvent/for-event-def.js +2 -0
  174. package/dist/schema/LiveStoreEvent/for-event-def.js.map +1 -0
  175. package/dist/schema/LiveStoreEvent/global.d.ts +36 -0
  176. package/dist/schema/LiveStoreEvent/global.d.ts.map +1 -0
  177. package/dist/schema/LiveStoreEvent/global.js +31 -0
  178. package/dist/schema/LiveStoreEvent/global.js.map +1 -0
  179. package/dist/schema/LiveStoreEvent/input.d.ts +46 -0
  180. package/dist/schema/LiveStoreEvent/input.d.ts.map +1 -0
  181. package/dist/schema/LiveStoreEvent/input.js +26 -0
  182. package/dist/schema/LiveStoreEvent/input.js.map +1 -0
  183. package/dist/schema/LiveStoreEvent/mod.d.ts +5 -0
  184. package/dist/schema/LiveStoreEvent/mod.d.ts.map +1 -0
  185. package/dist/schema/LiveStoreEvent/mod.js +5 -0
  186. package/dist/schema/LiveStoreEvent/mod.js.map +1 -0
  187. package/dist/schema/events.d.ts +1 -1
  188. package/dist/schema/events.d.ts.map +1 -1
  189. package/dist/schema/events.js +1 -1
  190. package/dist/schema/events.js.map +1 -1
  191. package/dist/schema/mod.d.ts +6 -4
  192. package/dist/schema/mod.d.ts.map +1 -1
  193. package/dist/schema/mod.js +5 -4
  194. package/dist/schema/mod.js.map +1 -1
  195. package/dist/schema/schema.d.ts +16 -1
  196. package/dist/schema/schema.d.ts.map +1 -1
  197. package/dist/schema/schema.js +32 -4
  198. package/dist/schema/schema.js.map +1 -1
  199. package/dist/schema/state/sqlite/client-document-def.d.ts +2 -1
  200. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
  201. package/dist/schema/state/sqlite/client-document-def.js +36 -15
  202. package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
  203. package/dist/schema/state/sqlite/client-document-def.test.js +121 -2
  204. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
  205. package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
  206. package/dist/schema/state/sqlite/column-annotations.js +1 -1
  207. package/dist/schema/state/sqlite/column-annotations.js.map +1 -1
  208. package/dist/schema/state/sqlite/column-annotations.test.js +2 -2
  209. package/dist/schema/state/sqlite/column-annotations.test.js.map +1 -1
  210. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
  211. package/dist/schema/state/sqlite/column-def.js +96 -47
  212. package/dist/schema/state/sqlite/column-def.js.map +1 -1
  213. package/dist/schema/state/sqlite/column-def.test.js +51 -12
  214. package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
  215. package/dist/schema/state/sqlite/column-spec.d.ts.map +1 -1
  216. package/dist/schema/state/sqlite/column-spec.js +30 -12
  217. package/dist/schema/state/sqlite/column-spec.js.map +1 -1
  218. package/dist/schema/state/sqlite/column-spec.test.js +24 -15
  219. package/dist/schema/state/sqlite/column-spec.test.js.map +1 -1
  220. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +2 -2
  221. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  222. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts +16 -10
  223. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -1
  224. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +15 -4
  225. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
  226. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js +1 -1
  227. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js.map +1 -1
  228. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts +1 -1
  229. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  230. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +1 -1
  231. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
  232. package/dist/schema/state/sqlite/mod.d.ts +2 -2
  233. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  234. package/dist/schema/state/sqlite/mod.js +5 -7
  235. package/dist/schema/state/sqlite/mod.js.map +1 -1
  236. package/dist/schema/state/sqlite/query-builder/api.d.ts +51 -22
  237. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  238. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -1
  239. package/dist/schema/state/sqlite/query-builder/astToSql.js +99 -22
  240. package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -1
  241. package/dist/schema/state/sqlite/query-builder/impl.d.ts +1 -1
  242. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  243. package/dist/schema/state/sqlite/query-builder/impl.js +28 -15
  244. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  245. package/dist/schema/state/sqlite/query-builder/impl.test.js +231 -93
  246. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  247. package/dist/schema/state/sqlite/schema-helpers.d.ts +2 -2
  248. package/dist/schema/state/sqlite/schema-helpers.d.ts.map +1 -1
  249. package/dist/schema/state/sqlite/schema-helpers.js +24 -14
  250. package/dist/schema/state/sqlite/schema-helpers.js.map +1 -1
  251. package/dist/schema/state/sqlite/schema-helpers.test.d.ts +2 -0
  252. package/dist/schema/state/sqlite/schema-helpers.test.d.ts.map +1 -0
  253. package/dist/schema/state/sqlite/schema-helpers.test.js +36 -0
  254. package/dist/schema/state/sqlite/schema-helpers.test.js.map +1 -0
  255. package/dist/schema/state/sqlite/{system-tables.d.ts → system-tables/eventlog-tables.d.ts} +21 -450
  256. package/dist/schema/state/sqlite/system-tables/eventlog-tables.d.ts.map +1 -0
  257. package/dist/schema/state/sqlite/system-tables/eventlog-tables.js +54 -0
  258. package/dist/schema/state/sqlite/system-tables/eventlog-tables.js.map +1 -0
  259. package/dist/schema/state/sqlite/system-tables/mod.d.ts +3 -0
  260. package/dist/schema/state/sqlite/system-tables/mod.d.ts.map +1 -0
  261. package/dist/schema/state/sqlite/system-tables/mod.js +3 -0
  262. package/dist/schema/state/sqlite/system-tables/mod.js.map +1 -0
  263. package/dist/schema/state/sqlite/system-tables/state-tables.d.ts +456 -0
  264. package/dist/schema/state/sqlite/system-tables/state-tables.d.ts.map +1 -0
  265. package/dist/schema/state/sqlite/system-tables/state-tables.js +55 -0
  266. package/dist/schema/state/sqlite/system-tables/state-tables.js.map +1 -0
  267. package/dist/schema/state/sqlite/table-def.d.ts +5 -3
  268. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  269. package/dist/schema/state/sqlite/table-def.js +1 -1
  270. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  271. package/dist/schema/state/sqlite/table-def.test.js +92 -3
  272. package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
  273. package/dist/schema/unknown-events.d.ts +47 -0
  274. package/dist/schema/unknown-events.d.ts.map +1 -0
  275. package/dist/schema/unknown-events.js +69 -0
  276. package/dist/schema/unknown-events.js.map +1 -0
  277. package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.d.ts +2 -0
  278. package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.d.ts.map +1 -0
  279. package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js +73 -0
  280. package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js.map +1 -0
  281. package/dist/schema-management/common.js +2 -2
  282. package/dist/schema-management/common.js.map +1 -1
  283. package/dist/schema-management/migrations.d.ts +32 -2
  284. package/dist/schema-management/migrations.d.ts.map +1 -1
  285. package/dist/schema-management/migrations.js +38 -6
  286. package/dist/schema-management/migrations.js.map +1 -1
  287. package/dist/schema-management/validate-schema.d.ts +3 -3
  288. package/dist/schema-management/validate-schema.d.ts.map +1 -1
  289. package/dist/schema-management/validate-schema.js +2 -2
  290. package/dist/schema-management/validate-schema.js.map +1 -1
  291. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  292. package/dist/sql-queries/sql-queries.js +18 -6
  293. package/dist/sql-queries/sql-queries.js.map +1 -1
  294. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  295. package/dist/sql-queries/sql-query-builder.js.map +1 -1
  296. package/dist/sqlite-db-helper.js +3 -3
  297. package/dist/sqlite-db-helper.js.map +1 -1
  298. package/dist/sqlite-types.d.ts +5 -5
  299. package/dist/sqlite-types.d.ts.map +1 -1
  300. package/dist/sqlite-types.js.map +1 -1
  301. package/dist/sync/ClientSessionSyncProcessor.d.ts +12 -12
  302. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  303. package/dist/sync/ClientSessionSyncProcessor.js +99 -114
  304. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  305. package/dist/sync/errors.d.ts +0 -33
  306. package/dist/sync/errors.d.ts.map +1 -1
  307. package/dist/sync/errors.js +5 -22
  308. package/dist/sync/errors.js.map +1 -1
  309. package/dist/sync/index.d.ts +2 -0
  310. package/dist/sync/index.d.ts.map +1 -1
  311. package/dist/sync/index.js +2 -0
  312. package/dist/sync/index.js.map +1 -1
  313. package/dist/sync/mock-sync-backend.d.ts +10 -8
  314. package/dist/sync/mock-sync-backend.d.ts.map +1 -1
  315. package/dist/sync/mock-sync-backend.js +71 -69
  316. package/dist/sync/mock-sync-backend.js.map +1 -1
  317. package/dist/sync/next/compact-events.d.ts.map +1 -1
  318. package/dist/sync/next/compact-events.js +11 -12
  319. package/dist/sync/next/compact-events.js.map +1 -1
  320. package/dist/sync/next/facts.d.ts +5 -5
  321. package/dist/sync/next/facts.d.ts.map +1 -1
  322. package/dist/sync/next/facts.js +7 -8
  323. package/dist/sync/next/facts.js.map +1 -1
  324. package/dist/sync/next/history-dag-common.d.ts +54 -15
  325. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  326. package/dist/sync/next/history-dag-common.js +198 -9
  327. package/dist/sync/next/history-dag-common.js.map +1 -1
  328. package/dist/sync/next/history-dag.d.ts.map +1 -1
  329. package/dist/sync/next/history-dag.js +11 -11
  330. package/dist/sync/next/history-dag.js.map +1 -1
  331. package/dist/sync/next/rebase-events.d.ts +5 -5
  332. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  333. package/dist/sync/next/rebase-events.js +6 -6
  334. package/dist/sync/next/rebase-events.js.map +1 -1
  335. package/dist/sync/next/test/compact-events.calculator.test.js +2 -2
  336. package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -1
  337. package/dist/sync/next/test/compact-events.test.d.ts.map +1 -1
  338. package/dist/sync/next/test/compact-events.test.js +2 -2
  339. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  340. package/dist/sync/next/test/event-fixtures.d.ts +2 -2
  341. package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
  342. package/dist/sync/next/test/event-fixtures.js +11 -11
  343. package/dist/sync/next/test/event-fixtures.js.map +1 -1
  344. package/dist/sync/sync-backend-kv.d.ts +3 -3
  345. package/dist/sync/sync-backend-kv.d.ts.map +1 -1
  346. package/dist/sync/sync-backend-kv.js +3 -3
  347. package/dist/sync/sync-backend-kv.js.map +1 -1
  348. package/dist/sync/sync-backend.d.ts +33 -13
  349. package/dist/sync/sync-backend.d.ts.map +1 -1
  350. package/dist/sync/sync-backend.js +38 -1
  351. package/dist/sync/sync-backend.js.map +1 -1
  352. package/dist/sync/sync.d.ts +23 -2
  353. package/dist/sync/sync.d.ts.map +1 -1
  354. package/dist/sync/syncstate.d.ts +55 -55
  355. package/dist/sync/syncstate.d.ts.map +1 -1
  356. package/dist/sync/syncstate.js +80 -98
  357. package/dist/sync/syncstate.js.map +1 -1
  358. package/dist/sync/syncstate.test.js +221 -132
  359. package/dist/sync/syncstate.test.js.map +1 -1
  360. package/dist/sync/transport-chunking.d.ts +36 -0
  361. package/dist/sync/transport-chunking.d.ts.map +1 -0
  362. package/dist/sync/transport-chunking.js +56 -0
  363. package/dist/sync/transport-chunking.js.map +1 -0
  364. package/dist/sync/validate-push-payload.d.ts +2 -2
  365. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  366. package/dist/sync/validate-push-payload.js +4 -6
  367. package/dist/sync/validate-push-payload.js.map +1 -1
  368. package/dist/testing/event-factory.d.ts +68 -0
  369. package/dist/testing/event-factory.d.ts.map +1 -0
  370. package/dist/testing/event-factory.js +78 -0
  371. package/dist/testing/event-factory.js.map +1 -0
  372. package/dist/testing/mod.d.ts +2 -0
  373. package/dist/testing/mod.d.ts.map +1 -0
  374. package/dist/testing/mod.js +2 -0
  375. package/dist/testing/mod.js.map +1 -0
  376. package/dist/util.js +2 -2
  377. package/dist/util.js.map +1 -1
  378. package/dist/version.d.ts +24 -5
  379. package/dist/version.d.ts.map +1 -1
  380. package/dist/version.js +25 -8
  381. package/dist/version.js.map +1 -1
  382. package/package.json +69 -16
  383. package/src/ClientSessionLeaderThreadProxy.ts +27 -12
  384. package/src/WorkerTransportError.ts +12 -0
  385. package/src/adapter-types.ts +50 -7
  386. package/src/bounded-collections.ts +6 -5
  387. package/src/debug-info.ts +37 -6
  388. package/src/devtools/devtools-compatibility.test.ts +18 -0
  389. package/src/devtools/devtools-messages-client-session.ts +22 -4
  390. package/src/devtools/devtools-messages-common.ts +7 -12
  391. package/src/devtools/devtools-messages-leader.ts +29 -10
  392. package/src/devtools/devtools-sessioninfo.ts +8 -5
  393. package/src/devtools/mod.ts +11 -2
  394. package/src/errors.ts +32 -24
  395. package/src/index.ts +4 -1
  396. package/src/leader-thread/LeaderSyncProcessor.ts +523 -373
  397. package/src/leader-thread/RejectedPushError.ts +106 -0
  398. package/src/leader-thread/connection.ts +1 -1
  399. package/src/leader-thread/eventlog.ts +112 -39
  400. package/src/leader-thread/leader-worker-devtools.ts +201 -120
  401. package/src/leader-thread/make-leader-thread-layer.test.ts +44 -0
  402. package/src/leader-thread/make-leader-thread-layer.ts +125 -40
  403. package/src/leader-thread/materialize-event.ts +40 -10
  404. package/src/leader-thread/mod.ts +1 -0
  405. package/src/leader-thread/recreate-db.ts +7 -7
  406. package/src/leader-thread/shutdown-channel.ts +4 -8
  407. package/src/leader-thread/stream-events.ts +206 -0
  408. package/src/leader-thread/types.ts +68 -18
  409. package/src/logging.ts +62 -0
  410. package/src/make-client-session.ts +11 -5
  411. package/src/materializer-helper.ts +27 -16
  412. package/src/otel.ts +13 -2
  413. package/src/rematerialize-from-eventlog.ts +61 -51
  414. package/src/schema/EventDef/define.ts +217 -0
  415. package/src/schema/EventDef/deprecated.test.ts +129 -0
  416. package/src/schema/EventDef/deprecated.ts +175 -0
  417. package/src/schema/EventDef/event-def.ts +125 -0
  418. package/src/schema/EventDef/facts.ts +135 -0
  419. package/src/schema/EventDef/materializer.ts +172 -0
  420. package/src/schema/EventDef/mod.ts +5 -0
  421. package/src/schema/EventSequenceNumber/client.ts +257 -0
  422. package/src/schema/EventSequenceNumber/global.ts +19 -0
  423. package/src/schema/EventSequenceNumber/mod.ts +37 -0
  424. package/src/schema/EventSequenceNumber.test.ts +72 -53
  425. package/src/schema/LiveStoreEvent/client.test.ts +129 -0
  426. package/src/schema/LiveStoreEvent/client.ts +235 -0
  427. package/src/schema/LiveStoreEvent/for-event-def.ts +60 -0
  428. package/src/schema/LiveStoreEvent/global.ts +45 -0
  429. package/src/schema/LiveStoreEvent/input.ts +63 -0
  430. package/src/schema/LiveStoreEvent/mod.ts +4 -0
  431. package/src/schema/events.ts +1 -1
  432. package/src/schema/mod.ts +6 -4
  433. package/src/schema/schema.ts +46 -5
  434. package/src/schema/state/sqlite/client-document-def.test.ts +144 -5
  435. package/src/schema/state/sqlite/client-document-def.ts +47 -34
  436. package/src/schema/state/sqlite/column-annotations.test.ts +3 -2
  437. package/src/schema/state/sqlite/column-annotations.ts +2 -1
  438. package/src/schema/state/sqlite/column-def.test.ts +66 -12
  439. package/src/schema/state/sqlite/column-def.ts +119 -47
  440. package/src/schema/state/sqlite/column-spec.test.ts +32 -17
  441. package/src/schema/state/sqlite/column-spec.ts +37 -11
  442. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +2 -2
  443. package/src/schema/state/sqlite/db-schema/dsl/field-defs.test.ts +2 -1
  444. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +41 -15
  445. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +13 -19
  446. package/src/schema/state/sqlite/mod.ts +7 -8
  447. package/src/schema/state/sqlite/query-builder/api.ts +55 -17
  448. package/src/schema/state/sqlite/query-builder/astToSql.ts +110 -21
  449. package/src/schema/state/sqlite/query-builder/impl.test.ts +267 -93
  450. package/src/schema/state/sqlite/query-builder/impl.ts +26 -13
  451. package/src/schema/state/sqlite/schema-helpers.test.ts +44 -0
  452. package/src/schema/state/sqlite/schema-helpers.ts +30 -22
  453. package/src/schema/state/sqlite/system-tables/eventlog-tables.ts +64 -0
  454. package/src/schema/state/sqlite/system-tables/mod.ts +2 -0
  455. package/src/schema/state/sqlite/system-tables/state-tables.ts +69 -0
  456. package/src/schema/state/sqlite/table-def.test.ts +114 -3
  457. package/src/schema/state/sqlite/table-def.ts +16 -22
  458. package/src/schema/unknown-events.ts +131 -0
  459. package/src/schema-management/__tests__/migrations-autoincrement-quoting.test.ts +88 -0
  460. package/src/schema-management/common.ts +2 -2
  461. package/src/schema-management/migrations.ts +42 -9
  462. package/src/schema-management/validate-schema.ts +3 -3
  463. package/src/sql-queries/sql-queries.ts +18 -6
  464. package/src/sql-queries/sql-query-builder.ts +1 -0
  465. package/src/sqlite-db-helper.ts +3 -3
  466. package/src/sqlite-types.ts +6 -5
  467. package/src/sync/ClientSessionSyncProcessor.ts +152 -142
  468. package/src/sync/errors.ts +12 -24
  469. package/src/sync/index.ts +2 -0
  470. package/src/sync/mock-sync-backend.ts +146 -104
  471. package/src/sync/next/compact-events.ts +10 -11
  472. package/src/sync/next/facts.ts +13 -14
  473. package/src/sync/next/history-dag-common.ts +280 -26
  474. package/src/sync/next/history-dag.ts +17 -13
  475. package/src/sync/next/rebase-events.ts +12 -12
  476. package/src/sync/next/test/compact-events.calculator.test.ts +3 -2
  477. package/src/sync/next/test/compact-events.test.ts +4 -3
  478. package/src/sync/next/test/event-fixtures.ts +13 -13
  479. package/src/sync/sync-backend-kv.ts +4 -3
  480. package/src/sync/sync-backend.ts +66 -17
  481. package/src/sync/sync.ts +24 -2
  482. package/src/sync/syncstate.test.ts +583 -419
  483. package/src/sync/syncstate.ts +127 -122
  484. package/src/sync/transport-chunking.ts +90 -0
  485. package/src/sync/validate-push-payload.ts +6 -8
  486. package/src/testing/event-factory.ts +131 -0
  487. package/src/testing/mod.ts +1 -0
  488. package/src/util.ts +2 -2
  489. package/src/version.ts +33 -8
  490. package/dist/schema/EventDef.d.ts +0 -126
  491. package/dist/schema/EventDef.d.ts.map +0 -1
  492. package/dist/schema/EventDef.js +0 -46
  493. package/dist/schema/EventDef.js.map +0 -1
  494. package/dist/schema/EventSequenceNumber.d.ts +0 -80
  495. package/dist/schema/EventSequenceNumber.d.ts.map +0 -1
  496. package/dist/schema/EventSequenceNumber.js +0 -139
  497. package/dist/schema/EventSequenceNumber.js.map +0 -1
  498. package/dist/schema/LiveStoreEvent.d.ts.map +0 -1
  499. package/dist/schema/LiveStoreEvent.js +0 -147
  500. package/dist/schema/LiveStoreEvent.js.map +0 -1
  501. package/dist/schema/state/sqlite/system-tables.d.ts.map +0 -1
  502. package/dist/schema/state/sqlite/system-tables.js +0 -81
  503. package/dist/schema/state/sqlite/system-tables.js.map +0 -1
  504. package/dist/sync/next/graphology.d.ts +0 -8
  505. package/dist/sync/next/graphology.d.ts.map +0 -1
  506. package/dist/sync/next/graphology.js +0 -30
  507. package/dist/sync/next/graphology.js.map +0 -1
  508. package/dist/sync/next/graphology_.d.ts +0 -3
  509. package/dist/sync/next/graphology_.d.ts.map +0 -1
  510. package/dist/sync/next/graphology_.js +0 -3
  511. package/dist/sync/next/graphology_.js.map +0 -1
  512. package/src/schema/EventDef.ts +0 -222
  513. package/src/schema/EventSequenceNumber.ts +0 -199
  514. package/src/schema/LiveStoreEvent.ts +0 -286
  515. package/src/schema/state/sqlite/system-tables.ts +0 -106
  516. package/src/sync/next/ambient.d.ts +0 -3
  517. package/src/sync/next/graphology.ts +0 -41
  518. package/src/sync/next/graphology_.ts +0 -2
@@ -1,4 +1,4 @@
1
- import { casesHandled, isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
1
+ import { casesHandled, isNotUndefined, LS_DEV, TRACE_VERBOSE } from '@livestore/utils'
2
2
  import type { HttpClient, Runtime, Scope, Tracer } from '@livestore/utils/effect'
3
3
  import {
4
4
  BucketQueue,
@@ -10,42 +10,37 @@ import {
10
10
  FiberHandle,
11
11
  Layer,
12
12
  Option,
13
- OtelTracer,
14
- pipe,
15
13
  Queue,
16
14
  ReadonlyArray,
17
15
  Schedule,
16
+ Schema,
18
17
  Stream,
19
18
  Subscribable,
20
19
  SubscriptionRef,
21
20
  } from '@livestore/utils/effect'
22
- import type * as otel from '@opentelemetry/api'
23
- import {
24
- type IntentionalShutdownCause,
25
- type MaterializeError,
26
- type SqliteDb,
27
- UnexpectedError,
28
- } from '../adapter-types.ts'
21
+
22
+ import { type MaterializeError, type SqliteDb, UnknownError } from '../adapter-types.ts'
23
+ import { IntentionalShutdownCause } from '../errors.ts'
29
24
  import { makeMaterializerHash } from '../materializer-helper.ts'
30
25
  import type { LiveStoreSchema } from '../schema/mod.ts'
31
- import { EventSequenceNumber, getEventDef, LiveStoreEvent, SystemTables } from '../schema/mod.ts'
32
- import {
33
- type InvalidPullError,
34
- type InvalidPushError,
35
- type IsOfflineError,
36
- LeaderAheadError,
37
- type SyncBackend,
38
- } from '../sync/sync.ts'
26
+ import { EventSequenceNumber, LiveStoreEvent, resolveEventDef, SystemTables } from '../schema/mod.ts'
27
+ import { EVENTLOG_META_TABLE, SYNC_STATUS_TABLE } from '../schema/state/sqlite/system-tables/eventlog-tables.ts'
28
+ import type { BackendIdMismatchError, IsOfflineError, SyncBackend } from '../sync/sync.ts'
29
+ import { isRejectedPushError, LeaderAheadError, NonMonotonicBatchError, StaleRebaseGenerationError } from './RejectedPushError.ts'
39
30
  import * as SyncState from '../sync/syncstate.ts'
40
31
  import { sql } from '../util.ts'
41
32
  import * as Eventlog from './eventlog.ts'
42
33
  import { rollback } from './materialize-event.ts'
34
+ import type { ShutdownChannel } from './shutdown-channel.ts'
43
35
  import type { InitialBlockingSyncContext, LeaderSyncProcessor } from './types.ts'
44
36
  import { LeaderThreadCtx } from './types.ts'
45
37
 
38
+ /** Serialize value to JSON string for trace attributes */
39
+ const jsonStringify = Schema.encodeSync(Schema.parseJson())
40
+
46
41
  type LocalPushQueueItem = [
47
- event: LiveStoreEvent.EncodedWithMeta,
48
- deferred: Deferred.Deferred<void, LeaderAheadError> | undefined,
42
+ event: LiveStoreEvent.Client.EncodedWithMeta,
43
+ deferred: Deferred.Deferred<void, LeaderAheadError | StaleRebaseGenerationError> | undefined,
49
44
  ]
50
45
 
51
46
  /**
@@ -66,11 +61,11 @@ type LocalPushQueueItem = [
66
61
  * - Maintains events in ascending order.
67
62
  * - Uses `Deferred` objects to resolve/reject events based on application success.
68
63
  * - Processes events from the queue, applying events in batches.
69
- * - Controlled by a `Latch` to manage execution flow.
70
- * - The latch closes on pull receipt and re-opens post-pull completion.
64
+ * - Controlled by a mutex (`Semaphore(1)`) to ensure mutual exclusion between local push and backend pull processing.
65
+ * - The backend pull side acquires the mutex before processing and releases it on post-pull completion.
71
66
  * - Processes up to `maxBatchSize` events per cycle.
72
67
  *
73
- * Currently we're advancing the state db and eventlog in lockstep, but we could also decouple this in the future
68
+ * Currently, we're advancing the state db and eventlog in lockstep, but we could also decouple this in the future
74
69
  *
75
70
  * Tricky concurrency scenarios:
76
71
  * - Queued local push batches becoming invalid due to a prior local push item being rejected.
@@ -84,6 +79,7 @@ export const makeLeaderSyncProcessor = ({
84
79
  initialBlockingSyncContext,
85
80
  initialSyncState,
86
81
  onError,
82
+ onBackendIdMismatch,
87
83
  livePull,
88
84
  params,
89
85
  testing,
@@ -93,13 +89,60 @@ export const makeLeaderSyncProcessor = ({
93
89
  initialBlockingSyncContext: InitialBlockingSyncContext
94
90
  /** Initial sync state rehydrated from the persisted eventlog or initial sync state */
95
91
  initialSyncState: SyncState.SyncState
92
+ /**
93
+ * What to do when a failure (any cause) occurs (except `BackendIdMismatchError`).
94
+ *
95
+ * - `'shutdown'`: Send the error to the shutdown channel and terminate the sync processor.
96
+ * - `'ignore'`: Continue running.
97
+ */
96
98
  onError: 'shutdown' | 'ignore'
99
+ /**
100
+ * What to do when the sync backend identity has changed (i.e. the backend was reset).
101
+ *
102
+ * - `'reset'`: Clear local databases (eventlog and state) and send an intentional shutdown signal.
103
+ * - `'shutdown'`: Send a shutdown signal without clearing local storage.
104
+ * - `'ignore'`: Continue running with stale data.
105
+ */
106
+ onBackendIdMismatch: 'reset' | 'shutdown' | 'ignore'
97
107
  params: {
98
108
  /**
109
+ * Maximum number of local events to process per batch cycle.
110
+ *
111
+ * This controls how many events from client sessions are applied to the local state
112
+ * in a single iteration before yielding to allow potential backend pulls.
113
+ *
114
+ * **Trade-offs:**
115
+ * - **Lower values (1-5):** More responsive to remote updates since pull processing can
116
+ * interleave more frequently. Better for high-conflict scenarios where rebases are common.
117
+ * Slightly higher per-event overhead due to more frequent transaction commits.
118
+ *
119
+ * - **Higher values (10-50+):** Better throughput for bulk local writes as more events are
120
+ * batched into a single transaction. However, may delay remote update processing and
121
+ * increase rebase complexity if many local events queue up during a slow pull.
122
+ *
123
+ * - **Very high values (100+):** Risk of starvation for pull processing if local pushes
124
+ * arrive continuously. May cause larger rollbacks during rebases. Not recommended
125
+ * unless you have a write-heavy workload with minimal remote synchronization.
126
+ *
99
127
  * @default 10
100
128
  */
101
129
  localPushBatchSize?: number
102
130
  /**
131
+ * Maximum number of events to push to the sync backend per batch.
132
+ *
133
+ * This controls how many events are sent in a single push request to the remote server.
134
+ *
135
+ * **Trade-offs:**
136
+ * - **Lower values (1-10):** Lower latency for each push operation. Faster feedback on
137
+ * push success/failure. Slightly higher network overhead due to more requests.
138
+ *
139
+ * - **Higher values (50-100):** Better network efficiency by amortizing request overhead.
140
+ * Preferred for high-throughput scenarios. May increase latency to first confirmation.
141
+ *
142
+ * - **Very high values (200+):** Risk of hitting server request size limits or timeouts.
143
+ * A single failed request loses the entire batch (will be retried). May cause memory
144
+ * pressure if events accumulate faster than they can be pushed.
145
+ *
103
146
  * @default 50
104
147
  */
105
148
  backendPushBatchSize?: number
@@ -114,18 +157,16 @@ export const makeLeaderSyncProcessor = ({
114
157
  localPushProcessing?: Effect.Effect<void>
115
158
  }
116
159
  }
117
- }): Effect.Effect<LeaderSyncProcessor, UnexpectedError, Scope.Scope> =>
160
+ }): Effect.Effect<LeaderSyncProcessor, never, Scope.Scope> =>
118
161
  Effect.gen(function* () {
119
- const syncBackendPushQueue = yield* BucketQueue.make<LiveStoreEvent.EncodedWithMeta>()
120
- const localPushBatchSize = params.localPushBatchSize ?? 1
121
- const backendPushBatchSize = params.backendPushBatchSize ?? 2
162
+ const syncBackendPushQueue = yield* BucketQueue.make<LiveStoreEvent.Client.EncodedWithMeta>()
163
+ const localPushBatchSize = params.localPushBatchSize ?? 10
164
+ const backendPushBatchSize = params.backendPushBatchSize ?? 50
122
165
 
123
166
  const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
124
167
 
125
- const isClientEvent = (eventEncoded: LiveStoreEvent.EncodedWithMeta) => {
126
- const { eventDef } = getEventDef(schema, eventEncoded.name)
127
- return eventDef.options.clientOnly
128
- }
168
+ const isClientEvent = (eventEncoded: LiveStoreEvent.Client.EncodedWithMeta) =>
169
+ schema.eventsDefsMap.get(eventEncoded.name)?.options.clientOnly ?? false
129
170
 
130
171
  const connectedClientSessionPullQueues = yield* makePullQueueSet
131
172
 
@@ -134,7 +175,6 @@ export const makeLeaderSyncProcessor = ({
134
175
  current: undefined as
135
176
  | undefined
136
177
  | {
137
- otelSpan: otel.Span | undefined
138
178
  span: Tracer.Span
139
179
  devtoolsLatch: Effect.Latch | undefined
140
180
  runtime: Runtime.Runtime<LeaderThreadCtx>
@@ -142,8 +182,8 @@ export const makeLeaderSyncProcessor = ({
142
182
  }
143
183
 
144
184
  const localPushesQueue = yield* BucketQueue.make<LocalPushQueueItem>()
145
- const localPushesLatch = yield* Effect.makeLatch(true)
146
- const pullLatch = yield* Effect.makeLatch(true)
185
+ // Ensures mutual exclusion between local push and backend pull processing.
186
+ const localPushBackendPullMutex = yield* Effect.makeSemaphore(1)
147
187
 
148
188
  /**
149
189
  * Additionally to the `syncStateSref` we also need the `pushHeadRef` in order to prevent old/duplicate
@@ -154,9 +194,9 @@ export const makeLeaderSyncProcessor = ({
154
194
  *
155
195
  * Thus the purpose of the pushHeadRef is the guard the integrity of the local push queue
156
196
  */
157
- const pushHeadRef = { current: EventSequenceNumber.ROOT }
158
- const advancePushHead = (eventNum: EventSequenceNumber.EventSequenceNumber) => {
159
- pushHeadRef.current = EventSequenceNumber.max(pushHeadRef.current, eventNum)
197
+ const pushHeadRef = { current: EventSequenceNumber.Client.ROOT }
198
+ const advancePushHead = (eventNum: EventSequenceNumber.Client.Composite) => {
199
+ pushHeadRef.current = EventSequenceNumber.Client.max(pushHeadRef.current, eventNum)
160
200
  }
161
201
 
162
202
  // NOTE: New events are only pushed to sync backend after successful local push processing
@@ -172,8 +212,8 @@ export const makeLeaderSyncProcessor = ({
172
212
 
173
213
  const waitForProcessing = options?.waitForProcessing ?? false
174
214
 
175
- if (waitForProcessing) {
176
- const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void, LeaderAheadError>())
215
+ if (waitForProcessing === true) {
216
+ const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void, LeaderAheadError | StaleRebaseGenerationError>())
177
217
 
178
218
  const items = newEvents.map((eventEncoded, i) => [eventEncoded, deferreds[i]] as LocalPushQueueItem)
179
219
 
@@ -188,41 +228,62 @@ export const makeLeaderSyncProcessor = ({
188
228
  Effect.withSpan('@livestore/common:LeaderSyncProcessor:push', {
189
229
  attributes: {
190
230
  batchSize: newEvents.length,
191
- batch: TRACE_VERBOSE ? newEvents : undefined,
231
+ batch: TRACE_VERBOSE === true ? newEvents : undefined,
192
232
  },
193
- links: ctxRef.current?.span ? [{ _tag: 'SpanLink', span: ctxRef.current.span, attributes: {} }] : undefined,
233
+ links:
234
+ ctxRef.current?.span !== undefined
235
+ ? [{ _tag: 'SpanLink', span: ctxRef.current.span, attributes: {} }]
236
+ : undefined,
194
237
  }),
195
238
  )
196
239
 
197
240
  const pushPartial: LeaderSyncProcessor['pushPartial'] = ({ event: { name, args }, clientId, sessionId }) =>
198
241
  Effect.gen(function* () {
199
- const syncState = yield* syncStateSref
200
- if (syncState === undefined) return shouldNeverHappen('Not initialized')
242
+ const syncState = yield* Effect.fromNullable(yield* syncStateSref).pipe(Effect.orDieDebugger)
243
+
244
+ const resolution = yield* resolveEventDef(schema, {
245
+ operation: '@livestore/common:LeaderSyncProcessor:pushPartial',
246
+ event: {
247
+ name,
248
+ args,
249
+ clientId,
250
+ sessionId,
251
+ seqNum: syncState.localHead,
252
+ },
253
+ })
201
254
 
202
- const { eventDef } = getEventDef(schema, name)
255
+ if (resolution._tag === 'unknown') {
256
+ // Ignore partial pushes for unrecognised events – they are still
257
+ // persisted server-side once a schema update ships.
258
+ return
259
+ }
203
260
 
204
- const eventEncoded = new LiveStoreEvent.EncodedWithMeta({
261
+ const eventEncoded = new LiveStoreEvent.Client.EncodedWithMeta({
205
262
  name,
206
263
  args,
207
264
  clientId,
208
265
  sessionId,
209
- ...EventSequenceNumber.nextPair({ seqNum: syncState.localHead, isClient: eventDef.options.clientOnly }),
266
+ ...EventSequenceNumber.Client.nextPair({
267
+ seqNum: syncState.localHead,
268
+ isClient: resolution.eventDef.options.clientOnly,
269
+ }),
210
270
  })
211
271
 
212
272
  yield* push([eventEncoded])
213
- }).pipe(Effect.catchTag('LeaderAheadError', Effect.orDie))
273
+ }).pipe(
274
+ // pushPartial constructs the event sequence number internally, so these errors should never happen.
275
+ Effect.catchIf(isRejectedPushError, Effect.die),
276
+ )
214
277
 
215
278
  // Starts various background loops
216
279
  const boot: LeaderSyncProcessor['boot'] = Effect.gen(function* () {
217
280
  const span = yield* Effect.currentSpan.pipe(Effect.orDie)
218
- const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)))
219
281
  const { devtools, shutdownChannel } = yield* LeaderThreadCtx
220
282
  const runtime = yield* Effect.runtime<LeaderThreadCtx>()
221
283
 
222
284
  ctxRef.current = {
223
- otelSpan,
224
285
  span,
225
- devtoolsLatch: devtools.enabled ? devtools.syncBackendLatch : undefined,
286
+ devtoolsLatch: devtools.enabled === true ? devtools.syncBackendLatch : undefined,
226
287
  runtime,
227
288
  }
228
289
 
@@ -232,10 +293,10 @@ export const makeLeaderSyncProcessor = ({
232
293
  // Rehydrate sync queue
233
294
  if (initialSyncState.pending.length > 0) {
234
295
  const globalPendingEvents = initialSyncState.pending
235
- // Don't sync clientOnly events
296
+ // Don't sync client-local events
236
297
  .filter((eventEncoded) => {
237
- const { eventDef } = getEventDef(schema, eventEncoded.name)
238
- return eventDef.options.clientOnly === false
298
+ const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
299
+ return eventDef === undefined ? true : eventDef.options.clientOnly === false
239
300
  })
240
301
 
241
302
  if (globalPendingEvents.length > 0) {
@@ -243,19 +304,18 @@ export const makeLeaderSyncProcessor = ({
243
304
  }
244
305
  }
245
306
 
307
+ const handleBackendIdMismatchError = (error: BackendIdMismatchError) =>
308
+ handleBackendIdMismatch({ error, onBackendIdMismatch, shutdownChannel })
309
+
246
310
  const maybeShutdownOnError = (
247
311
  cause: Cause.Cause<
248
- | UnexpectedError
249
- | IntentionalShutdownCause
250
- | IsOfflineError
251
- | InvalidPushError
252
- | InvalidPullError
312
+ | UnknownError
253
313
  | MaterializeError
254
314
  >,
255
315
  ) =>
256
316
  Effect.gen(function* () {
257
317
  if (onError === 'ignore') {
258
- if (LS_DEV) {
318
+ if (LS_DEV === true) {
259
319
  yield* Effect.logDebug(
260
320
  `Ignoring sync error (${cause._tag === 'Fail' ? cause.error._tag : cause._tag})`,
261
321
  Cause.pretty(cause),
@@ -264,35 +324,38 @@ export const makeLeaderSyncProcessor = ({
264
324
  return
265
325
  }
266
326
 
267
- const errorToSend = Cause.isFailType(cause) ? cause.error : UnexpectedError.make({ cause })
327
+ const errorToSend = Cause.isFailType(cause) === true ? cause.error : UnknownError.make({ cause })
268
328
  yield* shutdownChannel.send(errorToSend).pipe(Effect.orDie)
269
329
 
270
- return yield* Effect.die(cause)
330
+ return yield* Effect.failCause(cause).pipe(Effect.orDie)
271
331
  })
272
332
 
273
333
  yield* backgroundApplyLocalPushes({
274
- localPushesLatch,
334
+ localPushBackendPullMutex,
275
335
  localPushesQueue,
276
- pullLatch,
277
336
  syncStateSref,
278
337
  syncBackendPushQueue,
279
338
  schema,
280
339
  isClientEvent,
281
- otelSpan,
282
340
  connectedClientSessionPullQueues,
283
341
  localPushBatchSize,
284
342
  testing: {
285
343
  delay: testing?.delays?.localPushProcessing,
286
344
  },
287
- }).pipe(Effect.catchAllCause(maybeShutdownOnError), Effect.forkScoped)
345
+ }).pipe(
346
+ Effect.catchAllCause(maybeShutdownOnError),
347
+ Effect.forkScoped,
348
+ )
288
349
 
289
350
  const backendPushingFiberHandle = yield* FiberHandle.make<void, never>()
290
351
  const backendPushingEffect = backgroundBackendPushing({
291
352
  syncBackendPushQueue,
292
- otelSpan,
293
353
  devtoolsLatch: ctxRef.current?.devtoolsLatch,
294
354
  backendPushBatchSize,
295
- }).pipe(Effect.catchAllCause(maybeShutdownOnError))
355
+ }).pipe(
356
+ Effect.catchTag('BackendIdMismatchError', handleBackendIdMismatchError),
357
+ Effect.catchAllCause(maybeShutdownOnError),
358
+ )
296
359
 
297
360
  yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
298
361
 
@@ -311,20 +374,21 @@ export const makeLeaderSyncProcessor = ({
311
374
  yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
312
375
  }),
313
376
  syncStateSref,
314
- localPushesLatch,
315
- pullLatch,
377
+ localPushBackendPullMutex,
316
378
  livePull,
317
379
  dbState,
318
- otelSpan,
319
380
  initialBlockingSyncContext,
320
381
  devtoolsLatch: ctxRef.current?.devtoolsLatch,
321
382
  connectedClientSessionPullQueues,
322
383
  advancePushHead,
323
384
  }).pipe(
324
385
  Effect.retry({
325
- // We want to retry pulling if we've lost connection to the sync backend
326
- while: (cause) => cause._tag === 'IsOfflineError',
386
+ // Retry pulling when we've lost connection to the sync backend
387
+ // We're using `until` with a refinement instead of `while` to narrow `IsOfflineError` out of the error type.
388
+ // See https://github.com/Effect-TS/effect/issues/6122
389
+ until: (error): error is Exclude<typeof error, IsOfflineError> => error._tag !== 'IsOfflineError',
327
390
  }),
391
+ Effect.catchTag('BackendIdMismatchError', handleBackendIdMismatchError),
328
392
  Effect.catchAllCause(maybeShutdownOnError),
329
393
  // Needed to avoid `Fiber terminated with an unhandled error` logs which seem to happen because of the `Effect.retry` above.
330
394
  // This might be a bug in Effect. Only seems to happen in the browser.
@@ -355,17 +419,16 @@ export const makeLeaderSyncProcessor = ({
355
419
  - full new state db snapshot in the "rebase" case
356
420
  - downside: importing the snapshot is expensive
357
421
  */
358
- const pullQueue: LeaderSyncProcessor['pullQueue'] = ({ cursor }) => {
359
- const runtime = ctxRef.current?.runtime ?? shouldNeverHappen('Not initialized')
360
- return connectedClientSessionPullQueues.makeQueue(cursor).pipe(Effect.provide(runtime))
361
- }
422
+ const pullQueue: LeaderSyncProcessor['pullQueue'] = ({ cursor }) =>
423
+ Effect.fromNullable(ctxRef.current?.runtime).pipe(
424
+ Effect.orDieDebugger,
425
+ Effect.flatMap((runtime) =>
426
+ connectedClientSessionPullQueues.makeQueue(cursor).pipe(Effect.provide(runtime))
427
+ )
428
+ )
362
429
 
363
430
  const syncState = Subscribable.make({
364
- get: Effect.gen(function* () {
365
- const syncState = yield* syncStateSref
366
- if (syncState === undefined) return shouldNeverHappen('Not initialized')
367
- return syncState
368
- }),
431
+ get: syncStateSref.pipe(Effect.flatMap(Effect.fromNullable), Effect.orDieDebugger),
369
432
  changes: syncStateSref.changes.pipe(Stream.filter(isNotUndefined)),
370
433
  })
371
434
 
@@ -380,26 +443,22 @@ export const makeLeaderSyncProcessor = ({
380
443
  })
381
444
 
382
445
  const backgroundApplyLocalPushes = ({
383
- localPushesLatch,
446
+ localPushBackendPullMutex,
384
447
  localPushesQueue,
385
- pullLatch,
386
448
  syncStateSref,
387
449
  syncBackendPushQueue,
388
450
  schema,
389
451
  isClientEvent,
390
- otelSpan,
391
452
  connectedClientSessionPullQueues,
392
453
  localPushBatchSize,
393
454
  testing,
394
455
  }: {
395
- pullLatch: Effect.Latch
396
- localPushesLatch: Effect.Latch
456
+ localPushBackendPullMutex: Effect.Semaphore
397
457
  localPushesQueue: BucketQueue.BucketQueue<LocalPushQueueItem>
398
458
  syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
399
- syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.EncodedWithMeta>
459
+ syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.Client.EncodedWithMeta>
400
460
  schema: LiveStoreSchema
401
- isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
402
- otelSpan: otel.Span | undefined
461
+ isClientEvent: (eventEncoded: LiveStoreEvent.Client.EncodedWithMeta) => boolean
403
462
  connectedClientSessionPullQueues: PullQueueSet
404
463
  localPushBatchSize: number
405
464
  testing: {
@@ -414,136 +473,149 @@ const backgroundApplyLocalPushes = ({
414
473
 
415
474
  const batchItems = yield* BucketQueue.takeBetween(localPushesQueue, 1, localPushBatchSize)
416
475
 
417
- // Wait for the backend pulling to finish
418
- yield* localPushesLatch.await
419
-
420
- // Prevent backend pull processing until this local push is finished
421
- yield* pullLatch.close
422
-
423
- const syncState = yield* syncStateSref
424
- if (syncState === undefined) return shouldNeverHappen('Not initialized')
476
+ // Applies a batch of local pushes, guarded by the localPushBackendPullMutex to ensure mutual exclusion with backend pulling
477
+ yield* Effect.gen(function* () {
478
+ const syncState = yield* Effect.fromNullable(yield* syncStateSref).pipe(Effect.orDieDebugger)
425
479
 
426
- const currentRebaseGeneration = syncState.localHead.rebaseGeneration
480
+ const currentRebaseGeneration = syncState.localHead.rebaseGeneration
427
481
 
428
- // Since the rebase generation might have changed since enqueuing, we need to filter out items with older generation
429
- // It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
430
- const [newEvents, deferreds] = pipe(
431
- batchItems,
432
- ReadonlyArray.filter(([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration === currentRebaseGeneration),
433
- ReadonlyArray.unzip,
434
- )
435
-
436
- if (newEvents.length === 0) {
437
- // console.log('dropping old-gen batch', currentLocalPushGenerationRef.current)
438
- // Allow the backend pulling to start
439
- yield* pullLatch.open
440
- continue
441
- }
482
+ // Since the rebase generation might have changed since enqueuing, we need to filter out items with older generation
483
+ // It's important that we filter after acquiring the localPushBackendPullMutex, otherwise we might filter with the old generation
484
+ const [droppedItems, filteredItems] = ReadonlyArray.partition(
485
+ batchItems,
486
+ ([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration >= currentRebaseGeneration,
487
+ )
442
488
 
443
- const mergeResult = SyncState.merge({
444
- syncState,
445
- payload: { _tag: 'local-push', newEvents },
446
- isClientEvent,
447
- isEqualEvent: LiveStoreEvent.isEqualEncoded,
448
- })
489
+ if (droppedItems.length > 0) {
490
+ yield* Effect.spanEvent(`push:drop-old-generation`, {
491
+ droppedCount: droppedItems.length,
492
+ currentRebaseGeneration,
493
+ })
449
494
 
450
- switch (mergeResult._tag) {
451
- case 'unexpected-error': {
452
- otelSpan?.addEvent(`push:unexpected-error`, {
453
- batchSize: newEvents.length,
454
- newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
455
- })
456
- return yield* new UnexpectedError({ cause: mergeResult.message })
495
+ /**
496
+ * Dropped pushes may still have a deferred awaiting completion.
497
+ * Fail it so the caller learns the leader advanced and resubmits with the updated generation.
498
+ */
499
+ yield* Effect.forEach(
500
+ droppedItems.filter(
501
+ (item): item is [LiveStoreEvent.Client.EncodedWithMeta, Deferred.Deferred<void, LeaderAheadError | StaleRebaseGenerationError>] =>
502
+ item[1] !== undefined,
503
+ ),
504
+ ([eventEncoded, deferred]) =>
505
+ Deferred.fail(
506
+ deferred,
507
+ StaleRebaseGenerationError.make({
508
+ currentRebaseGeneration,
509
+ providedRebaseGeneration: eventEncoded.seqNum.rebaseGeneration,
510
+ sessionId: eventEncoded.sessionId,
511
+ }),
512
+ ),
513
+ )
457
514
  }
458
- case 'rebase': {
459
- return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
515
+
516
+ if (filteredItems.length === 0) {
517
+ return
460
518
  }
461
- case 'reject': {
462
- otelSpan?.addEvent(`push:reject`, {
463
- batchSize: newEvents.length,
464
- mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
465
- })
466
519
 
467
- // TODO: how to test this?
468
- const nextRebaseGeneration = currentRebaseGeneration + 1
520
+ const [newEvents, deferreds] = ReadonlyArray.unzip(filteredItems)
469
521
 
470
- const providedNum = newEvents.at(0)!.seqNum
471
- // All subsequent pushes with same generation should be rejected as well
472
- // We're also handling the case where the localPushQueue already contains events
473
- // from the next generation which we preserve in the queue
474
- const remainingEventsMatchingGeneration = yield* BucketQueue.takeSplitWhere(
475
- localPushesQueue,
476
- ([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration >= nextRebaseGeneration,
477
- )
522
+ yield* Effect.annotateCurrentSpan({
523
+ 'batchSize': newEvents.length,
524
+ ...(TRACE_VERBOSE === true ? { 'newEvents': jsonStringify(newEvents) } : {}),
525
+ })
526
+
527
+ const mergeResult = yield* SyncState.merge({
528
+ syncState,
529
+ payload: { _tag: 'local-push', newEvents },
530
+ isClientEvent,
531
+ isEqualEvent: LiveStoreEvent.Client.isEqualEncoded,
532
+ })
478
533
 
479
- // TODO we still need to better understand and handle this scenario
480
- if (LS_DEV && (yield* BucketQueue.size(localPushesQueue)) > 0) {
481
- console.log('localPushesQueue is not empty', yield* BucketQueue.size(localPushesQueue))
482
- // biome-ignore lint/suspicious/noDebugger: debugging
483
- debugger
534
+ switch (mergeResult._tag) {
535
+ case 'rebase': {
536
+ return yield* Effect.dieDebugger('The leader thread should never have to rebase due to a local push')
484
537
  }
538
+ case 'reject': {
539
+ yield* Effect.spanEvent(`push:reject`, {
540
+ batchSize: newEvents.length,
541
+ ...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
542
+ })
485
543
 
486
- const allDeferredsToReject = [
487
- ...deferreds,
488
- ...remainingEventsMatchingGeneration.map(([_, deferred]) => deferred),
489
- ].filter(isNotUndefined)
544
+ // TODO: how to test this?
545
+ const nextRebaseGeneration = currentRebaseGeneration + 1
490
546
 
491
- yield* Effect.forEach(allDeferredsToReject, (deferred) =>
492
- Deferred.fail(
493
- deferred,
494
- LeaderAheadError.make({ minimumExpectedNum: mergeResult.expectedMinimumId, providedNum }),
495
- ),
496
- )
547
+ const providedNum = newEvents.at(0)!.seqNum
548
+ // All subsequent pushes with same generation should be rejected as well
549
+ // We're also handling the case where the localPushQueue already contains events
550
+ // from the next generation which we preserve in the queue
551
+ const remainingEventsMatchingGeneration = yield* BucketQueue.takeSplitWhere(
552
+ localPushesQueue,
553
+ ([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration >= nextRebaseGeneration,
554
+ )
497
555
 
498
- // Allow the backend pulling to start
499
- yield* pullLatch.open
556
+ // TODO we still need to better understand and handle this scenario
557
+ if (LS_DEV === true && (yield* BucketQueue.size(localPushesQueue)) > 0) {
558
+ console.log('localPushesQueue is not empty', yield* BucketQueue.size(localPushesQueue))
559
+ // oxlint-disable-next-line eslint(no-debugger) -- intentional breakpoint for unexpected queue state
560
+ debugger
561
+ }
500
562
 
501
- // In this case we're skipping state update and down/upstream processing
502
- // We've cleared the local push queue and are now waiting for new local pushes / backend pulls
503
- continue
504
- }
505
- case 'advance': {
506
- break
507
- }
508
- default: {
509
- casesHandled(mergeResult)
510
- }
511
- }
563
+ const allDeferredsToReject = [
564
+ ...deferreds,
565
+ ...remainingEventsMatchingGeneration.map(([_, deferred]) => deferred),
566
+ ].filter(isNotUndefined)
512
567
 
513
- yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
568
+ yield* Effect.forEach(allDeferredsToReject, (deferred) =>
569
+ Deferred.fail(
570
+ deferred,
571
+ LeaderAheadError.make({ minimumExpectedNum: mergeResult.expectedMinimumId, providedNum, sessionId: newEvents.at(0)!.sessionId }),
572
+ ),
573
+ )
514
574
 
515
- yield* connectedClientSessionPullQueues.offer({
516
- payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
517
- leaderHead: mergeResult.newSyncState.localHead,
518
- })
575
+ // In this case we're skipping state update and down/upstream processing
576
+ // We've cleared the local push queue and are now waiting for new local pushes / backend pulls
577
+ return
578
+ }
579
+ case 'advance': {
580
+ break
581
+ }
582
+ default: {
583
+ casesHandled(mergeResult)
584
+ }
585
+ }
519
586
 
520
- otelSpan?.addEvent(`push:advance`, {
521
- batchSize: newEvents.length,
522
- mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
523
- })
587
+ yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
524
588
 
525
- // Don't sync clientOnly events
526
- const filteredBatch = mergeResult.newEvents.filter((eventEncoded) => {
527
- const { eventDef } = getEventDef(schema, eventEncoded.name)
528
- return eventDef.options.clientOnly === false
529
- })
589
+ yield* connectedClientSessionPullQueues.offer({
590
+ payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
591
+ leaderHead: mergeResult.newSyncState.localHead,
592
+ })
530
593
 
531
- yield* BucketQueue.offerAll(syncBackendPushQueue, filteredBatch)
594
+ yield* Effect.spanEvent(`push:advance`, {
595
+ batchSize: newEvents.length,
596
+ ...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
597
+ })
532
598
 
533
- yield* materializeEventsBatch({ batchItems: mergeResult.newEvents, deferreds })
599
+ // Don't sync client-local events
600
+ const filteredBatch = mergeResult.newEvents.filter((eventEncoded) => {
601
+ const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
602
+ return eventDef === undefined ? true : eventDef.options.clientOnly === false
603
+ })
534
604
 
535
- // Allow the backend pulling to start
536
- yield* pullLatch.open
605
+ yield* BucketQueue.offerAll(syncBackendPushQueue, filteredBatch)
606
+
607
+ yield* materializeEventsBatch({ batchItems: mergeResult.newEvents, deferreds })
608
+ }).pipe(localPushBackendPullMutex.withPermits(1))
537
609
  }
538
610
  })
539
611
 
540
612
  type MaterializeEventsBatch = (_: {
541
- batchItems: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>
613
+ batchItems: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>
542
614
  /**
543
615
  * The deferreds are used by the caller to know when the mutation has been processed.
544
616
  * Indexes are aligned with `batchItems`
545
617
  */
546
- deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError> | undefined> | undefined
618
+ deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError | StaleRebaseGenerationError> | undefined> | undefined
547
619
  }) => Effect.Effect<void, MaterializeError, LeaderThreadCtx>
548
620
 
549
621
  // TODO how to handle errors gracefully
@@ -557,7 +629,7 @@ const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds
557
629
 
558
630
  yield* Effect.addFinalizer((exit) =>
559
631
  Effect.gen(function* () {
560
- if (Exit.isSuccess(exit)) return
632
+ if (Exit.isSuccess(exit) === true) return
561
633
 
562
634
  // Rollback in case of an error
563
635
  db.execute('ROLLBACK', undefined)
@@ -586,90 +658,99 @@ const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds
586
658
  Effect.tapCauseLogPretty,
587
659
  )
588
660
 
589
- const backgroundBackendPulling = ({
661
+ const backgroundBackendPulling = Effect.fn('@livestore/common:LeaderSyncProcessor:backend-pulling')(function* ({
590
662
  isClientEvent,
591
663
  restartBackendPushing,
592
- otelSpan,
593
664
  dbState,
594
665
  syncStateSref,
595
- localPushesLatch,
666
+ localPushBackendPullMutex,
596
667
  livePull,
597
- pullLatch,
598
668
  devtoolsLatch,
599
669
  initialBlockingSyncContext,
600
670
  connectedClientSessionPullQueues,
601
671
  advancePushHead,
602
672
  }: {
603
- isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
673
+ isClientEvent: (eventEncoded: LiveStoreEvent.Client.EncodedWithMeta) => boolean
604
674
  restartBackendPushing: (
605
- filteredRebasedPending: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
606
- ) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx | HttpClient.HttpClient>
607
- otelSpan: otel.Span | undefined
675
+ filteredRebasedPending: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>,
676
+ ) => Effect.Effect<void, never, LeaderThreadCtx | HttpClient.HttpClient>
608
677
  syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
609
678
  dbState: SqliteDb
610
- localPushesLatch: Effect.Latch
611
- pullLatch: Effect.Latch
679
+ localPushBackendPullMutex: Effect.Semaphore
612
680
  livePull: boolean
613
681
  devtoolsLatch: Effect.Latch | undefined
614
682
  initialBlockingSyncContext: InitialBlockingSyncContext
615
683
  connectedClientSessionPullQueues: PullQueueSet
616
- advancePushHead: (eventNum: EventSequenceNumber.EventSequenceNumber) => void
617
- }) =>
618
- Effect.gen(function* () {
619
- const { syncBackend, dbState: db, dbEventlog, schema } = yield* LeaderThreadCtx
684
+ advancePushHead: (eventNum: EventSequenceNumber.Client.Composite) => void
685
+ }) {
686
+ const { syncBackend, dbState: db, dbEventlog, schema } = yield* LeaderThreadCtx
620
687
 
621
- if (syncBackend === undefined) return
688
+ if (syncBackend === undefined) return
622
689
 
623
- const onNewPullChunk = (newEvents: LiveStoreEvent.EncodedWithMeta[], pageInfo: SyncBackend.PullResPageInfo) =>
624
- Effect.gen(function* () {
625
- if (newEvents.length === 0) return
690
+ let pullMutexHeld = false
626
691
 
627
- if (devtoolsLatch !== undefined) {
628
- yield* devtoolsLatch.await
692
+ const releasePullMutexIfHeld = Effect.gen(function* () {
693
+ if (pullMutexHeld === false) return
694
+ pullMutexHeld = false
695
+ yield* localPushBackendPullMutex.release(1)
696
+ })
697
+
698
+ const isPullPaginationComplete = (pageInfo: SyncBackend.PullResPageInfo) => pageInfo._tag === 'NoMore'
699
+
700
+ const onNewPullChunk = (newEvents: LiveStoreEvent.Client.EncodedWithMeta[], pageInfo: SyncBackend.PullResPageInfo) =>
701
+ Effect.gen(function* () {
702
+ if (devtoolsLatch !== undefined) {
703
+ yield* devtoolsLatch.await
704
+ }
705
+
706
+ if (newEvents.length === 0) {
707
+ if (isPullPaginationComplete(pageInfo) === true) {
708
+ yield* releasePullMutexIfHeld
629
709
  }
710
+ return
711
+ }
630
712
 
631
- // Prevent more local pushes from being processed until this pull is finished
632
- yield* localPushesLatch.close
713
+ // Prevent more local pushes from being processed until this pull pagination sequence is finished.
714
+ if (pullMutexHeld === false) {
715
+ yield* localPushBackendPullMutex.take(1)
716
+ pullMutexHeld = true
717
+ }
633
718
 
634
- // Wait for pending local pushes to finish
635
- yield* pullLatch.await
719
+ const chunkExit = yield* Effect.gen(function* () {
720
+ const syncState = yield* Effect.fromNullable(yield* syncStateSref).pipe(Effect.orDieDebugger)
636
721
 
637
- const syncState = yield* syncStateSref
638
- if (syncState === undefined) return shouldNeverHappen('Not initialized')
722
+ yield* Effect.annotateCurrentSpan({
723
+ 'merge.newEventsCount': newEvents.length,
724
+ ...(TRACE_VERBOSE === true ? { 'merge.newEvents': jsonStringify(newEvents) } : {}),
725
+ })
639
726
 
640
- const mergeResult = SyncState.merge({
641
- syncState,
642
- payload: SyncState.PayloadUpstreamAdvance.make({ newEvents }),
643
- isClientEvent,
644
- isEqualEvent: LiveStoreEvent.isEqualEncoded,
645
- ignoreClientEvents: true,
646
- })
727
+ const mergeResult = yield* SyncState.merge({
728
+ syncState,
729
+ payload: SyncState.PayloadUpstreamAdvance.make({ newEvents }),
730
+ isClientEvent,
731
+ isEqualEvent: LiveStoreEvent.Client.isEqualEncoded,
732
+ ignoreClientEvents: true,
733
+ })
647
734
 
648
- if (mergeResult._tag === 'reject') {
649
- return shouldNeverHappen('The leader thread should never reject upstream advances')
650
- } else if (mergeResult._tag === 'unexpected-error') {
651
- otelSpan?.addEvent(`pull:unexpected-error`, {
652
- newEventsCount: newEvents.length,
653
- newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
654
- })
655
- return yield* new UnexpectedError({ cause: mergeResult.message })
656
- }
735
+ if (mergeResult._tag === 'reject') {
736
+ return yield* Effect.dieDebugger('The leader thread should never reject upstream advances')
737
+ }
657
738
 
658
739
  const newBackendHead = newEvents.at(-1)!.seqNum
659
740
 
660
741
  Eventlog.updateBackendHead(dbEventlog, newBackendHead)
661
742
 
662
743
  if (mergeResult._tag === 'rebase') {
663
- otelSpan?.addEvent(`pull:rebase[${mergeResult.newSyncState.localHead.rebaseGeneration}]`, {
664
- newEventsCount: newEvents.length,
665
- newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
666
- rollbackCount: mergeResult.rollbackEvents.length,
667
- mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
744
+ yield* Effect.spanEvent(`pull:rebase[${mergeResult.newSyncState.localHead.rebaseGeneration}]`, {
745
+ newEventsCount: newEvents.length,
746
+ ...(TRACE_VERBOSE === true ? { newEvents: jsonStringify(newEvents) } : {}),
747
+ rollbackCount: mergeResult.rollbackEvents.length,
748
+ ...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
668
749
  })
669
750
 
670
751
  const globalRebasedPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
671
- const { eventDef } = getEventDef(schema, event.name)
672
- return eventDef.options.clientOnly === false
752
+ const eventDef = schema.eventsDefsMap.get(event.name)
753
+ return eventDef === undefined ? true : eventDef.options.clientOnly === false
673
754
  })
674
755
  yield* restartBackendPushing(globalRebasedPendingEvents)
675
756
 
@@ -686,15 +767,15 @@ const backgroundBackendPulling = ({
686
767
  leaderHead: mergeResult.newSyncState.localHead,
687
768
  })
688
769
  } else {
689
- otelSpan?.addEvent(`pull:advance`, {
770
+ yield* Effect.spanEvent(`pull:advance`, {
690
771
  newEventsCount: newEvents.length,
691
- mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
772
+ ...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
692
773
  })
693
774
 
694
775
  // Ensure push fiber is active after advance by restarting with current pending (non-client) events
695
776
  const globalPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
696
- const { eventDef } = getEventDef(schema, event.name)
697
- return eventDef.options.clientOnly === false
777
+ const eventDef = schema.eventsDefsMap.get(event.name)
778
+ return eventDef === undefined ? true : eventDef.options.clientOnly === false
698
779
  })
699
780
  yield* restartBackendPushing(globalPendingEvents)
700
781
 
@@ -708,10 +789,10 @@ const backgroundBackendPulling = ({
708
789
  // `newEvents` instead which we filter via `mergeResult.confirmedEvents`
709
790
  const confirmedNewEvents = newEvents.filter((event) =>
710
791
  mergeResult.confirmedEvents.some((confirmedEvent) =>
711
- EventSequenceNumber.isEqual(event.seqNum, confirmedEvent.seqNum),
792
+ EventSequenceNumber.Client.isEqual(event.seqNum, confirmedEvent.seqNum),
712
793
  ),
713
794
  )
714
- yield* Eventlog.updateSyncMetadata(confirmedNewEvents).pipe(UnexpectedError.mapToUnexpectedError)
795
+ yield* Eventlog.updateSyncMetadata(confirmedNewEvents).pipe(Effect.orDieDebugger)
715
796
  }
716
797
  }
717
798
 
@@ -723,138 +804,128 @@ const backgroundBackendPulling = ({
723
804
  yield* materializeEventsBatch({ batchItems: mergeResult.newEvents, deferreds: undefined })
724
805
 
725
806
  yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
807
+ }).pipe(Effect.exit)
726
808
 
727
- // Allow local pushes to be processed again
728
- if (pageInfo._tag === 'NoMore') {
729
- yield* localPushesLatch.open
730
- }
731
- })
809
+ if (Exit.isFailure(chunkExit) === true) {
810
+ yield* releasePullMutexIfHeld
811
+ return yield* Effect.failCause(chunkExit.cause)
812
+ }
732
813
 
733
- const syncState = yield* syncStateSref
734
- if (syncState === undefined) return shouldNeverHappen('Not initialized')
735
- const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: syncState.upstreamHead.global })
814
+ if (isPullPaginationComplete(pageInfo) === true) {
815
+ yield* releasePullMutexIfHeld
816
+ }
817
+ })
736
818
 
737
- const hashMaterializerResult = makeMaterializerHash({ schema, dbState })
819
+ const syncState = yield* Effect.fromNullable(yield* syncStateSref).pipe(Effect.orDieDebugger)
820
+ const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: syncState.upstreamHead.global })
738
821
 
739
- yield* syncBackend.pull(cursorInfo, { live: livePull }).pipe(
740
- // TODO only take from queue while connected
741
- Stream.tap(({ batch, pageInfo }) =>
742
- Effect.gen(function* () {
743
- // yield* Effect.spanEvent('batch', {
744
- // attributes: {
745
- // batchSize: batch.length,
746
- // batch: TRACE_VERBOSE ? batch : undefined,
747
- // },
748
- // })
749
- // NOTE we only want to take process events when the sync backend is connected
750
- // (e.g. needed for simulating being offline)
751
- // TODO remove when there's a better way to handle this in stream above
752
- yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
753
- yield* onNewPullChunk(
754
- batch.map((_) =>
755
- LiveStoreEvent.EncodedWithMeta.fromGlobal(_.eventEncoded, {
756
- syncMetadata: _.metadata,
757
- // TODO we can't really know the materializer result here yet beyond the first event batch item as we need to materialize it one by one first
758
- // This is a bug and needs to be fixed https://github.com/livestorejs/livestore/issues/503#issuecomment-3114533165
759
- materializerHashLeader: hashMaterializerResult(LiveStoreEvent.encodedFromGlobal(_.eventEncoded)),
760
- materializerHashSession: Option.none(),
761
- }),
762
- ),
763
- pageInfo,
764
- )
765
- yield* initialBlockingSyncContext.update({ processed: batch.length, pageInfo })
766
- }),
767
- ),
768
- Stream.runDrain,
769
- Effect.interruptible,
770
- )
822
+ const hashMaterializerResult = makeMaterializerHash({ schema, dbState })
771
823
 
772
- // Should only ever happen when livePull is false
773
- yield* Effect.logDebug('backend-pulling finished', { livePull })
774
- }).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pulling'))
824
+ yield* syncBackend.pull(cursorInfo, { live: livePull }).pipe(
825
+ // TODO only take from queue while connected
826
+ Stream.tap(({ batch, pageInfo }) =>
827
+ Effect.gen(function* () {
828
+ // NOTE we only want to take process events when the sync backend is connected
829
+ // (e.g. needed for simulating being offline)
830
+ // TODO remove when there's a better way to handle this in stream above
831
+ yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
832
+ yield* onNewPullChunk(
833
+ batch.map((_) =>
834
+ LiveStoreEvent.Client.EncodedWithMeta.fromGlobal(_.eventEncoded, {
835
+ syncMetadata: _.metadata,
836
+ // TODO we can't really know the materializer result here yet beyond the first event batch item as we need to materialize it one by one first
837
+ // This is a bug and needs to be fixed https://github.com/livestorejs/livestore/issues/503#issuecomment-3114533165
838
+ materializerHashLeader: hashMaterializerResult(LiveStoreEvent.Global.toClientEncoded(_.eventEncoded)),
839
+ materializerHashSession: Option.none(),
840
+ }),
841
+ ),
842
+ pageInfo,
843
+ )
844
+ yield* initialBlockingSyncContext.update({ processed: batch.length, pageInfo })
845
+ }),
846
+ ),
847
+ Stream.runDrain,
848
+ Effect.interruptible,
849
+ Effect.ensuring(releasePullMutexIfHeld),
850
+ )
851
+
852
+ // Should only ever happen when livePull is false
853
+ yield* Effect.logDebug('backend-pulling finished', { livePull })
854
+ })
775
855
 
776
- const backgroundBackendPushing = ({
856
+ const backgroundBackendPushing = Effect.fn('@livestore/common:LeaderSyncProcessor:backend-pushing')(function* ({
777
857
  syncBackendPushQueue,
778
- otelSpan,
779
858
  devtoolsLatch,
780
859
  backendPushBatchSize,
781
860
  }: {
782
- syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.EncodedWithMeta>
783
- otelSpan: otel.Span | undefined
861
+ syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.Client.EncodedWithMeta>
784
862
  devtoolsLatch: Effect.Latch | undefined
785
863
  backendPushBatchSize: number
786
- }) =>
787
- Effect.gen(function* () {
788
- const { syncBackend } = yield* LeaderThreadCtx
789
- if (syncBackend === undefined) return
864
+ }) {
865
+ const { syncBackend } = yield* LeaderThreadCtx
866
+ if (syncBackend === undefined) return
790
867
 
791
- while (true) {
792
- yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
868
+ while (true) {
869
+ yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
793
870
 
794
- const queueItems = yield* BucketQueue.takeBetween(syncBackendPushQueue, 1, backendPushBatchSize)
871
+ const queueItems = yield* BucketQueue.takeBetween(syncBackendPushQueue, 1, backendPushBatchSize)
795
872
 
796
- yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
873
+ yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
797
874
 
798
- if (devtoolsLatch !== undefined) {
799
- yield* devtoolsLatch.await
800
- }
875
+ if (devtoolsLatch !== undefined) {
876
+ yield* devtoolsLatch.await
877
+ }
801
878
 
802
- otelSpan?.addEvent('backend-push', {
803
- batchSize: queueItems.length,
804
- batch: TRACE_VERBOSE ? JSON.stringify(queueItems) : undefined,
805
- })
879
+ yield* Effect.spanEvent('backend-push', {
880
+ batchSize: queueItems.length,
881
+ ...(TRACE_VERBOSE === true ? { batch: jsonStringify(queueItems) } : {}),
882
+ })
806
883
 
807
- // Push with declarative retry/backoff using Effect schedules
808
- // - Exponential backoff starting at 1s and doubling (1s, 2s, 4s, 8s, 16s, 30s ...)
809
- // - Delay clamped at 30s (continues retrying at 30s)
810
- // - Resets automatically after successful push
811
- // TODO(metrics): expose counters/gauges for retry attempts and queue health via devtools/metrics
812
-
813
- // Only retry for transient UnexpectedError cases
814
- const isRetryable = (err: InvalidPushError | IsOfflineError) =>
815
- err._tag === 'InvalidPushError' && err.cause._tag === 'LiveStore.UnexpectedError'
816
-
817
- // Input: InvalidPushError | IsOfflineError, Output: Duration
818
- const retrySchedule: Schedule.Schedule<Duration.DurationInput, InvalidPushError | IsOfflineError> =
819
- Schedule.exponential(Duration.seconds(1)).pipe(
820
- Schedule.andThenEither(Schedule.spaced(Duration.seconds(30))), // clamp at 30 second intervals
821
- Schedule.compose(Schedule.elapsed),
822
- Schedule.whileInput(isRetryable),
823
- )
884
+ // Push with declarative retry/backoff using Effect schedules
885
+ // - Exponential backoff starting at 1s and doubling (1s, 2s, 4s, 8s, 16s, 30s ...)
886
+ // - Delay clamped at 30s (continues retrying at 30s)
887
+ // - Resets automatically after successful push
888
+ // TODO(metrics): expose counters/gauges for retry attempts and queue health via devtools/metrics
889
+ yield* Effect.gen(function* () {
890
+ const iteration = yield* Schedule.CurrentIterationMetadata
824
891
 
825
- yield* Effect.gen(function* () {
826
- const iteration = yield* Schedule.CurrentIterationMetadata
892
+ const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
827
893
 
828
- const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
894
+ const retries = iteration.recurrence
895
+ if (retries > 0 && pushResult._tag === 'Right') {
896
+ yield* Effect.spanEvent('backend-push-retry-success', { retries, batchSize: queueItems.length })
897
+ }
829
898
 
830
- const retries = iteration.recurrence
831
- if (retries > 0 && pushResult._tag === 'Right') {
832
- otelSpan?.addEvent('backend-push-retry-success', { retries, batchSize: queueItems.length })
899
+ if (pushResult._tag === 'Left') {
900
+ yield* Effect.spanEvent('backend-push-error', {
901
+ error: pushResult.left.toString(),
902
+ retries,
903
+ batchSize: queueItems.length,
904
+ })
905
+ const error = pushResult.left
906
+ if (error._tag === 'ServerAheadError') {
907
+ // It's a core part of the sync protocol that the sync backend will emit a new pull chunk alongside the ServerAheadError
908
+ yield* Effect.logDebug('handled backend-push-error (waiting for interupt caused by pull)', { error })
909
+ return yield* Effect.never
833
910
  }
834
911
 
835
- if (pushResult._tag === 'Left') {
836
- otelSpan?.addEvent('backend-push-error', {
837
- error: pushResult.left.toString(),
838
- retries,
839
- batchSize: queueItems.length,
840
- })
841
- const error = pushResult.left
842
- if (
843
- error._tag === 'IsOfflineError' ||
844
- (error._tag === 'InvalidPushError' && error.cause._tag === 'ServerAheadError')
845
- ) {
846
- // It's a core part of the sync protocol that the sync backend will emit a new pull chunk alongside the ServerAheadError
847
- yield* Effect.logDebug('handled backend-push-error (waiting for interupt caused by pull)', { error })
848
- return yield* Effect.never
849
- }
850
-
851
- return yield* error
852
- }
853
- }).pipe(Effect.retry(retrySchedule))
854
- }
855
- }).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pushing'))
912
+ return yield* error
913
+ }
914
+ }).pipe(
915
+ // Retry transient errors
916
+ Effect.retry({
917
+ schedule: Schedule.exponential(Duration.seconds(1)).pipe(
918
+ Schedule.modifyDelay((_, delay) => Duration.min(delay, Duration.seconds(30))) // Cap delay at 30s intervals.
919
+ ),
920
+ while: (error) => error._tag === 'IsOfflineError' || error._tag === 'UnknownError',
921
+ }),
922
+ // This is needed to narrow the Error type. Our retry policy runs indefinitely, but Effect.retry does not narrow the Error type.
923
+ Effect.catchIf((error) => error._tag === 'IsOfflineError' || error._tag === 'UnknownError', Effect.die),
924
+ )
925
+ }
926
+ }, Effect.interruptible)
856
927
 
857
- const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.EventSequenceNumber) => {
928
+ const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.Client.Composite) => {
858
929
  // Since we're using the session changeset rows to query for the current head,
859
930
  // we're keeping at least one row for the current head, and thus are using `<` instead of `<=`
860
931
  db.execute(sql`DELETE FROM ${SystemTables.SESSION_CHANGESET_META_TABLE} WHERE seqNumGlobal < ${newHead.global}`)
@@ -862,16 +933,16 @@ const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.EventSeque
862
933
 
863
934
  interface PullQueueSet {
864
935
  makeQueue: (
865
- cursor: EventSequenceNumber.EventSequenceNumber,
936
+ cursor: EventSequenceNumber.Client.Composite,
866
937
  ) => Effect.Effect<
867
938
  Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>,
868
- UnexpectedError,
939
+ never,
869
940
  Scope.Scope | LeaderThreadCtx
870
941
  >
871
942
  offer: (item: {
872
943
  payload: typeof SyncState.PayloadUpstream.Type
873
- leaderHead: EventSequenceNumber.EventSequenceNumber
874
- }) => Effect.Effect<void, UnexpectedError>
944
+ leaderHead: EventSequenceNumber.Client.Composite
945
+ }) => Effect.Effect<void, never>
875
946
  }
876
947
 
877
948
  const makePullQueueSet = Effect.gen(function* () {
@@ -901,17 +972,17 @@ const makePullQueueSet = Effect.gen(function* () {
901
972
 
902
973
  const payloadsSinceCursor = Array.from(cachedPayloads.entries())
903
974
  .flatMap(([seqNumStr, payloads]) =>
904
- payloads.map((payload) => ({ payload, seqNum: EventSequenceNumber.fromString(seqNumStr) })),
975
+ payloads.map((payload) => ({ payload, seqNum: EventSequenceNumber.Client.fromString(seqNumStr) })),
905
976
  )
906
- .filter(({ seqNum }) => EventSequenceNumber.isGreaterThan(seqNum, cursor))
907
- .toSorted((a, b) => EventSequenceNumber.compare(a.seqNum, b.seqNum))
977
+ .filter(({ seqNum }) => EventSequenceNumber.Client.isGreaterThan(seqNum, cursor))
978
+ .toSorted((a, b) => EventSequenceNumber.Client.compare(a.seqNum, b.seqNum))
908
979
  .map(({ payload }) => {
909
980
  if (payload._tag === 'upstream-advance') {
910
981
  return {
911
982
  payload: {
912
983
  _tag: 'upstream-advance' as const,
913
984
  newEvents: ReadonlyArray.dropWhile(payload.newEvents, (eventEncoded) =>
914
- EventSequenceNumber.isGreaterThanOrEqual(cursor, eventEncoded.seqNum),
985
+ EventSequenceNumber.Client.isGreaterThanOrEqual(cursor, eventEncoded.seqNum),
915
986
  ),
916
987
  },
917
988
  }
@@ -957,8 +1028,8 @@ const makePullQueueSet = Effect.gen(function* () {
957
1028
 
958
1029
  const offer: PullQueueSet['offer'] = (item) =>
959
1030
  Effect.gen(function* () {
960
- const seqNumStr = EventSequenceNumber.toString(item.leaderHead)
961
- if (cachedPayloads.has(seqNumStr)) {
1031
+ const seqNumStr = EventSequenceNumber.Client.toString(item.leaderHead)
1032
+ if (cachedPayloads.has(seqNumStr) === true) {
962
1033
  cachedPayloads.get(seqNumStr)!.push(item.payload)
963
1034
  } else {
964
1035
  cachedPayloads.set(seqNumStr, [item.payload])
@@ -982,29 +1053,108 @@ const makePullQueueSet = Effect.gen(function* () {
982
1053
  }
983
1054
  })
984
1055
 
1056
+ /**
1057
+ * Validate a client-provided batch before it is admitted to the leader queue.
1058
+ * Ensures the numbers form a strictly increasing chain and that the first
1059
+ * event sits ahead of the current push head.
1060
+ */
985
1061
  const validatePushBatch = (
986
- batch: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
987
- pushHead: EventSequenceNumber.EventSequenceNumber,
1062
+ batch: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>,
1063
+ pushHead: EventSequenceNumber.Client.Composite,
988
1064
  ) =>
989
1065
  Effect.gen(function* () {
990
1066
  if (batch.length === 0) {
991
1067
  return
992
1068
  }
993
1069
 
994
- // Make sure batch is monotonically increasing
1070
+ // Defensive check: callers should already provide a strictly increasing sequence
1071
+ // of event numbers.
995
1072
  for (let i = 1; i < batch.length; i++) {
996
- if (EventSequenceNumber.isGreaterThanOrEqual(batch[i - 1]!.seqNum, batch[i]!.seqNum)) {
997
- shouldNeverHappen(
998
- `Events must be ordered in monotonically ascending order by eventNum. Received: [${batch.map((e) => EventSequenceNumber.toString(e.seqNum)).join(', ')}]`,
999
- )
1073
+ if (EventSequenceNumber.Client.isGreaterThanOrEqual(batch[i - 1]!.seqNum, batch[i]!.seqNum) === true) {
1074
+ return yield* NonMonotonicBatchError.make({
1075
+ precedingSeqNum: batch[i - 1]!.seqNum,
1076
+ violatingSeqNum: batch[i]!.seqNum,
1077
+ violationIndex: i,
1078
+ sessionId: batch[i]!.sessionId,
1079
+ })
1000
1080
  }
1001
1081
  }
1002
1082
 
1003
- // Make sure smallest sequence number is > pushHead
1004
- if (EventSequenceNumber.isGreaterThanOrEqual(pushHead, batch[0]!.seqNum)) {
1083
+ // Reject stale batches whose first event is at or behind the leader's push head.
1084
+ if (EventSequenceNumber.Client.isGreaterThanOrEqual(pushHead, batch[0]!.seqNum) === true) {
1005
1085
  return yield* LeaderAheadError.make({
1006
1086
  minimumExpectedNum: pushHead,
1007
1087
  providedNum: batch[0]!.seqNum,
1088
+ sessionId: batch[0]!.sessionId,
1008
1089
  })
1009
1090
  }
1010
1091
  })
1092
+
1093
+ /**
1094
+ * Handles a BackendIdMismatchError based on the configured behavior.
1095
+ * This occurs when the sync backend has been reset and has a new identity.
1096
+ */
1097
+ const handleBackendIdMismatch = Effect.fn('@livestore/common:LeaderSyncProcessor:handleBackendIdMismatch')(function* ({
1098
+ error,
1099
+ onBackendIdMismatch,
1100
+ shutdownChannel,
1101
+ }: {
1102
+ error: BackendIdMismatchError
1103
+ onBackendIdMismatch: 'reset' | 'shutdown' | 'ignore'
1104
+ shutdownChannel: ShutdownChannel
1105
+ }) {
1106
+ const { dbEventlog, dbState } = yield* LeaderThreadCtx
1107
+
1108
+ if (onBackendIdMismatch === 'reset') {
1109
+ yield* Effect.logWarning(
1110
+ 'Sync backend identity changed (backend was reset). Clearing local storage and shutting down.',
1111
+ error,
1112
+ )
1113
+
1114
+ // Clear local databases so the client can start fresh on next boot
1115
+ yield* clearLocalDatabases({ dbEventlog, dbState })
1116
+
1117
+ // Send shutdown signal with special reason
1118
+ yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'backend-id-mismatch' })).pipe(Effect.orDie)
1119
+
1120
+ return yield* Effect.die(error)
1121
+ }
1122
+
1123
+ if (onBackendIdMismatch === 'shutdown') {
1124
+ yield* Effect.logWarning(
1125
+ 'Sync backend identity changed (backend was reset). Shutting down without clearing local storage.',
1126
+ error,
1127
+ )
1128
+
1129
+ yield* shutdownChannel.send(error).pipe(Effect.orDie)
1130
+
1131
+ return yield* Effect.die(error)
1132
+ }
1133
+
1134
+ // ignore mode
1135
+ if (LS_DEV === true) {
1136
+ yield* Effect.logDebug(
1137
+ 'Ignoring BackendIdMismatchError (sync backend was reset but client continues with stale data)',
1138
+ error,
1139
+ )
1140
+ }
1141
+ })
1142
+
1143
+ /**
1144
+ * Clears local databases (eventlog and state) so the client can start fresh on next boot.
1145
+ * This is used when the sync backend identity has changed (i.e. backend was reset).
1146
+ */
1147
+ const clearLocalDatabases = ({ dbEventlog, dbState }: { dbEventlog: SqliteDb; dbState: SqliteDb }) =>
1148
+ Effect.sync(() => {
1149
+ // Clear eventlog tables
1150
+ dbEventlog.execute(sql`DELETE FROM ${EVENTLOG_META_TABLE}`)
1151
+ dbEventlog.execute(sql`DELETE FROM ${SYNC_STATUS_TABLE}`)
1152
+
1153
+ // Drop all state tables - they'll be recreated on next boot
1154
+ const tables = dbState.select<{ name: string }>(
1155
+ sql`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`,
1156
+ )
1157
+ for (const { name } of tables) {
1158
+ dbState.execute(`DROP TABLE IF EXISTS "${name}"`)
1159
+ }
1160
+ })