@livestore/common 0.3.0-dev.3 → 0.3.0-dev.31

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