@livestore/common 0.3.0-dev.4 → 0.3.0-dev.41

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 (470) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixture.d.ts +83 -221
  3. package/dist/__tests__/fixture.d.ts.map +1 -1
  4. package/dist/__tests__/fixture.js +33 -11
  5. package/dist/__tests__/fixture.js.map +1 -1
  6. package/dist/adapter-types.d.ts +132 -75
  7. package/dist/adapter-types.d.ts.map +1 -1
  8. package/dist/adapter-types.js +36 -7
  9. package/dist/adapter-types.js.map +1 -1
  10. package/dist/bounded-collections.d.ts.map +1 -1
  11. package/dist/debug-info.d.ts +1 -1
  12. package/dist/debug-info.d.ts.map +1 -1
  13. package/dist/debug-info.js +1 -0
  14. package/dist/debug-info.js.map +1 -1
  15. package/dist/devtools/devtools-messages-client-session.d.ts +390 -0
  16. package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -0
  17. package/dist/devtools/devtools-messages-client-session.js +97 -0
  18. package/dist/devtools/devtools-messages-client-session.js.map +1 -0
  19. package/dist/devtools/devtools-messages-common.d.ts +68 -0
  20. package/dist/devtools/devtools-messages-common.d.ts.map +1 -0
  21. package/dist/devtools/devtools-messages-common.js +60 -0
  22. package/dist/devtools/devtools-messages-common.js.map +1 -0
  23. package/dist/devtools/devtools-messages-leader.d.ts +394 -0
  24. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -0
  25. package/dist/devtools/devtools-messages-leader.js +147 -0
  26. package/dist/devtools/devtools-messages-leader.js.map +1 -0
  27. package/dist/devtools/devtools-messages.d.ts +3 -592
  28. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  29. package/dist/devtools/devtools-messages.js +3 -171
  30. package/dist/devtools/devtools-messages.js.map +1 -1
  31. package/dist/devtools/devtools-sessioninfo.d.ts +32 -0
  32. package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -0
  33. package/dist/devtools/devtools-sessioninfo.js +36 -0
  34. package/dist/devtools/devtools-sessioninfo.js.map +1 -0
  35. package/dist/devtools/mod.d.ts +55 -0
  36. package/dist/devtools/mod.d.ts.map +1 -0
  37. package/dist/devtools/mod.js +33 -0
  38. package/dist/devtools/mod.js.map +1 -0
  39. package/dist/index.d.ts +7 -13
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +7 -9
  42. package/dist/index.js.map +1 -1
  43. package/dist/leader-thread/LeaderSyncProcessor.d.ts +62 -0
  44. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -0
  45. package/dist/leader-thread/LeaderSyncProcessor.js +593 -0
  46. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -0
  47. package/dist/leader-thread/connection.d.ts +34 -6
  48. package/dist/leader-thread/connection.d.ts.map +1 -1
  49. package/dist/leader-thread/connection.js +22 -7
  50. package/dist/leader-thread/connection.js.map +1 -1
  51. package/dist/leader-thread/eventlog.d.ts +27 -0
  52. package/dist/leader-thread/eventlog.d.ts.map +1 -0
  53. package/dist/leader-thread/eventlog.js +119 -0
  54. package/dist/leader-thread/eventlog.js.map +1 -0
  55. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  56. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  57. package/dist/leader-thread/leader-worker-devtools.js +165 -134
  58. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  59. package/dist/leader-thread/make-leader-thread-layer.d.ts +26 -12
  60. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  61. package/dist/leader-thread/make-leader-thread-layer.js +76 -48
  62. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  63. package/dist/leader-thread/materialize-event.d.ts +16 -0
  64. package/dist/leader-thread/materialize-event.d.ts.map +1 -0
  65. package/dist/leader-thread/materialize-event.js +105 -0
  66. package/dist/leader-thread/materialize-event.js.map +1 -0
  67. package/dist/leader-thread/mod.d.ts +1 -1
  68. package/dist/leader-thread/mod.d.ts.map +1 -1
  69. package/dist/leader-thread/mod.js +1 -1
  70. package/dist/leader-thread/mod.js.map +1 -1
  71. package/dist/leader-thread/recreate-db.d.ts +4 -2
  72. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  73. package/dist/leader-thread/recreate-db.js +33 -31
  74. package/dist/leader-thread/recreate-db.js.map +1 -1
  75. package/dist/leader-thread/shutdown-channel.d.ts +2 -5
  76. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  77. package/dist/leader-thread/shutdown-channel.js +2 -4
  78. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  79. package/dist/leader-thread/types.d.ts +89 -40
  80. package/dist/leader-thread/types.d.ts.map +1 -1
  81. package/dist/leader-thread/types.js +1 -3
  82. package/dist/leader-thread/types.js.map +1 -1
  83. package/dist/make-client-session.d.ts +21 -0
  84. package/dist/make-client-session.d.ts.map +1 -0
  85. package/dist/make-client-session.js +51 -0
  86. package/dist/make-client-session.js.map +1 -0
  87. package/dist/materializer-helper.d.ts +23 -0
  88. package/dist/materializer-helper.d.ts.map +1 -0
  89. package/dist/materializer-helper.js +84 -0
  90. package/dist/materializer-helper.js.map +1 -0
  91. package/dist/otel.d.ts +2 -0
  92. package/dist/otel.d.ts.map +1 -1
  93. package/dist/otel.js +5 -0
  94. package/dist/otel.js.map +1 -1
  95. package/dist/rematerialize-from-eventlog.d.ts +14 -0
  96. package/dist/rematerialize-from-eventlog.d.ts.map +1 -0
  97. package/dist/rematerialize-from-eventlog.js +64 -0
  98. package/dist/rematerialize-from-eventlog.js.map +1 -0
  99. package/dist/schema/EventDef.d.ts +146 -0
  100. package/dist/schema/EventDef.d.ts.map +1 -0
  101. package/dist/schema/EventDef.js +58 -0
  102. package/dist/schema/EventDef.js.map +1 -0
  103. package/dist/schema/EventId.d.ts +35 -15
  104. package/dist/schema/EventId.d.ts.map +1 -1
  105. package/dist/schema/EventId.js +57 -11
  106. package/dist/schema/EventId.js.map +1 -1
  107. package/dist/schema/EventId.test.d.ts +2 -0
  108. package/dist/schema/EventId.test.d.ts.map +1 -0
  109. package/dist/schema/EventId.test.js +11 -0
  110. package/dist/schema/EventId.test.js.map +1 -0
  111. package/dist/schema/LiveStoreEvent.d.ts +255 -0
  112. package/dist/schema/LiveStoreEvent.d.ts.map +1 -0
  113. package/dist/schema/LiveStoreEvent.js +118 -0
  114. package/dist/schema/LiveStoreEvent.js.map +1 -0
  115. package/dist/schema/events.d.ts +2 -0
  116. package/dist/schema/events.d.ts.map +1 -0
  117. package/dist/schema/events.js +2 -0
  118. package/dist/schema/events.js.map +1 -0
  119. package/dist/schema/mod.d.ts +7 -5
  120. package/dist/schema/mod.d.ts.map +1 -1
  121. package/dist/schema/mod.js +7 -5
  122. package/dist/schema/mod.js.map +1 -1
  123. package/dist/schema/schema.d.ts +48 -30
  124. package/dist/schema/schema.d.ts.map +1 -1
  125. package/dist/schema/schema.js +36 -43
  126. package/dist/schema/schema.js.map +1 -1
  127. package/dist/schema/state/mod.d.ts +3 -0
  128. package/dist/schema/state/mod.d.ts.map +1 -0
  129. package/dist/schema/state/mod.js +3 -0
  130. package/dist/schema/state/mod.js.map +1 -0
  131. package/dist/schema/state/sqlite/client-document-def.d.ts +223 -0
  132. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -0
  133. package/dist/schema/state/sqlite/client-document-def.js +170 -0
  134. package/dist/schema/state/sqlite/client-document-def.js.map +1 -0
  135. package/dist/schema/state/sqlite/client-document-def.test.d.ts +2 -0
  136. package/dist/schema/state/sqlite/client-document-def.test.d.ts.map +1 -0
  137. package/dist/schema/state/sqlite/client-document-def.test.js +201 -0
  138. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -0
  139. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +69 -0
  140. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -0
  141. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +71 -0
  142. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -0
  143. package/dist/schema/state/sqlite/db-schema/ast/validate.d.ts +3 -0
  144. package/dist/schema/state/sqlite/db-schema/ast/validate.d.ts.map +1 -0
  145. package/dist/schema/state/sqlite/db-schema/ast/validate.js +12 -0
  146. package/dist/schema/state/sqlite/db-schema/ast/validate.js.map +1 -0
  147. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts +90 -0
  148. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -0
  149. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +87 -0
  150. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -0
  151. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.d.ts +2 -0
  152. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.d.ts.map +1 -0
  153. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js +29 -0
  154. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js.map +1 -0
  155. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts +90 -0
  156. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -0
  157. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +41 -0
  158. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -0
  159. package/dist/schema/state/sqlite/db-schema/hash.d.ts +2 -0
  160. package/dist/schema/state/sqlite/db-schema/hash.d.ts.map +1 -0
  161. package/dist/schema/state/sqlite/db-schema/hash.js +14 -0
  162. package/dist/schema/state/sqlite/db-schema/hash.js.map +1 -0
  163. package/dist/schema/state/sqlite/db-schema/mod.d.ts +3 -0
  164. package/dist/schema/state/sqlite/db-schema/mod.d.ts.map +1 -0
  165. package/dist/schema/state/sqlite/db-schema/mod.js +3 -0
  166. package/dist/schema/state/sqlite/db-schema/mod.js.map +1 -0
  167. package/dist/schema/state/sqlite/mod.d.ts +17 -0
  168. package/dist/schema/state/sqlite/mod.d.ts.map +1 -0
  169. package/dist/schema/state/sqlite/mod.js +41 -0
  170. package/dist/schema/state/sqlite/mod.js.map +1 -0
  171. package/dist/schema/state/sqlite/query-builder/api.d.ts +294 -0
  172. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -0
  173. package/dist/schema/state/sqlite/query-builder/api.js +6 -0
  174. package/dist/schema/state/sqlite/query-builder/api.js.map +1 -0
  175. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts +7 -0
  176. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -0
  177. package/dist/schema/state/sqlite/query-builder/astToSql.js +190 -0
  178. package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -0
  179. package/dist/schema/state/sqlite/query-builder/impl.d.ts +7 -0
  180. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -0
  181. package/dist/schema/state/sqlite/query-builder/impl.js +286 -0
  182. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -0
  183. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +87 -0
  184. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -0
  185. package/dist/schema/state/sqlite/query-builder/impl.test.js +554 -0
  186. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -0
  187. package/dist/{query-builder → schema/state/sqlite/query-builder}/mod.d.ts +7 -0
  188. package/dist/schema/state/sqlite/query-builder/mod.d.ts.map +1 -0
  189. package/dist/{query-builder → schema/state/sqlite/query-builder}/mod.js +7 -0
  190. package/dist/schema/state/sqlite/query-builder/mod.js.map +1 -0
  191. package/dist/schema/state/sqlite/schema-helpers.d.ts.map +1 -0
  192. package/dist/schema/{schema-helpers.js → state/sqlite/schema-helpers.js} +1 -1
  193. package/dist/schema/state/sqlite/schema-helpers.js.map +1 -0
  194. package/dist/schema/state/sqlite/system-tables.d.ts +574 -0
  195. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -0
  196. package/dist/schema/state/sqlite/system-tables.js +87 -0
  197. package/dist/schema/state/sqlite/system-tables.js.map +1 -0
  198. package/dist/schema/state/sqlite/table-def.d.ts +84 -0
  199. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -0
  200. package/dist/schema/state/sqlite/table-def.js +36 -0
  201. package/dist/schema/state/sqlite/table-def.js.map +1 -0
  202. package/dist/schema-management/common.d.ts +7 -7
  203. package/dist/schema-management/common.d.ts.map +1 -1
  204. package/dist/schema-management/common.js.map +1 -1
  205. package/dist/schema-management/migrations.d.ts +6 -6
  206. package/dist/schema-management/migrations.d.ts.map +1 -1
  207. package/dist/schema-management/migrations.js +33 -24
  208. package/dist/schema-management/migrations.js.map +1 -1
  209. package/dist/schema-management/validate-schema.d.ts +8 -0
  210. package/dist/schema-management/validate-schema.d.ts.map +1 -0
  211. package/dist/schema-management/validate-schema.js +39 -0
  212. package/dist/schema-management/validate-schema.js.map +1 -0
  213. package/dist/sql-queries/misc.d.ts.map +1 -1
  214. package/dist/sql-queries/sql-queries.d.ts +1 -1
  215. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  216. package/dist/sql-queries/sql-queries.js.map +1 -1
  217. package/dist/sql-queries/sql-query-builder.d.ts +1 -1
  218. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  219. package/dist/sql-queries/sql-query-builder.js.map +1 -1
  220. package/dist/sql-queries/types.d.ts +2 -1
  221. package/dist/sql-queries/types.d.ts.map +1 -1
  222. package/dist/sql-queries/types.js.map +1 -1
  223. package/dist/sync/ClientSessionSyncProcessor.d.ts +66 -0
  224. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -0
  225. package/dist/sync/ClientSessionSyncProcessor.js +209 -0
  226. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -0
  227. package/dist/sync/index.d.ts +1 -1
  228. package/dist/sync/index.d.ts.map +1 -1
  229. package/dist/sync/index.js +1 -1
  230. package/dist/sync/index.js.map +1 -1
  231. package/dist/sync/next/compact-events.d.ts.map +1 -1
  232. package/dist/sync/next/facts.d.ts +19 -19
  233. package/dist/sync/next/facts.d.ts.map +1 -1
  234. package/dist/sync/next/facts.js +3 -3
  235. package/dist/sync/next/facts.js.map +1 -1
  236. package/dist/sync/next/history-dag-common.d.ts +6 -7
  237. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  238. package/dist/sync/next/history-dag-common.js +4 -2
  239. package/dist/sync/next/history-dag-common.js.map +1 -1
  240. package/dist/sync/next/history-dag.d.ts.map +1 -1
  241. package/dist/sync/next/history-dag.js +2 -2
  242. package/dist/sync/next/history-dag.js.map +1 -1
  243. package/dist/sync/next/rebase-events.d.ts +10 -8
  244. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  245. package/dist/sync/next/rebase-events.js +11 -8
  246. package/dist/sync/next/rebase-events.js.map +1 -1
  247. package/dist/sync/next/test/compact-events.calculator.test.js +38 -33
  248. package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -1
  249. package/dist/sync/next/test/compact-events.test.js +76 -76
  250. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  251. package/dist/sync/next/test/{mutation-fixtures.d.ts → event-fixtures.d.ts} +25 -25
  252. package/dist/sync/next/test/event-fixtures.d.ts.map +1 -0
  253. package/dist/sync/next/test/{mutation-fixtures.js → event-fixtures.js} +67 -36
  254. package/dist/sync/next/test/event-fixtures.js.map +1 -0
  255. package/dist/sync/next/test/mod.d.ts +1 -1
  256. package/dist/sync/next/test/mod.d.ts.map +1 -1
  257. package/dist/sync/next/test/mod.js +1 -1
  258. package/dist/sync/next/test/mod.js.map +1 -1
  259. package/dist/sync/sync.d.ts +55 -20
  260. package/dist/sync/sync.d.ts.map +1 -1
  261. package/dist/sync/sync.js +7 -3
  262. package/dist/sync/sync.js.map +1 -1
  263. package/dist/sync/syncstate.d.ts +213 -82
  264. package/dist/sync/syncstate.d.ts.map +1 -1
  265. package/dist/sync/syncstate.js +319 -120
  266. package/dist/sync/syncstate.js.map +1 -1
  267. package/dist/sync/syncstate.test.js +295 -275
  268. package/dist/sync/syncstate.test.js.map +1 -1
  269. package/dist/sync/validate-push-payload.d.ts +2 -2
  270. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  271. package/dist/sync/validate-push-payload.js +2 -2
  272. package/dist/sync/validate-push-payload.js.map +1 -1
  273. package/dist/util.d.ts +2 -2
  274. package/dist/util.d.ts.map +1 -1
  275. package/dist/version.d.ts +1 -1
  276. package/dist/version.d.ts.map +1 -1
  277. package/dist/version.js +1 -1
  278. package/dist/version.js.map +1 -1
  279. package/package.json +13 -6
  280. package/src/__tests__/fixture.ts +36 -15
  281. package/src/adapter-types.ts +116 -83
  282. package/src/debug-info.ts +1 -0
  283. package/src/devtools/devtools-messages-client-session.ts +142 -0
  284. package/src/devtools/devtools-messages-common.ts +115 -0
  285. package/src/devtools/devtools-messages-leader.ts +191 -0
  286. package/src/devtools/devtools-messages.ts +3 -243
  287. package/src/devtools/devtools-sessioninfo.ts +101 -0
  288. package/src/devtools/mod.ts +59 -0
  289. package/src/index.ts +7 -15
  290. package/src/leader-thread/LeaderSyncProcessor.ts +933 -0
  291. package/src/leader-thread/connection.ts +54 -9
  292. package/src/leader-thread/eventlog.ts +194 -0
  293. package/src/leader-thread/leader-worker-devtools.ts +235 -191
  294. package/src/leader-thread/make-leader-thread-layer.ts +138 -78
  295. package/src/leader-thread/materialize-event.ts +169 -0
  296. package/src/leader-thread/mod.ts +1 -1
  297. package/src/leader-thread/recreate-db.ts +38 -39
  298. package/src/leader-thread/shutdown-channel.ts +2 -4
  299. package/src/leader-thread/types.ts +98 -53
  300. package/src/make-client-session.ts +119 -0
  301. package/src/materializer-helper.ts +135 -0
  302. package/src/otel.ts +8 -0
  303. package/src/rematerialize-from-eventlog.ts +117 -0
  304. package/src/schema/EventDef.ts +227 -0
  305. package/src/schema/EventId.test.ts +12 -0
  306. package/src/schema/EventId.ts +75 -15
  307. package/src/schema/LiveStoreEvent.ts +239 -0
  308. package/src/schema/events.ts +1 -0
  309. package/src/schema/mod.ts +7 -5
  310. package/src/schema/schema.ts +85 -81
  311. package/src/schema/state/mod.ts +2 -0
  312. package/src/schema/state/sqlite/client-document-def.test.ts +238 -0
  313. package/src/schema/state/sqlite/client-document-def.ts +444 -0
  314. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +142 -0
  315. package/src/schema/state/sqlite/db-schema/ast/validate.ts +13 -0
  316. package/src/schema/state/sqlite/db-schema/dsl/__snapshots__/field-defs.test.ts.snap +206 -0
  317. package/src/schema/state/sqlite/db-schema/dsl/field-defs.test.ts +35 -0
  318. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +242 -0
  319. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +222 -0
  320. package/src/schema/state/sqlite/db-schema/hash.ts +14 -0
  321. package/src/schema/state/sqlite/db-schema/mod.ts +2 -0
  322. package/src/schema/state/sqlite/mod.ts +73 -0
  323. package/src/schema/state/sqlite/query-builder/api.ts +440 -0
  324. package/src/schema/state/sqlite/query-builder/astToSql.ts +232 -0
  325. package/src/schema/state/sqlite/query-builder/impl.test.ts +608 -0
  326. package/src/schema/state/sqlite/query-builder/impl.ts +350 -0
  327. package/src/{query-builder → schema/state/sqlite/query-builder}/mod.ts +7 -0
  328. package/src/schema/{schema-helpers.ts → state/sqlite/schema-helpers.ts} +1 -1
  329. package/src/schema/state/sqlite/system-tables.ts +116 -0
  330. package/src/schema/state/sqlite/table-def.ts +197 -0
  331. package/src/schema-management/common.ts +7 -7
  332. package/src/schema-management/migrations.ts +43 -37
  333. package/src/schema-management/validate-schema.ts +61 -0
  334. package/src/sql-queries/sql-queries.ts +1 -1
  335. package/src/sql-queries/sql-query-builder.ts +1 -2
  336. package/src/sql-queries/types.ts +3 -1
  337. package/src/sync/ClientSessionSyncProcessor.ts +332 -0
  338. package/src/sync/index.ts +1 -1
  339. package/src/sync/next/facts.ts +32 -33
  340. package/src/sync/next/history-dag-common.ts +9 -5
  341. package/src/sync/next/history-dag.ts +2 -2
  342. package/src/sync/next/rebase-events.ts +22 -16
  343. package/src/sync/next/test/compact-events.calculator.test.ts +45 -45
  344. package/src/sync/next/test/compact-events.test.ts +78 -78
  345. package/src/sync/next/test/event-fixtures.ts +219 -0
  346. package/src/sync/next/test/mod.ts +1 -1
  347. package/src/sync/sync.ts +51 -19
  348. package/src/sync/syncstate.test.ts +335 -308
  349. package/src/sync/syncstate.ts +394 -212
  350. package/src/sync/validate-push-payload.ts +7 -4
  351. package/src/version.ts +1 -1
  352. package/dist/derived-mutations.d.ts +0 -109
  353. package/dist/derived-mutations.d.ts.map +0 -1
  354. package/dist/derived-mutations.js +0 -54
  355. package/dist/derived-mutations.js.map +0 -1
  356. package/dist/derived-mutations.test.d.ts +0 -2
  357. package/dist/derived-mutations.test.d.ts.map +0 -1
  358. package/dist/derived-mutations.test.js +0 -93
  359. package/dist/derived-mutations.test.js.map +0 -1
  360. package/dist/devtools/devtools-bridge.d.ts +0 -12
  361. package/dist/devtools/devtools-bridge.d.ts.map +0 -1
  362. package/dist/devtools/devtools-bridge.js +0 -2
  363. package/dist/devtools/devtools-bridge.js.map +0 -1
  364. package/dist/devtools/devtools-window-message.d.ts +0 -29
  365. package/dist/devtools/devtools-window-message.d.ts.map +0 -1
  366. package/dist/devtools/devtools-window-message.js +0 -33
  367. package/dist/devtools/devtools-window-message.js.map +0 -1
  368. package/dist/devtools/index.d.ts +0 -42
  369. package/dist/devtools/index.d.ts.map +0 -1
  370. package/dist/devtools/index.js +0 -48
  371. package/dist/devtools/index.js.map +0 -1
  372. package/dist/init-singleton-tables.d.ts +0 -4
  373. package/dist/init-singleton-tables.d.ts.map +0 -1
  374. package/dist/init-singleton-tables.js +0 -16
  375. package/dist/init-singleton-tables.js.map +0 -1
  376. package/dist/leader-thread/apply-mutation.d.ts +0 -8
  377. package/dist/leader-thread/apply-mutation.d.ts.map +0 -1
  378. package/dist/leader-thread/apply-mutation.js +0 -95
  379. package/dist/leader-thread/apply-mutation.js.map +0 -1
  380. package/dist/leader-thread/leader-sync-processor.d.ts +0 -47
  381. package/dist/leader-thread/leader-sync-processor.d.ts.map +0 -1
  382. package/dist/leader-thread/leader-sync-processor.js +0 -422
  383. package/dist/leader-thread/leader-sync-processor.js.map +0 -1
  384. package/dist/leader-thread/mutationlog.d.ts +0 -23
  385. package/dist/leader-thread/mutationlog.d.ts.map +0 -1
  386. package/dist/leader-thread/mutationlog.js +0 -27
  387. package/dist/leader-thread/mutationlog.js.map +0 -1
  388. package/dist/leader-thread/pull-queue-set.d.ts +0 -7
  389. package/dist/leader-thread/pull-queue-set.d.ts.map +0 -1
  390. package/dist/leader-thread/pull-queue-set.js +0 -39
  391. package/dist/leader-thread/pull-queue-set.js.map +0 -1
  392. package/dist/mutation.d.ts +0 -13
  393. package/dist/mutation.d.ts.map +0 -1
  394. package/dist/mutation.js +0 -57
  395. package/dist/mutation.js.map +0 -1
  396. package/dist/query-builder/api.d.ts +0 -190
  397. package/dist/query-builder/api.d.ts.map +0 -1
  398. package/dist/query-builder/api.js +0 -8
  399. package/dist/query-builder/api.js.map +0 -1
  400. package/dist/query-builder/impl.d.ts +0 -12
  401. package/dist/query-builder/impl.d.ts.map +0 -1
  402. package/dist/query-builder/impl.js +0 -244
  403. package/dist/query-builder/impl.js.map +0 -1
  404. package/dist/query-builder/impl.test.d.ts +0 -2
  405. package/dist/query-builder/impl.test.d.ts.map +0 -1
  406. package/dist/query-builder/impl.test.js +0 -212
  407. package/dist/query-builder/impl.test.js.map +0 -1
  408. package/dist/query-builder/mod.d.ts.map +0 -1
  409. package/dist/query-builder/mod.js.map +0 -1
  410. package/dist/query-info.d.ts +0 -38
  411. package/dist/query-info.d.ts.map +0 -1
  412. package/dist/query-info.js +0 -7
  413. package/dist/query-info.js.map +0 -1
  414. package/dist/rehydrate-from-mutationlog.d.ts +0 -14
  415. package/dist/rehydrate-from-mutationlog.d.ts.map +0 -1
  416. package/dist/rehydrate-from-mutationlog.js +0 -72
  417. package/dist/rehydrate-from-mutationlog.js.map +0 -1
  418. package/dist/schema/MutationEvent.d.ts +0 -191
  419. package/dist/schema/MutationEvent.d.ts.map +0 -1
  420. package/dist/schema/MutationEvent.js +0 -56
  421. package/dist/schema/MutationEvent.js.map +0 -1
  422. package/dist/schema/mutations.d.ts +0 -107
  423. package/dist/schema/mutations.d.ts.map +0 -1
  424. package/dist/schema/mutations.js +0 -42
  425. package/dist/schema/mutations.js.map +0 -1
  426. package/dist/schema/schema-helpers.d.ts.map +0 -1
  427. package/dist/schema/schema-helpers.js.map +0 -1
  428. package/dist/schema/system-tables.d.ts +0 -399
  429. package/dist/schema/system-tables.d.ts.map +0 -1
  430. package/dist/schema/system-tables.js +0 -51
  431. package/dist/schema/system-tables.js.map +0 -1
  432. package/dist/schema/table-def.d.ts +0 -156
  433. package/dist/schema/table-def.d.ts.map +0 -1
  434. package/dist/schema/table-def.js +0 -79
  435. package/dist/schema/table-def.js.map +0 -1
  436. package/dist/schema-management/validate-mutation-defs.d.ts +0 -8
  437. package/dist/schema-management/validate-mutation-defs.d.ts.map +0 -1
  438. package/dist/schema-management/validate-mutation-defs.js +0 -39
  439. package/dist/schema-management/validate-mutation-defs.js.map +0 -1
  440. package/dist/sync/client-session-sync-processor.d.ts +0 -45
  441. package/dist/sync/client-session-sync-processor.d.ts.map +0 -1
  442. package/dist/sync/client-session-sync-processor.js +0 -131
  443. package/dist/sync/client-session-sync-processor.js.map +0 -1
  444. package/dist/sync/next/test/mutation-fixtures.d.ts.map +0 -1
  445. package/dist/sync/next/test/mutation-fixtures.js.map +0 -1
  446. package/src/derived-mutations.test.ts +0 -101
  447. package/src/derived-mutations.ts +0 -166
  448. package/src/devtools/devtools-bridge.ts +0 -13
  449. package/src/devtools/devtools-window-message.ts +0 -27
  450. package/src/devtools/index.ts +0 -48
  451. package/src/init-singleton-tables.ts +0 -24
  452. package/src/leader-thread/apply-mutation.ts +0 -143
  453. package/src/leader-thread/leader-sync-processor.ts +0 -666
  454. package/src/leader-thread/mutationlog.ts +0 -42
  455. package/src/leader-thread/pull-queue-set.ts +0 -58
  456. package/src/mutation.ts +0 -81
  457. package/src/query-builder/api.ts +0 -289
  458. package/src/query-builder/impl.test.ts +0 -239
  459. package/src/query-builder/impl.ts +0 -285
  460. package/src/query-info.ts +0 -78
  461. package/src/rehydrate-from-mutationlog.ts +0 -127
  462. package/src/schema/MutationEvent.ts +0 -161
  463. package/src/schema/mutations.ts +0 -192
  464. package/src/schema/system-tables.ts +0 -97
  465. package/src/schema/table-def.ts +0 -343
  466. package/src/schema-management/validate-mutation-defs.ts +0 -63
  467. package/src/sync/client-session-sync-processor.ts +0 -207
  468. package/src/sync/next/test/mutation-fixtures.ts +0 -231
  469. package/tsconfig.json +0 -11
  470. /package/dist/schema/{schema-helpers.d.ts → state/sqlite/schema-helpers.d.ts} +0 -0
@@ -0,0 +1,933 @@
1
+ import { casesHandled, isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
2
+ import type { HttpClient, Runtime, Scope, Tracer } from '@livestore/utils/effect'
3
+ import {
4
+ BucketQueue,
5
+ Deferred,
6
+ Effect,
7
+ Exit,
8
+ FiberHandle,
9
+ OtelTracer,
10
+ Queue,
11
+ ReadonlyArray,
12
+ Stream,
13
+ Subscribable,
14
+ SubscriptionRef,
15
+ } from '@livestore/utils/effect'
16
+ import type * as otel from '@opentelemetry/api'
17
+
18
+ import type { SqliteDb } from '../adapter-types.js'
19
+ import { UnexpectedError } from '../adapter-types.js'
20
+ import type { LiveStoreSchema } from '../schema/mod.js'
21
+ import { EventId, getEventDef, LiveStoreEvent, SystemTables } from '../schema/mod.js'
22
+ import { LeaderAheadError } from '../sync/sync.js'
23
+ import * as SyncState from '../sync/syncstate.js'
24
+ import { sql } from '../util.js'
25
+ import * as Eventlog from './eventlog.js'
26
+ import { rollback } from './materialize-event.js'
27
+ import type { InitialBlockingSyncContext, LeaderSyncProcessor } from './types.js'
28
+ import { LeaderThreadCtx } from './types.js'
29
+
30
+ type LocalPushQueueItem = [
31
+ event: LiveStoreEvent.EncodedWithMeta,
32
+ deferred: Deferred.Deferred<void, LeaderAheadError> | undefined,
33
+ /** Used to determine whether the batch has become invalid due to a rejected local push batch */
34
+ generation: number,
35
+ ]
36
+
37
+ /**
38
+ * The LeaderSyncProcessor manages synchronization of events between
39
+ * the local state and the sync backend, ensuring efficient and orderly processing.
40
+ *
41
+ * In the LeaderSyncProcessor, pulling always has precedence over pushing.
42
+ *
43
+ * Responsibilities:
44
+ * - Queueing incoming local events in a localPushesQueue.
45
+ * - Broadcasting events to client sessions via pull queues.
46
+ * - Pushing events to the sync backend.
47
+ *
48
+ * Notes:
49
+ *
50
+ * local push processing:
51
+ * - localPushesQueue:
52
+ * - Maintains events in ascending order.
53
+ * - Uses `Deferred` objects to resolve/reject events based on application success.
54
+ * - Processes events from the queue, applying events in batches.
55
+ * - Controlled by a `Latch` to manage execution flow.
56
+ * - The latch closes on pull receipt and re-opens post-pull completion.
57
+ * - Processes up to `maxBatchSize` events per cycle.
58
+ *
59
+ * Currently we're advancing the db read model and eventlog in lockstep, but we could also decouple this in the future
60
+ *
61
+ * Tricky concurrency scenarios:
62
+ * - Queued local push batches becoming invalid due to a prior local push item being rejected.
63
+ * Solution: Introduce a generation number for local push batches which is used to filter out old batches items in case of rejection.
64
+ *
65
+ */
66
+ export const makeLeaderSyncProcessor = ({
67
+ schema,
68
+ dbEventlogMissing,
69
+ dbEventlog,
70
+ dbState,
71
+ dbStateMissing,
72
+ initialBlockingSyncContext,
73
+ onError,
74
+ params,
75
+ testing,
76
+ }: {
77
+ schema: LiveStoreSchema
78
+ /** Only used to know whether we can safely query dbEventlog during setup execution */
79
+ dbEventlogMissing: boolean
80
+ dbEventlog: SqliteDb
81
+ dbState: SqliteDb
82
+ /** Only used to know whether we can safely query dbState during setup execution */
83
+ dbStateMissing: boolean
84
+ initialBlockingSyncContext: InitialBlockingSyncContext
85
+ onError: 'shutdown' | 'ignore'
86
+ params: {
87
+ /**
88
+ * @default 10
89
+ */
90
+ localPushBatchSize?: number
91
+ /**
92
+ * @default 50
93
+ */
94
+ backendPushBatchSize?: number
95
+ }
96
+ testing: {
97
+ delays?: {
98
+ localPushProcessing?: Effect.Effect<void>
99
+ }
100
+ }
101
+ }): Effect.Effect<LeaderSyncProcessor, UnexpectedError, Scope.Scope> =>
102
+ Effect.gen(function* () {
103
+ const syncBackendPushQueue = yield* BucketQueue.make<LiveStoreEvent.EncodedWithMeta>()
104
+ const localPushBatchSize = params.localPushBatchSize ?? 10
105
+ const backendPushBatchSize = params.backendPushBatchSize ?? 50
106
+
107
+ const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
108
+
109
+ const isClientEvent = (eventEncoded: LiveStoreEvent.EncodedWithMeta) => {
110
+ const { eventDef } = getEventDef(schema, eventEncoded.name)
111
+ return eventDef.options.clientOnly
112
+ }
113
+
114
+ const connectedClientSessionPullQueues = yield* makePullQueueSet
115
+
116
+ /**
117
+ * Tracks generations of queued local push events.
118
+ * If a local-push batch is rejected, all subsequent push queue items with the same generation are also rejected,
119
+ * even if they would be valid on their own.
120
+ */
121
+ // TODO get rid of this in favour of the `mergeGeneration` event id field
122
+ const currentLocalPushGenerationRef = { current: 0 }
123
+
124
+ type MergeCounter = number
125
+ const mergeCounterRef = { current: dbStateMissing ? 0 : yield* getMergeCounterFromDb(dbState) }
126
+ const mergePayloads = new Map<MergeCounter, typeof SyncState.PayloadUpstream.Type>()
127
+
128
+ // This context depends on data from `boot`, we should find a better implementation to avoid this ref indirection.
129
+ const ctxRef = {
130
+ current: undefined as
131
+ | undefined
132
+ | {
133
+ otelSpan: otel.Span | undefined
134
+ span: Tracer.Span
135
+ devtoolsLatch: Effect.Latch | undefined
136
+ runtime: Runtime.Runtime<LeaderThreadCtx>
137
+ },
138
+ }
139
+
140
+ const localPushesQueue = yield* BucketQueue.make<LocalPushQueueItem>()
141
+ const localPushesLatch = yield* Effect.makeLatch(true)
142
+ const pullLatch = yield* Effect.makeLatch(true)
143
+
144
+ /**
145
+ * Additionally to the `syncStateSref` we also need the `pushHeadRef` in order to prevent old/duplicate
146
+ * events from being pushed in a scenario like this:
147
+ * - client session A pushes e1
148
+ * - leader sync processor takes a bit and hasn't yet taken e1 from the localPushesQueue
149
+ * - client session B also pushes e1 (which should be rejected)
150
+ *
151
+ * Thus the purpoe of the pushHeadRef is the guard the integrity of the local push queue
152
+ */
153
+ const pushHeadRef = { current: EventId.ROOT }
154
+ const advancePushHead = (eventId: EventId.EventId) => {
155
+ pushHeadRef.current = EventId.max(pushHeadRef.current, eventId)
156
+ }
157
+
158
+ // NOTE: New events are only pushed to sync backend after successful local push processing
159
+ const push: LeaderSyncProcessor['push'] = (newEvents, options) =>
160
+ Effect.gen(function* () {
161
+ if (newEvents.length === 0) return
162
+
163
+ yield* validatePushBatch(newEvents, pushHeadRef.current)
164
+
165
+ advancePushHead(newEvents.at(-1)!.id)
166
+
167
+ const waitForProcessing = options?.waitForProcessing ?? false
168
+ const generation = currentLocalPushGenerationRef.current
169
+
170
+ if (waitForProcessing) {
171
+ const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void, LeaderAheadError>())
172
+
173
+ const items = newEvents.map(
174
+ (eventEncoded, i) => [eventEncoded, deferreds[i], generation] as LocalPushQueueItem,
175
+ )
176
+
177
+ yield* BucketQueue.offerAll(localPushesQueue, items)
178
+
179
+ yield* Effect.all(deferreds)
180
+ } else {
181
+ const items = newEvents.map((eventEncoded) => [eventEncoded, undefined, generation] as LocalPushQueueItem)
182
+ yield* BucketQueue.offerAll(localPushesQueue, items)
183
+ }
184
+ }).pipe(
185
+ Effect.withSpan('@livestore/common:LeaderSyncProcessor:push', {
186
+ attributes: {
187
+ batchSize: newEvents.length,
188
+ batch: TRACE_VERBOSE ? newEvents : undefined,
189
+ },
190
+ links: ctxRef.current?.span ? [{ _tag: 'SpanLink', span: ctxRef.current.span, attributes: {} }] : undefined,
191
+ }),
192
+ )
193
+
194
+ const pushPartial: LeaderSyncProcessor['pushPartial'] = ({ event: { name, args }, clientId, sessionId }) =>
195
+ Effect.gen(function* () {
196
+ const syncState = yield* syncStateSref
197
+ if (syncState === undefined) return shouldNeverHappen('Not initialized')
198
+
199
+ const { eventDef } = getEventDef(schema, name)
200
+
201
+ const eventEncoded = new LiveStoreEvent.EncodedWithMeta({
202
+ name,
203
+ args,
204
+ clientId,
205
+ sessionId,
206
+ ...EventId.nextPair(syncState.localHead, eventDef.options.clientOnly),
207
+ })
208
+
209
+ yield* push([eventEncoded])
210
+ }).pipe(Effect.catchTag('LeaderAheadError', Effect.orDie))
211
+
212
+ // Starts various background loops
213
+ const boot: LeaderSyncProcessor['boot'] = Effect.gen(function* () {
214
+ const span = yield* Effect.currentSpan.pipe(Effect.orDie)
215
+ const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)))
216
+ const { devtools, shutdownChannel } = yield* LeaderThreadCtx
217
+ const runtime = yield* Effect.runtime<LeaderThreadCtx>()
218
+
219
+ ctxRef.current = {
220
+ otelSpan,
221
+ span,
222
+ devtoolsLatch: devtools.enabled ? devtools.syncBackendLatch : undefined,
223
+ runtime,
224
+ }
225
+
226
+ const initialLocalHead = dbEventlogMissing ? EventId.ROOT : Eventlog.getClientHeadFromDb(dbEventlog)
227
+
228
+ const initialBackendHead = dbEventlogMissing ? EventId.ROOT.global : Eventlog.getBackendHeadFromDb(dbEventlog)
229
+
230
+ if (initialBackendHead > initialLocalHead.global) {
231
+ return shouldNeverHappen(
232
+ `During boot the backend head (${initialBackendHead}) should never be greater than the local head (${initialLocalHead.global})`,
233
+ )
234
+ }
235
+
236
+ const pendingEvents = dbEventlogMissing
237
+ ? []
238
+ : yield* Eventlog.getEventsSince({ global: initialBackendHead, client: EventId.clientDefault })
239
+
240
+ const initialSyncState = new SyncState.SyncState({
241
+ pending: pendingEvents,
242
+ upstreamHead: { global: initialBackendHead, client: EventId.clientDefault },
243
+ localHead: initialLocalHead,
244
+ })
245
+
246
+ /** State transitions need to happen atomically, so we use a Ref to track the state */
247
+ yield* SubscriptionRef.set(syncStateSref, initialSyncState)
248
+
249
+ // Rehydrate sync queue
250
+ if (pendingEvents.length > 0) {
251
+ const globalPendingEvents = pendingEvents
252
+ // Don't sync clientOnly events
253
+ .filter((eventEncoded) => {
254
+ const { eventDef } = getEventDef(schema, eventEncoded.name)
255
+ return eventDef.options.clientOnly === false
256
+ })
257
+
258
+ if (globalPendingEvents.length > 0) {
259
+ yield* BucketQueue.offerAll(syncBackendPushQueue, globalPendingEvents)
260
+ }
261
+ }
262
+
263
+ const shutdownOnError = (cause: unknown) =>
264
+ Effect.gen(function* () {
265
+ if (onError === 'shutdown') {
266
+ yield* shutdownChannel.send(UnexpectedError.make({ cause }))
267
+ yield* Effect.die(cause)
268
+ }
269
+ })
270
+
271
+ yield* backgroundApplyLocalPushes({
272
+ localPushesLatch,
273
+ localPushesQueue,
274
+ pullLatch,
275
+ syncStateSref,
276
+ syncBackendPushQueue,
277
+ schema,
278
+ isClientEvent,
279
+ otelSpan,
280
+ currentLocalPushGenerationRef,
281
+ connectedClientSessionPullQueues,
282
+ mergeCounterRef,
283
+ mergePayloads,
284
+ localPushBatchSize,
285
+ testing: {
286
+ delay: testing?.delays?.localPushProcessing,
287
+ },
288
+ }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
289
+
290
+ const backendPushingFiberHandle = yield* FiberHandle.make()
291
+ const backendPushingEffect = backgroundBackendPushing({
292
+ syncBackendPushQueue,
293
+ otelSpan,
294
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
295
+ backendPushBatchSize,
296
+ }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError))
297
+
298
+ yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
299
+
300
+ yield* backgroundBackendPulling({
301
+ initialBackendHead,
302
+ isClientEvent,
303
+ restartBackendPushing: (filteredRebasedPending) =>
304
+ Effect.gen(function* () {
305
+ // Stop current pushing fiber
306
+ yield* FiberHandle.clear(backendPushingFiberHandle)
307
+
308
+ // Reset the sync backend push queue
309
+ yield* BucketQueue.clear(syncBackendPushQueue)
310
+ yield* BucketQueue.offerAll(syncBackendPushQueue, filteredRebasedPending)
311
+
312
+ // Restart pushing fiber
313
+ yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
314
+ }),
315
+ syncStateSref,
316
+ localPushesLatch,
317
+ pullLatch,
318
+ otelSpan,
319
+ initialBlockingSyncContext,
320
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
321
+ connectedClientSessionPullQueues,
322
+ mergeCounterRef,
323
+ mergePayloads,
324
+ advancePushHead,
325
+ }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
326
+
327
+ return { initialLeaderHead: initialLocalHead }
328
+ }).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'))
329
+
330
+ const pull: LeaderSyncProcessor['pull'] = ({ cursor }) =>
331
+ Effect.gen(function* () {
332
+ const queue = yield* pullQueue({ cursor })
333
+ return Stream.fromQueue(queue)
334
+ }).pipe(Stream.unwrapScoped)
335
+
336
+ const pullQueue: LeaderSyncProcessor['pullQueue'] = ({ cursor }) => {
337
+ const runtime = ctxRef.current?.runtime ?? shouldNeverHappen('Not initialized')
338
+ return Effect.gen(function* () {
339
+ const queue = yield* connectedClientSessionPullQueues.makeQueue
340
+ const payloadsSinceCursor = Array.from(mergePayloads.entries())
341
+ .map(([mergeCounter, payload]) => ({ payload, mergeCounter }))
342
+ .filter(({ mergeCounter }) => mergeCounter > cursor.mergeCounter)
343
+ .toSorted((a, b) => a.mergeCounter - b.mergeCounter)
344
+ .map(({ payload, mergeCounter }) => {
345
+ if (payload._tag === 'upstream-advance') {
346
+ return {
347
+ payload: {
348
+ _tag: 'upstream-advance' as const,
349
+ newEvents: ReadonlyArray.dropWhile(payload.newEvents, (eventEncoded) =>
350
+ EventId.isGreaterThanOrEqual(cursor.eventId, eventEncoded.id),
351
+ ),
352
+ },
353
+ mergeCounter,
354
+ }
355
+ } else {
356
+ return { payload, mergeCounter }
357
+ }
358
+ })
359
+
360
+ yield* queue.offerAll(payloadsSinceCursor)
361
+
362
+ return queue
363
+ }).pipe(Effect.provide(runtime))
364
+ }
365
+
366
+ const syncState = Subscribable.make({
367
+ get: Effect.gen(function* () {
368
+ const syncState = yield* syncStateSref
369
+ if (syncState === undefined) return shouldNeverHappen('Not initialized')
370
+ return syncState
371
+ }),
372
+ changes: syncStateSref.changes.pipe(Stream.filter(isNotUndefined)),
373
+ })
374
+
375
+ return {
376
+ pull,
377
+ pullQueue,
378
+ push,
379
+ pushPartial,
380
+ boot,
381
+ syncState,
382
+ getMergeCounter: () => mergeCounterRef.current,
383
+ } satisfies LeaderSyncProcessor
384
+ })
385
+
386
+ const backgroundApplyLocalPushes = ({
387
+ localPushesLatch,
388
+ localPushesQueue,
389
+ pullLatch,
390
+ syncStateSref,
391
+ syncBackendPushQueue,
392
+ schema,
393
+ isClientEvent,
394
+ otelSpan,
395
+ currentLocalPushGenerationRef,
396
+ connectedClientSessionPullQueues,
397
+ mergeCounterRef,
398
+ mergePayloads,
399
+ localPushBatchSize,
400
+ testing,
401
+ }: {
402
+ pullLatch: Effect.Latch
403
+ localPushesLatch: Effect.Latch
404
+ localPushesQueue: BucketQueue.BucketQueue<LocalPushQueueItem>
405
+ syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
406
+ syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.EncodedWithMeta>
407
+ schema: LiveStoreSchema
408
+ isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
409
+ otelSpan: otel.Span | undefined
410
+ currentLocalPushGenerationRef: { current: number }
411
+ connectedClientSessionPullQueues: PullQueueSet
412
+ mergeCounterRef: { current: number }
413
+ mergePayloads: Map<number, typeof SyncState.PayloadUpstream.Type>
414
+ localPushBatchSize: number
415
+ testing: {
416
+ delay: Effect.Effect<void> | undefined
417
+ }
418
+ }) =>
419
+ Effect.gen(function* () {
420
+ while (true) {
421
+ if (testing.delay !== undefined) {
422
+ yield* testing.delay.pipe(Effect.withSpan('localPushProcessingDelay'))
423
+ }
424
+
425
+ const batchItems = yield* BucketQueue.takeBetween(localPushesQueue, 1, localPushBatchSize)
426
+
427
+ // Wait for the backend pulling to finish
428
+ yield* localPushesLatch.await
429
+
430
+ // Prevent backend pull processing until this local push is finished
431
+ yield* pullLatch.close
432
+
433
+ // Since the generation might have changed since enqueuing, we need to filter out items with older generation
434
+ // It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
435
+ const filteredBatchItems = batchItems
436
+ .filter(([_1, _2, generation]) => generation === currentLocalPushGenerationRef.current)
437
+ .map(([eventEncoded, deferred]) => [eventEncoded, deferred] as const)
438
+
439
+ if (filteredBatchItems.length === 0) {
440
+ // console.log('dropping old-gen batch', currentLocalPushGenerationRef.current)
441
+ // Allow the backend pulling to start
442
+ yield* pullLatch.open
443
+ continue
444
+ }
445
+
446
+ const [newEvents, deferreds] = ReadonlyArray.unzip(filteredBatchItems)
447
+
448
+ const syncState = yield* syncStateSref
449
+ if (syncState === undefined) return shouldNeverHappen('Not initialized')
450
+
451
+ const mergeResult = SyncState.merge({
452
+ syncState,
453
+ payload: { _tag: 'local-push', newEvents },
454
+ isClientEvent,
455
+ isEqualEvent: LiveStoreEvent.isEqualEncoded,
456
+ })
457
+
458
+ const mergeCounter = yield* incrementMergeCounter(mergeCounterRef)
459
+
460
+ switch (mergeResult._tag) {
461
+ case 'unexpected-error': {
462
+ otelSpan?.addEvent(`[${mergeCounter}]:push:unexpected-error`, {
463
+ batchSize: newEvents.length,
464
+ newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
465
+ })
466
+ return yield* Effect.fail(mergeResult.cause)
467
+ }
468
+ case 'rebase': {
469
+ return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
470
+ }
471
+ case 'reject': {
472
+ otelSpan?.addEvent(`[${mergeCounter}]:push:reject`, {
473
+ batchSize: newEvents.length,
474
+ mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
475
+ })
476
+
477
+ // TODO: how to test this?
478
+ currentLocalPushGenerationRef.current++
479
+
480
+ const nextGeneration = currentLocalPushGenerationRef.current
481
+
482
+ const providedId = newEvents.at(0)!.id
483
+ // All subsequent pushes with same generation should be rejected as well
484
+ // We're also handling the case where the localPushQueue already contains events
485
+ // from the next generation which we preserve in the queue
486
+ const remainingEventsMatchingGeneration = yield* BucketQueue.takeSplitWhere(
487
+ localPushesQueue,
488
+ (item) => item[2] >= nextGeneration,
489
+ )
490
+
491
+ // TODO we still need to better understand and handle this scenario
492
+ if (LS_DEV && (yield* BucketQueue.size(localPushesQueue)) > 0) {
493
+ console.log('localPushesQueue is not empty', yield* BucketQueue.size(localPushesQueue))
494
+ debugger
495
+ }
496
+
497
+ const allDeferredsToReject = [
498
+ ...deferreds,
499
+ ...remainingEventsMatchingGeneration.map(([_, deferred]) => deferred),
500
+ ].filter(isNotUndefined)
501
+
502
+ yield* Effect.forEach(allDeferredsToReject, (deferred) =>
503
+ Deferred.fail(
504
+ deferred,
505
+ LeaderAheadError.make({
506
+ minimumExpectedId: mergeResult.expectedMinimumId,
507
+ providedId,
508
+ // nextGeneration,
509
+ }),
510
+ ),
511
+ )
512
+
513
+ // Allow the backend pulling to start
514
+ yield* pullLatch.open
515
+
516
+ // In this case we're skipping state update and down/upstream processing
517
+ // We've cleared the local push queue and are now waiting for new local pushes / backend pulls
518
+ continue
519
+ }
520
+ case 'advance': {
521
+ break
522
+ }
523
+ default: {
524
+ casesHandled(mergeResult)
525
+ }
526
+ }
527
+
528
+ yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
529
+
530
+ yield* connectedClientSessionPullQueues.offer({
531
+ payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
532
+ mergeCounter,
533
+ })
534
+ mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }))
535
+
536
+ otelSpan?.addEvent(`[${mergeCounter}]:push:advance`, {
537
+ batchSize: newEvents.length,
538
+ mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
539
+ })
540
+
541
+ // Don't sync clientOnly events
542
+ const filteredBatch = mergeResult.newEvents.filter((eventEncoded) => {
543
+ const { eventDef } = getEventDef(schema, eventEncoded.name)
544
+ return eventDef.options.clientOnly === false
545
+ })
546
+
547
+ yield* BucketQueue.offerAll(syncBackendPushQueue, filteredBatch)
548
+
549
+ yield* materializeEventsBatch({ batchItems: mergeResult.newEvents, deferreds })
550
+
551
+ // Allow the backend pulling to start
552
+ yield* pullLatch.open
553
+ }
554
+ })
555
+
556
+ type MaterializeEventsBatch = (_: {
557
+ batchItems: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>
558
+ /**
559
+ * The deferreds are used by the caller to know when the mutation has been processed.
560
+ * Indexes are aligned with `batchItems`
561
+ */
562
+ deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError> | undefined> | undefined
563
+ }) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
564
+
565
+ // TODO how to handle errors gracefully
566
+ const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds }) =>
567
+ Effect.gen(function* () {
568
+ const { dbState: db, dbEventlog, materializeEvent } = yield* LeaderThreadCtx
569
+
570
+ // NOTE We always start a transaction to ensure consistency between db and eventlog (even for single-item batches)
571
+ db.execute('BEGIN TRANSACTION', undefined) // Start the transaction
572
+ dbEventlog.execute('BEGIN TRANSACTION', undefined) // Start the transaction
573
+
574
+ yield* Effect.addFinalizer((exit) =>
575
+ Effect.gen(function* () {
576
+ if (Exit.isSuccess(exit)) return
577
+
578
+ // Rollback in case of an error
579
+ db.execute('ROLLBACK', undefined)
580
+ dbEventlog.execute('ROLLBACK', undefined)
581
+ }),
582
+ )
583
+
584
+ for (let i = 0; i < batchItems.length; i++) {
585
+ const { sessionChangeset } = yield* materializeEvent(batchItems[i]!)
586
+ batchItems[i]!.meta.sessionChangeset = sessionChangeset
587
+
588
+ if (deferreds?.[i] !== undefined) {
589
+ yield* Deferred.succeed(deferreds[i]!, void 0)
590
+ }
591
+ }
592
+
593
+ db.execute('COMMIT', undefined) // Commit the transaction
594
+ dbEventlog.execute('COMMIT', undefined) // Commit the transaction
595
+ }).pipe(
596
+ Effect.uninterruptible,
597
+ Effect.scoped,
598
+ Effect.withSpan('@livestore/common:LeaderSyncProcessor:materializeEventItems', {
599
+ attributes: { batchSize: batchItems.length },
600
+ }),
601
+ Effect.tapCauseLogPretty,
602
+ UnexpectedError.mapToUnexpectedError,
603
+ )
604
+
605
+ const backgroundBackendPulling = ({
606
+ initialBackendHead,
607
+ isClientEvent,
608
+ restartBackendPushing,
609
+ otelSpan,
610
+ syncStateSref,
611
+ localPushesLatch,
612
+ pullLatch,
613
+ devtoolsLatch,
614
+ initialBlockingSyncContext,
615
+ connectedClientSessionPullQueues,
616
+ mergeCounterRef,
617
+ mergePayloads,
618
+ advancePushHead,
619
+ }: {
620
+ initialBackendHead: EventId.GlobalEventId
621
+ isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
622
+ restartBackendPushing: (
623
+ filteredRebasedPending: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
624
+ ) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx | HttpClient.HttpClient>
625
+ otelSpan: otel.Span | undefined
626
+ syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
627
+ localPushesLatch: Effect.Latch
628
+ pullLatch: Effect.Latch
629
+ devtoolsLatch: Effect.Latch | undefined
630
+ initialBlockingSyncContext: InitialBlockingSyncContext
631
+ connectedClientSessionPullQueues: PullQueueSet
632
+ mergeCounterRef: { current: number }
633
+ mergePayloads: Map<number, typeof SyncState.PayloadUpstream.Type>
634
+ advancePushHead: (eventId: EventId.EventId) => void
635
+ }) =>
636
+ Effect.gen(function* () {
637
+ const { syncBackend, dbState: db, dbEventlog, schema } = yield* LeaderThreadCtx
638
+
639
+ if (syncBackend === undefined) return
640
+
641
+ const onNewPullChunk = (newEvents: LiveStoreEvent.EncodedWithMeta[], remaining: number) =>
642
+ Effect.gen(function* () {
643
+ if (newEvents.length === 0) return
644
+
645
+ if (devtoolsLatch !== undefined) {
646
+ yield* devtoolsLatch.await
647
+ }
648
+
649
+ // Prevent more local pushes from being processed until this pull is finished
650
+ yield* localPushesLatch.close
651
+
652
+ // Wait for pending local pushes to finish
653
+ yield* pullLatch.await
654
+
655
+ const syncState = yield* syncStateSref
656
+ if (syncState === undefined) return shouldNeverHappen('Not initialized')
657
+
658
+ const mergeResult = SyncState.merge({
659
+ syncState,
660
+ payload: SyncState.PayloadUpstreamAdvance.make({ newEvents }),
661
+ isClientEvent,
662
+ isEqualEvent: LiveStoreEvent.isEqualEncoded,
663
+ ignoreClientEvents: true,
664
+ })
665
+
666
+ const mergeCounter = yield* incrementMergeCounter(mergeCounterRef)
667
+
668
+ if (mergeResult._tag === 'reject') {
669
+ return shouldNeverHappen('The leader thread should never reject upstream advances')
670
+ } else if (mergeResult._tag === 'unexpected-error') {
671
+ otelSpan?.addEvent(`[${mergeCounter}]:pull:unexpected-error`, {
672
+ newEventsCount: newEvents.length,
673
+ newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
674
+ })
675
+ return yield* Effect.fail(mergeResult.cause)
676
+ }
677
+
678
+ const newBackendHead = newEvents.at(-1)!.id
679
+
680
+ Eventlog.updateBackendHead(dbEventlog, newBackendHead)
681
+
682
+ if (mergeResult._tag === 'rebase') {
683
+ otelSpan?.addEvent(`[${mergeCounter}]:pull:rebase`, {
684
+ newEventsCount: newEvents.length,
685
+ newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
686
+ rollbackCount: mergeResult.rollbackEvents.length,
687
+ mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
688
+ })
689
+
690
+ const globalRebasedPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
691
+ const { eventDef } = getEventDef(schema, event.name)
692
+ return eventDef.options.clientOnly === false
693
+ })
694
+ yield* restartBackendPushing(globalRebasedPendingEvents)
695
+
696
+ if (mergeResult.rollbackEvents.length > 0) {
697
+ yield* rollback({
698
+ dbState: db,
699
+ dbEventlog,
700
+ eventIdsToRollback: mergeResult.rollbackEvents.map((_) => _.id),
701
+ })
702
+ }
703
+
704
+ yield* connectedClientSessionPullQueues.offer({
705
+ payload: SyncState.PayloadUpstreamRebase.make({
706
+ newEvents: mergeResult.newEvents,
707
+ rollbackEvents: mergeResult.rollbackEvents,
708
+ }),
709
+ mergeCounter,
710
+ })
711
+ mergePayloads.set(
712
+ mergeCounter,
713
+ SyncState.PayloadUpstreamRebase.make({
714
+ newEvents: mergeResult.newEvents,
715
+ rollbackEvents: mergeResult.rollbackEvents,
716
+ }),
717
+ )
718
+ } else {
719
+ otelSpan?.addEvent(`[${mergeCounter}]:pull:advance`, {
720
+ newEventsCount: newEvents.length,
721
+ mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
722
+ })
723
+
724
+ yield* connectedClientSessionPullQueues.offer({
725
+ payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
726
+ mergeCounter,
727
+ })
728
+ mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }))
729
+
730
+ if (mergeResult.confirmedEvents.length > 0) {
731
+ // `mergeResult.confirmedEvents` don't contain the correct sync metadata, so we need to use
732
+ // `newEvents` instead which we filter via `mergeResult.confirmedEvents`
733
+ const confirmedNewEvents = newEvents.filter((event) =>
734
+ mergeResult.confirmedEvents.some((confirmedEvent) => EventId.isEqual(event.id, confirmedEvent.id)),
735
+ )
736
+ yield* Eventlog.updateSyncMetadata(confirmedNewEvents)
737
+ }
738
+ }
739
+
740
+ // Removes the changeset rows which are no longer needed as we'll never have to rollback beyond this point
741
+ trimChangesetRows(db, newBackendHead)
742
+
743
+ advancePushHead(mergeResult.newSyncState.localHead)
744
+
745
+ yield* materializeEventsBatch({ batchItems: mergeResult.newEvents, deferreds: undefined })
746
+
747
+ yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
748
+
749
+ // Allow local pushes to be processed again
750
+ if (remaining === 0) {
751
+ yield* localPushesLatch.open
752
+ }
753
+ })
754
+
755
+ const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo(initialBackendHead)
756
+
757
+ yield* syncBackend.pull(cursorInfo).pipe(
758
+ // TODO only take from queue while connected
759
+ Stream.tap(({ batch, remaining }) =>
760
+ Effect.gen(function* () {
761
+ // yield* Effect.spanEvent('batch', {
762
+ // attributes: {
763
+ // batchSize: batch.length,
764
+ // batch: TRACE_VERBOSE ? batch : undefined,
765
+ // },
766
+ // })
767
+
768
+ // NOTE we only want to take process events when the sync backend is connected
769
+ // (e.g. needed for simulating being offline)
770
+ // TODO remove when there's a better way to handle this in stream above
771
+ yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
772
+
773
+ yield* onNewPullChunk(
774
+ batch.map((_) => LiveStoreEvent.EncodedWithMeta.fromGlobal(_.eventEncoded, _.metadata)),
775
+ remaining,
776
+ )
777
+
778
+ yield* initialBlockingSyncContext.update({ processed: batch.length, remaining })
779
+ }),
780
+ ),
781
+ Stream.runDrain,
782
+ Effect.interruptible,
783
+ )
784
+ }).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pulling'))
785
+
786
+ const backgroundBackendPushing = ({
787
+ syncBackendPushQueue,
788
+ otelSpan,
789
+ devtoolsLatch,
790
+ backendPushBatchSize,
791
+ }: {
792
+ syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.EncodedWithMeta>
793
+ otelSpan: otel.Span | undefined
794
+ devtoolsLatch: Effect.Latch | undefined
795
+ backendPushBatchSize: number
796
+ }) =>
797
+ Effect.gen(function* () {
798
+ const { syncBackend } = yield* LeaderThreadCtx
799
+ if (syncBackend === undefined) return
800
+
801
+ while (true) {
802
+ yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
803
+
804
+ const queueItems = yield* BucketQueue.takeBetween(syncBackendPushQueue, 1, backendPushBatchSize)
805
+
806
+ yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
807
+
808
+ if (devtoolsLatch !== undefined) {
809
+ yield* devtoolsLatch.await
810
+ }
811
+
812
+ otelSpan?.addEvent('backend-push', {
813
+ batchSize: queueItems.length,
814
+ batch: TRACE_VERBOSE ? JSON.stringify(queueItems) : undefined,
815
+ })
816
+
817
+ // TODO handle push errors (should only happen during concurrent pull+push)
818
+ const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
819
+
820
+ if (pushResult._tag === 'Left') {
821
+ if (LS_DEV) {
822
+ yield* Effect.logDebug('handled backend-push-error', { error: pushResult.left.toString() })
823
+ }
824
+ otelSpan?.addEvent('backend-push-error', { error: pushResult.left.toString() })
825
+ // wait for interrupt caused by background pulling which will then restart pushing
826
+ return yield* Effect.never
827
+ }
828
+ }
829
+ }).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pushing'))
830
+
831
+ const trimChangesetRows = (db: SqliteDb, newHead: EventId.EventId) => {
832
+ // Since we're using the session changeset rows to query for the current head,
833
+ // we're keeping at least one row for the current head, and thus are using `<` instead of `<=`
834
+ db.execute(sql`DELETE FROM ${SystemTables.SESSION_CHANGESET_META_TABLE} WHERE idGlobal < ${newHead.global}`)
835
+ }
836
+
837
+ interface PullQueueSet {
838
+ makeQueue: Effect.Effect<
839
+ Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>,
840
+ UnexpectedError,
841
+ Scope.Scope | LeaderThreadCtx
842
+ >
843
+ offer: (item: {
844
+ payload: typeof SyncState.PayloadUpstream.Type
845
+ mergeCounter: number
846
+ }) => Effect.Effect<void, UnexpectedError>
847
+ }
848
+
849
+ const makePullQueueSet = Effect.gen(function* () {
850
+ const set = new Set<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>>()
851
+
852
+ yield* Effect.addFinalizer(() =>
853
+ Effect.gen(function* () {
854
+ for (const queue of set) {
855
+ yield* Queue.shutdown(queue)
856
+ }
857
+
858
+ set.clear()
859
+ }),
860
+ )
861
+
862
+ const makeQueue: PullQueueSet['makeQueue'] = Effect.gen(function* () {
863
+ const queue = yield* Queue.unbounded<{
864
+ payload: typeof SyncState.PayloadUpstream.Type
865
+ mergeCounter: number
866
+ }>().pipe(Effect.acquireRelease(Queue.shutdown))
867
+
868
+ yield* Effect.addFinalizer(() => Effect.sync(() => set.delete(queue)))
869
+
870
+ set.add(queue)
871
+
872
+ return queue
873
+ })
874
+
875
+ const offer: PullQueueSet['offer'] = (item) =>
876
+ Effect.gen(function* () {
877
+ // Short-circuit if the payload is an empty upstream advance
878
+ if (item.payload._tag === 'upstream-advance' && item.payload.newEvents.length === 0) {
879
+ return
880
+ }
881
+
882
+ for (const queue of set) {
883
+ yield* Queue.offer(queue, item)
884
+ }
885
+ })
886
+
887
+ return {
888
+ makeQueue,
889
+ offer,
890
+ }
891
+ })
892
+
893
+ const incrementMergeCounter = (mergeCounterRef: { current: number }) =>
894
+ Effect.gen(function* () {
895
+ const { dbState } = yield* LeaderThreadCtx
896
+ mergeCounterRef.current++
897
+ dbState.execute(
898
+ sql`INSERT OR REPLACE INTO ${SystemTables.LEADER_MERGE_COUNTER_TABLE} (id, mergeCounter) VALUES (0, ${mergeCounterRef.current})`,
899
+ )
900
+ return mergeCounterRef.current
901
+ })
902
+
903
+ const getMergeCounterFromDb = (dbState: SqliteDb) =>
904
+ Effect.gen(function* () {
905
+ const result = dbState.select<{ mergeCounter: number }>(
906
+ sql`SELECT mergeCounter FROM ${SystemTables.LEADER_MERGE_COUNTER_TABLE} WHERE id = 0`,
907
+ )
908
+ return result[0]?.mergeCounter ?? 0
909
+ })
910
+
911
+ const validatePushBatch = (batch: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>, pushHead: EventId.EventId) =>
912
+ Effect.gen(function* () {
913
+ if (batch.length === 0) {
914
+ return
915
+ }
916
+
917
+ // Make sure batch is monotonically increasing
918
+ for (let i = 1; i < batch.length; i++) {
919
+ if (EventId.isGreaterThanOrEqual(batch[i - 1]!.id, batch[i]!.id)) {
920
+ shouldNeverHappen(
921
+ `Events must be ordered in monotonically ascending order by eventId. Received: [${batch.map((e) => EventId.toString(e.id)).join(', ')}]`,
922
+ )
923
+ }
924
+ }
925
+
926
+ // Make sure smallest event id is > pushHead
927
+ if (EventId.isGreaterThanOrEqual(pushHead, batch[0]!.id)) {
928
+ return yield* LeaderAheadError.make({
929
+ minimumExpectedId: pushHead,
930
+ providedId: batch[0]!.id,
931
+ })
932
+ }
933
+ })