@livestore/common 0.3.0-dev.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (480) 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 +120 -64
  7. package/dist/adapter-types.d.ts.map +1 -1
  8. package/dist/adapter-types.js +39 -8
  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 -580
  28. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  29. package/dist/devtools/devtools-messages.js +3 -174
  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 -9
  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 +45 -30
  44. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  45. package/dist/leader-thread/LeaderSyncProcessor.js +484 -321
  46. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  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.map +1 -1
  56. package/dist/leader-thread/leader-worker-devtools.js +155 -80
  57. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  58. package/dist/leader-thread/make-leader-thread-layer.d.ts +22 -9
  59. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  60. package/dist/leader-thread/make-leader-thread-layer.js +67 -45
  61. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  62. package/dist/leader-thread/materialize-event.d.ts +16 -0
  63. package/dist/leader-thread/materialize-event.d.ts.map +1 -0
  64. package/dist/leader-thread/materialize-event.js +109 -0
  65. package/dist/leader-thread/materialize-event.js.map +1 -0
  66. package/dist/leader-thread/mod.d.ts +1 -1
  67. package/dist/leader-thread/mod.d.ts.map +1 -1
  68. package/dist/leader-thread/mod.js +1 -1
  69. package/dist/leader-thread/mod.js.map +1 -1
  70. package/dist/leader-thread/recreate-db.d.ts +4 -2
  71. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  72. package/dist/leader-thread/recreate-db.js +28 -32
  73. package/dist/leader-thread/recreate-db.js.map +1 -1
  74. package/dist/leader-thread/shutdown-channel.d.ts +2 -5
  75. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  76. package/dist/leader-thread/shutdown-channel.js +2 -4
  77. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  78. package/dist/leader-thread/types.d.ts +85 -38
  79. package/dist/leader-thread/types.d.ts.map +1 -1
  80. package/dist/leader-thread/types.js +1 -3
  81. package/dist/leader-thread/types.js.map +1 -1
  82. package/dist/make-client-session.d.ts +23 -0
  83. package/dist/make-client-session.d.ts.map +1 -0
  84. package/dist/make-client-session.js +57 -0
  85. package/dist/make-client-session.js.map +1 -0
  86. package/dist/materializer-helper.d.ts +23 -0
  87. package/dist/materializer-helper.d.ts.map +1 -0
  88. package/dist/materializer-helper.js +86 -0
  89. package/dist/materializer-helper.js.map +1 -0
  90. package/dist/otel.d.ts +2 -0
  91. package/dist/otel.d.ts.map +1 -1
  92. package/dist/otel.js +5 -0
  93. package/dist/otel.js.map +1 -1
  94. package/dist/rematerialize-from-eventlog.d.ts +14 -0
  95. package/dist/rematerialize-from-eventlog.d.ts.map +1 -0
  96. package/dist/rematerialize-from-eventlog.js +64 -0
  97. package/dist/rematerialize-from-eventlog.js.map +1 -0
  98. package/dist/schema/EventDef.d.ts +146 -0
  99. package/dist/schema/EventDef.d.ts.map +1 -0
  100. package/dist/schema/EventDef.js +58 -0
  101. package/dist/schema/EventDef.js.map +1 -0
  102. package/dist/schema/EventSequenceNumber.d.ts +57 -0
  103. package/dist/schema/EventSequenceNumber.d.ts.map +1 -0
  104. package/dist/schema/EventSequenceNumber.js +82 -0
  105. package/dist/schema/EventSequenceNumber.js.map +1 -0
  106. package/dist/schema/EventSequenceNumber.test.d.ts +2 -0
  107. package/dist/schema/EventSequenceNumber.test.d.ts.map +1 -0
  108. package/dist/schema/EventSequenceNumber.test.js +11 -0
  109. package/dist/schema/EventSequenceNumber.test.js.map +1 -0
  110. package/dist/schema/LiveStoreEvent.d.ts +257 -0
  111. package/dist/schema/LiveStoreEvent.d.ts.map +1 -0
  112. package/dist/schema/LiveStoreEvent.js +117 -0
  113. package/dist/schema/LiveStoreEvent.js.map +1 -0
  114. package/dist/schema/events.d.ts +2 -0
  115. package/dist/schema/events.d.ts.map +1 -0
  116. package/dist/schema/events.js +2 -0
  117. package/dist/schema/events.js.map +1 -0
  118. package/dist/schema/mod.d.ts +8 -6
  119. package/dist/schema/mod.d.ts.map +1 -1
  120. package/dist/schema/mod.js +8 -6
  121. package/dist/schema/mod.js.map +1 -1
  122. package/dist/schema/schema.d.ts +50 -32
  123. package/dist/schema/schema.d.ts.map +1 -1
  124. package/dist/schema/schema.js +36 -43
  125. package/dist/schema/schema.js.map +1 -1
  126. package/dist/schema/state/mod.d.ts +3 -0
  127. package/dist/schema/state/mod.d.ts.map +1 -0
  128. package/dist/schema/state/mod.js +3 -0
  129. package/dist/schema/state/mod.js.map +1 -0
  130. package/dist/schema/state/sqlite/client-document-def.d.ts +223 -0
  131. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -0
  132. package/dist/schema/state/sqlite/client-document-def.js +170 -0
  133. package/dist/schema/state/sqlite/client-document-def.js.map +1 -0
  134. package/dist/schema/state/sqlite/client-document-def.test.d.ts +2 -0
  135. package/dist/schema/state/sqlite/client-document-def.test.d.ts.map +1 -0
  136. package/dist/schema/state/sqlite/client-document-def.test.js +201 -0
  137. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -0
  138. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +69 -0
  139. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -0
  140. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +71 -0
  141. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -0
  142. package/dist/schema/state/sqlite/db-schema/ast/validate.d.ts +3 -0
  143. package/dist/schema/state/sqlite/db-schema/ast/validate.d.ts.map +1 -0
  144. package/dist/schema/state/sqlite/db-schema/ast/validate.js +12 -0
  145. package/dist/schema/state/sqlite/db-schema/ast/validate.js.map +1 -0
  146. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts +90 -0
  147. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -0
  148. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +87 -0
  149. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -0
  150. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.d.ts +2 -0
  151. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.d.ts.map +1 -0
  152. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js +29 -0
  153. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js.map +1 -0
  154. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts +90 -0
  155. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -0
  156. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +41 -0
  157. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -0
  158. package/dist/schema/state/sqlite/db-schema/hash.d.ts +2 -0
  159. package/dist/schema/state/sqlite/db-schema/hash.d.ts.map +1 -0
  160. package/dist/schema/state/sqlite/db-schema/hash.js +14 -0
  161. package/dist/schema/state/sqlite/db-schema/hash.js.map +1 -0
  162. package/dist/schema/state/sqlite/db-schema/mod.d.ts +3 -0
  163. package/dist/schema/state/sqlite/db-schema/mod.d.ts.map +1 -0
  164. package/dist/schema/state/sqlite/db-schema/mod.js +3 -0
  165. package/dist/schema/state/sqlite/db-schema/mod.js.map +1 -0
  166. package/dist/schema/state/sqlite/mod.d.ts +17 -0
  167. package/dist/schema/state/sqlite/mod.d.ts.map +1 -0
  168. package/dist/schema/state/sqlite/mod.js +41 -0
  169. package/dist/schema/state/sqlite/mod.js.map +1 -0
  170. package/dist/schema/state/sqlite/query-builder/api.d.ts +294 -0
  171. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -0
  172. package/dist/schema/state/sqlite/query-builder/api.js +6 -0
  173. package/dist/schema/state/sqlite/query-builder/api.js.map +1 -0
  174. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts +7 -0
  175. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -0
  176. package/dist/schema/state/sqlite/query-builder/astToSql.js +190 -0
  177. package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -0
  178. package/dist/schema/state/sqlite/query-builder/impl.d.ts +7 -0
  179. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -0
  180. package/dist/schema/state/sqlite/query-builder/impl.js +286 -0
  181. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -0
  182. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +87 -0
  183. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -0
  184. package/dist/schema/state/sqlite/query-builder/impl.test.js +563 -0
  185. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -0
  186. package/dist/{query-builder → schema/state/sqlite/query-builder}/mod.d.ts +7 -0
  187. package/dist/schema/state/sqlite/query-builder/mod.d.ts.map +1 -0
  188. package/dist/{query-builder → schema/state/sqlite/query-builder}/mod.js +7 -0
  189. package/dist/schema/state/sqlite/query-builder/mod.js.map +1 -0
  190. package/dist/schema/state/sqlite/schema-helpers.d.ts.map +1 -0
  191. package/dist/schema/{schema-helpers.js → state/sqlite/schema-helpers.js} +1 -1
  192. package/dist/schema/state/sqlite/schema-helpers.js.map +1 -0
  193. package/dist/schema/state/sqlite/system-tables.d.ts +574 -0
  194. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -0
  195. package/dist/schema/state/sqlite/system-tables.js +88 -0
  196. package/dist/schema/state/sqlite/system-tables.js.map +1 -0
  197. package/dist/schema/state/sqlite/table-def.d.ts +84 -0
  198. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -0
  199. package/dist/schema/state/sqlite/table-def.js +36 -0
  200. package/dist/schema/state/sqlite/table-def.js.map +1 -0
  201. package/dist/schema-management/common.d.ts +7 -7
  202. package/dist/schema-management/common.d.ts.map +1 -1
  203. package/dist/schema-management/common.js.map +1 -1
  204. package/dist/schema-management/migrations.d.ts +6 -6
  205. package/dist/schema-management/migrations.d.ts.map +1 -1
  206. package/dist/schema-management/migrations.js +27 -18
  207. package/dist/schema-management/migrations.js.map +1 -1
  208. package/dist/schema-management/validate-schema.d.ts +8 -0
  209. package/dist/schema-management/validate-schema.d.ts.map +1 -0
  210. package/dist/schema-management/validate-schema.js +39 -0
  211. package/dist/schema-management/validate-schema.js.map +1 -0
  212. package/dist/sql-queries/misc.d.ts.map +1 -1
  213. package/dist/sql-queries/sql-queries.d.ts +1 -1
  214. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  215. package/dist/sql-queries/sql-queries.js.map +1 -1
  216. package/dist/sql-queries/sql-query-builder.d.ts +1 -1
  217. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  218. package/dist/sql-queries/sql-query-builder.js.map +1 -1
  219. package/dist/sql-queries/types.d.ts +2 -1
  220. package/dist/sql-queries/types.d.ts.map +1 -1
  221. package/dist/sql-queries/types.js.map +1 -1
  222. package/dist/sync/ClientSessionSyncProcessor.d.ts +40 -19
  223. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  224. package/dist/sync/ClientSessionSyncProcessor.js +150 -72
  225. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  226. package/dist/sync/next/compact-events.d.ts.map +1 -1
  227. package/dist/sync/next/compact-events.js +38 -35
  228. package/dist/sync/next/compact-events.js.map +1 -1
  229. package/dist/sync/next/facts.d.ts +21 -21
  230. package/dist/sync/next/facts.d.ts.map +1 -1
  231. package/dist/sync/next/facts.js +11 -11
  232. package/dist/sync/next/facts.js.map +1 -1
  233. package/dist/sync/next/history-dag-common.d.ts +9 -7
  234. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  235. package/dist/sync/next/history-dag-common.js +10 -5
  236. package/dist/sync/next/history-dag-common.js.map +1 -1
  237. package/dist/sync/next/history-dag.d.ts +0 -2
  238. package/dist/sync/next/history-dag.d.ts.map +1 -1
  239. package/dist/sync/next/history-dag.js +16 -14
  240. package/dist/sync/next/history-dag.js.map +1 -1
  241. package/dist/sync/next/rebase-events.d.ts +10 -8
  242. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  243. package/dist/sync/next/rebase-events.js +18 -10
  244. package/dist/sync/next/rebase-events.js.map +1 -1
  245. package/dist/sync/next/test/compact-events.calculator.test.js +39 -34
  246. package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -1
  247. package/dist/sync/next/test/compact-events.test.js +77 -77
  248. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  249. package/dist/sync/next/test/{mutation-fixtures.d.ts → event-fixtures.d.ts} +35 -25
  250. package/dist/sync/next/test/event-fixtures.d.ts.map +1 -0
  251. package/dist/sync/next/test/{mutation-fixtures.js → event-fixtures.js} +81 -38
  252. package/dist/sync/next/test/event-fixtures.js.map +1 -0
  253. package/dist/sync/next/test/mod.d.ts +1 -1
  254. package/dist/sync/next/test/mod.d.ts.map +1 -1
  255. package/dist/sync/next/test/mod.js +1 -1
  256. package/dist/sync/next/test/mod.js.map +1 -1
  257. package/dist/sync/sync.d.ts +46 -21
  258. package/dist/sync/sync.d.ts.map +1 -1
  259. package/dist/sync/sync.js +10 -6
  260. package/dist/sync/sync.js.map +1 -1
  261. package/dist/sync/syncstate.d.ts +213 -82
  262. package/dist/sync/syncstate.d.ts.map +1 -1
  263. package/dist/sync/syncstate.js +337 -139
  264. package/dist/sync/syncstate.js.map +1 -1
  265. package/dist/sync/syncstate.test.js +309 -286
  266. package/dist/sync/syncstate.test.js.map +1 -1
  267. package/dist/sync/validate-push-payload.d.ts +2 -2
  268. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  269. package/dist/sync/validate-push-payload.js +4 -4
  270. package/dist/sync/validate-push-payload.js.map +1 -1
  271. package/dist/util.d.ts +2 -2
  272. package/dist/util.d.ts.map +1 -1
  273. package/dist/version.d.ts +2 -2
  274. package/dist/version.d.ts.map +1 -1
  275. package/dist/version.js +2 -2
  276. package/dist/version.js.map +1 -1
  277. package/package.json +10 -4
  278. package/src/__tests__/fixture.ts +36 -15
  279. package/src/adapter-types.ts +107 -68
  280. package/src/debug-info.ts +1 -0
  281. package/src/devtools/devtools-messages-client-session.ts +142 -0
  282. package/src/devtools/devtools-messages-common.ts +115 -0
  283. package/src/devtools/devtools-messages-leader.ts +191 -0
  284. package/src/devtools/devtools-messages.ts +3 -246
  285. package/src/devtools/devtools-sessioninfo.ts +101 -0
  286. package/src/devtools/mod.ts +59 -0
  287. package/src/index.ts +7 -9
  288. package/src/leader-thread/LeaderSyncProcessor.ts +738 -477
  289. package/src/leader-thread/connection.ts +54 -9
  290. package/src/leader-thread/eventlog.ts +199 -0
  291. package/src/leader-thread/leader-worker-devtools.ts +227 -104
  292. package/src/leader-thread/make-leader-thread-layer.ts +121 -72
  293. package/src/leader-thread/materialize-event.ts +173 -0
  294. package/src/leader-thread/mod.ts +1 -1
  295. package/src/leader-thread/recreate-db.ts +33 -38
  296. package/src/leader-thread/shutdown-channel.ts +2 -4
  297. package/src/leader-thread/types.ts +94 -48
  298. package/src/make-client-session.ts +136 -0
  299. package/src/materializer-helper.ts +138 -0
  300. package/src/otel.ts +8 -0
  301. package/src/rematerialize-from-eventlog.ts +117 -0
  302. package/src/schema/EventDef.ts +227 -0
  303. package/src/schema/EventSequenceNumber.test.ts +12 -0
  304. package/src/schema/EventSequenceNumber.ts +121 -0
  305. package/src/schema/LiveStoreEvent.ts +240 -0
  306. package/src/schema/events.ts +1 -0
  307. package/src/schema/mod.ts +8 -6
  308. package/src/schema/schema.ts +88 -84
  309. package/src/schema/state/mod.ts +2 -0
  310. package/src/schema/state/sqlite/client-document-def.test.ts +238 -0
  311. package/src/schema/state/sqlite/client-document-def.ts +444 -0
  312. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +142 -0
  313. package/src/schema/state/sqlite/db-schema/ast/validate.ts +13 -0
  314. package/src/schema/state/sqlite/db-schema/dsl/__snapshots__/field-defs.test.ts.snap +206 -0
  315. package/src/schema/state/sqlite/db-schema/dsl/field-defs.test.ts +35 -0
  316. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +242 -0
  317. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +222 -0
  318. package/src/schema/state/sqlite/db-schema/hash.ts +14 -0
  319. package/src/schema/state/sqlite/db-schema/mod.ts +2 -0
  320. package/src/schema/state/sqlite/mod.ts +73 -0
  321. package/src/schema/state/sqlite/query-builder/api.ts +440 -0
  322. package/src/schema/state/sqlite/query-builder/astToSql.ts +232 -0
  323. package/src/schema/state/sqlite/query-builder/impl.test.ts +617 -0
  324. package/src/schema/state/sqlite/query-builder/impl.ts +351 -0
  325. package/src/{query-builder → schema/state/sqlite/query-builder}/mod.ts +7 -0
  326. package/src/schema/{schema-helpers.ts → state/sqlite/schema-helpers.ts} +1 -1
  327. package/src/schema/state/sqlite/system-tables.ts +117 -0
  328. package/src/schema/state/sqlite/table-def.ts +197 -0
  329. package/src/schema-management/common.ts +7 -7
  330. package/src/schema-management/migrations.ts +37 -31
  331. package/src/schema-management/validate-schema.ts +61 -0
  332. package/src/sql-queries/sql-queries.ts +1 -1
  333. package/src/sql-queries/sql-query-builder.ts +1 -2
  334. package/src/sql-queries/types.ts +3 -1
  335. package/src/sync/ClientSessionSyncProcessor.ts +220 -94
  336. package/src/sync/next/compact-events.ts +38 -35
  337. package/src/sync/next/facts.ts +43 -41
  338. package/src/sync/next/history-dag-common.ts +17 -10
  339. package/src/sync/next/history-dag.ts +16 -17
  340. package/src/sync/next/rebase-events.ts +29 -17
  341. package/src/sync/next/test/compact-events.calculator.test.ts +46 -46
  342. package/src/sync/next/test/compact-events.test.ts +79 -79
  343. package/src/sync/next/test/event-fixtures.ts +226 -0
  344. package/src/sync/next/test/mod.ts +1 -1
  345. package/src/sync/sync.ts +46 -21
  346. package/src/sync/syncstate.test.ts +346 -320
  347. package/src/sync/syncstate.ts +422 -230
  348. package/src/sync/validate-push-payload.ts +6 -6
  349. package/src/version.ts +2 -2
  350. package/dist/derived-mutations.d.ts +0 -109
  351. package/dist/derived-mutations.d.ts.map +0 -1
  352. package/dist/derived-mutations.js +0 -54
  353. package/dist/derived-mutations.js.map +0 -1
  354. package/dist/derived-mutations.test.d.ts +0 -2
  355. package/dist/derived-mutations.test.d.ts.map +0 -1
  356. package/dist/derived-mutations.test.js +0 -93
  357. package/dist/derived-mutations.test.js.map +0 -1
  358. package/dist/devtools/devtools-bridge.d.ts +0 -13
  359. package/dist/devtools/devtools-bridge.d.ts.map +0 -1
  360. package/dist/devtools/devtools-bridge.js +0 -2
  361. package/dist/devtools/devtools-bridge.js.map +0 -1
  362. package/dist/devtools/devtools-window-message.d.ts +0 -29
  363. package/dist/devtools/devtools-window-message.d.ts.map +0 -1
  364. package/dist/devtools/devtools-window-message.js +0 -33
  365. package/dist/devtools/devtools-window-message.js.map +0 -1
  366. package/dist/devtools/index.d.ts +0 -42
  367. package/dist/devtools/index.d.ts.map +0 -1
  368. package/dist/devtools/index.js +0 -48
  369. package/dist/devtools/index.js.map +0 -1
  370. package/dist/init-singleton-tables.d.ts +0 -4
  371. package/dist/init-singleton-tables.d.ts.map +0 -1
  372. package/dist/init-singleton-tables.js +0 -16
  373. package/dist/init-singleton-tables.js.map +0 -1
  374. package/dist/leader-thread/apply-mutation.d.ts +0 -11
  375. package/dist/leader-thread/apply-mutation.d.ts.map +0 -1
  376. package/dist/leader-thread/apply-mutation.js +0 -107
  377. package/dist/leader-thread/apply-mutation.js.map +0 -1
  378. package/dist/leader-thread/leader-sync-processor.d.ts +0 -47
  379. package/dist/leader-thread/leader-sync-processor.d.ts.map +0 -1
  380. package/dist/leader-thread/leader-sync-processor.js +0 -430
  381. package/dist/leader-thread/leader-sync-processor.js.map +0 -1
  382. package/dist/leader-thread/mutationlog.d.ts +0 -10
  383. package/dist/leader-thread/mutationlog.d.ts.map +0 -1
  384. package/dist/leader-thread/mutationlog.js +0 -28
  385. package/dist/leader-thread/mutationlog.js.map +0 -1
  386. package/dist/leader-thread/pull-queue-set.d.ts +0 -7
  387. package/dist/leader-thread/pull-queue-set.d.ts.map +0 -1
  388. package/dist/leader-thread/pull-queue-set.js +0 -39
  389. package/dist/leader-thread/pull-queue-set.js.map +0 -1
  390. package/dist/mutation.d.ts +0 -20
  391. package/dist/mutation.d.ts.map +0 -1
  392. package/dist/mutation.js +0 -57
  393. package/dist/mutation.js.map +0 -1
  394. package/dist/query-builder/api.d.ts +0 -190
  395. package/dist/query-builder/api.d.ts.map +0 -1
  396. package/dist/query-builder/api.js +0 -8
  397. package/dist/query-builder/api.js.map +0 -1
  398. package/dist/query-builder/impl.d.ts +0 -12
  399. package/dist/query-builder/impl.d.ts.map +0 -1
  400. package/dist/query-builder/impl.js +0 -244
  401. package/dist/query-builder/impl.js.map +0 -1
  402. package/dist/query-builder/impl.test.d.ts +0 -2
  403. package/dist/query-builder/impl.test.d.ts.map +0 -1
  404. package/dist/query-builder/impl.test.js +0 -212
  405. package/dist/query-builder/impl.test.js.map +0 -1
  406. package/dist/query-builder/mod.d.ts.map +0 -1
  407. package/dist/query-builder/mod.js.map +0 -1
  408. package/dist/query-info.d.ts +0 -38
  409. package/dist/query-info.d.ts.map +0 -1
  410. package/dist/query-info.js +0 -7
  411. package/dist/query-info.js.map +0 -1
  412. package/dist/rehydrate-from-mutationlog.d.ts +0 -14
  413. package/dist/rehydrate-from-mutationlog.d.ts.map +0 -1
  414. package/dist/rehydrate-from-mutationlog.js +0 -66
  415. package/dist/rehydrate-from-mutationlog.js.map +0 -1
  416. package/dist/schema/EventId.d.ts +0 -39
  417. package/dist/schema/EventId.d.ts.map +0 -1
  418. package/dist/schema/EventId.js +0 -38
  419. package/dist/schema/EventId.js.map +0 -1
  420. package/dist/schema/EventId.test.d.ts +0 -2
  421. package/dist/schema/EventId.test.d.ts.map +0 -1
  422. package/dist/schema/EventId.test.js +0 -11
  423. package/dist/schema/EventId.test.js.map +0 -1
  424. package/dist/schema/MutationEvent.d.ts +0 -166
  425. package/dist/schema/MutationEvent.d.ts.map +0 -1
  426. package/dist/schema/MutationEvent.js +0 -72
  427. package/dist/schema/MutationEvent.js.map +0 -1
  428. package/dist/schema/MutationEvent.test.d.ts +0 -2
  429. package/dist/schema/MutationEvent.test.d.ts.map +0 -1
  430. package/dist/schema/MutationEvent.test.js +0 -2
  431. package/dist/schema/MutationEvent.test.js.map +0 -1
  432. package/dist/schema/mutations.d.ts +0 -107
  433. package/dist/schema/mutations.d.ts.map +0 -1
  434. package/dist/schema/mutations.js +0 -42
  435. package/dist/schema/mutations.js.map +0 -1
  436. package/dist/schema/schema-helpers.d.ts.map +0 -1
  437. package/dist/schema/schema-helpers.js.map +0 -1
  438. package/dist/schema/system-tables.d.ts +0 -399
  439. package/dist/schema/system-tables.d.ts.map +0 -1
  440. package/dist/schema/system-tables.js +0 -59
  441. package/dist/schema/system-tables.js.map +0 -1
  442. package/dist/schema/table-def.d.ts +0 -156
  443. package/dist/schema/table-def.d.ts.map +0 -1
  444. package/dist/schema/table-def.js +0 -79
  445. package/dist/schema/table-def.js.map +0 -1
  446. package/dist/schema-management/validate-mutation-defs.d.ts +0 -8
  447. package/dist/schema-management/validate-mutation-defs.d.ts.map +0 -1
  448. package/dist/schema-management/validate-mutation-defs.js +0 -39
  449. package/dist/schema-management/validate-mutation-defs.js.map +0 -1
  450. package/dist/sync/client-session-sync-processor.d.ts +0 -45
  451. package/dist/sync/client-session-sync-processor.d.ts.map +0 -1
  452. package/dist/sync/client-session-sync-processor.js +0 -131
  453. package/dist/sync/client-session-sync-processor.js.map +0 -1
  454. package/dist/sync/next/test/mutation-fixtures.d.ts.map +0 -1
  455. package/dist/sync/next/test/mutation-fixtures.js.map +0 -1
  456. package/src/derived-mutations.test.ts +0 -101
  457. package/src/derived-mutations.ts +0 -170
  458. package/src/devtools/devtools-bridge.ts +0 -14
  459. package/src/devtools/devtools-window-message.ts +0 -27
  460. package/src/devtools/index.ts +0 -48
  461. package/src/init-singleton-tables.ts +0 -24
  462. package/src/leader-thread/apply-mutation.ts +0 -161
  463. package/src/leader-thread/mutationlog.ts +0 -46
  464. package/src/leader-thread/pull-queue-set.ts +0 -58
  465. package/src/mutation.ts +0 -91
  466. package/src/query-builder/api.ts +0 -289
  467. package/src/query-builder/impl.test.ts +0 -239
  468. package/src/query-builder/impl.ts +0 -285
  469. package/src/query-info.ts +0 -78
  470. package/src/rehydrate-from-mutationlog.ts +0 -119
  471. package/src/schema/EventId.test.ts +0 -12
  472. package/src/schema/EventId.ts +0 -60
  473. package/src/schema/MutationEvent.ts +0 -181
  474. package/src/schema/mutations.ts +0 -192
  475. package/src/schema/system-tables.ts +0 -105
  476. package/src/schema/table-def.ts +0 -343
  477. package/src/schema-management/validate-mutation-defs.ts +0 -63
  478. package/src/sync/next/test/mutation-fixtures.ts +0 -224
  479. package/tsconfig.json +0 -11
  480. /package/dist/schema/{schema-helpers.d.ts → state/sqlite/schema-helpers.d.ts} +0 -0
@@ -1,344 +1,492 @@
1
- import { shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils';
2
- import { BucketQueue, Deferred, Effect, Exit, Fiber, FiberHandle, Option, OtelTracer, ReadonlyArray, Ref, Schema, Stream, SubscriptionRef, } from '@livestore/utils/effect';
1
+ import { casesHandled, isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils';
2
+ import { BucketQueue, Deferred, Effect, Exit, FiberHandle, OtelTracer, Queue, ReadonlyArray, Stream, Subscribable, SubscriptionRef, } from '@livestore/utils/effect';
3
3
  import { UnexpectedError } from '../adapter-types.js';
4
- import { EventId, MUTATION_LOG_META_TABLE, MutationEvent, mutationLogMetaTable, SESSION_CHANGESET_META_TABLE, } from '../schema/mod.js';
5
- import { updateRows } from '../sql-queries/index.js';
6
- import { InvalidPushError } from '../sync/sync.js';
4
+ import { EventSequenceNumber, getEventDef, LiveStoreEvent, SystemTables } from '../schema/mod.js';
5
+ import { LeaderAheadError } from '../sync/sync.js';
7
6
  import * as SyncState from '../sync/syncstate.js';
8
7
  import { sql } from '../util.js';
9
- import { makeApplyMutation } from './apply-mutation.js';
10
- import { execSql } from './connection.js';
11
- import { getBackendHeadFromDb, getLocalHeadFromDb, getMutationEventsSince, updateBackendHead } from './mutationlog.js';
8
+ import * as Eventlog from './eventlog.js';
9
+ import { rollback } from './materialize-event.js';
12
10
  import { LeaderThreadCtx } from './types.js';
13
11
  /**
14
- * The general idea of the sync processor is to "follow the sync state"
15
- * and apply/rollback mutations as needed to the read model and mutation log.
16
- * The leader sync processor is also responsible for
17
- * - broadcasting mutations to client sessions via the pull queues.
18
- * - pushing mutations to the sync backend
12
+ * The LeaderSyncProcessor manages synchronization of events between
13
+ * the local state and the sync backend, ensuring efficient and orderly processing.
19
14
  *
20
- * In the leader sync processor, pulling always has precedence over pushing.
15
+ * In the LeaderSyncProcessor, pulling always has precedence over pushing.
21
16
  *
22
- * External events:
23
- * - Mutation pushed from client session
24
- * - Mutation pushed from devtools (via pushPartial)
25
- * - Mutation pulled from sync backend
17
+ * Responsibilities:
18
+ * - Queueing incoming local events in a localPushesQueue.
19
+ * - Broadcasting events to client sessions via pull queues.
20
+ * - Pushing events to the sync backend.
26
21
  *
27
- * The machine can be in the following states:
28
- * - in-sync: fully synced with remote, now idling
29
- * - applying-syncstate-advance (with pointer to current progress in case of rebase interrupt)
22
+ * Notes:
30
23
  *
31
- * Transitions:
32
- * - in-sync -> applying-syncstate-advance
33
- * - applying-syncstate-advance -> in-sync
34
- * - applying-syncstate-advance -> applying-syncstate-advance (need to interrupt previous operation)
24
+ * local push processing:
25
+ * - localPushesQueue:
26
+ * - Maintains events in ascending order.
27
+ * - Uses `Deferred` objects to resolve/reject events based on application success.
28
+ * - Processes events from the queue, applying events in batches.
29
+ * - Controlled by a `Latch` to manage execution flow.
30
+ * - The latch closes on pull receipt and re-opens post-pull completion.
31
+ * - Processes up to `maxBatchSize` events per cycle.
35
32
  *
36
- * Queuing vs interrupting behaviour:
37
- * - Operations caused by pull can never be interrupted
38
- * - Incoming pull can interrupt current push
39
- * - Incoming pull needs to wait to previous pull to finish
40
- * - Incoming push needs to wait to previous push to finish
33
+ * Currently we're advancing the db read model and eventlog in lockstep, but we could also decouple this in the future
34
+ *
35
+ * Tricky concurrency scenarios:
36
+ * - Queued local push batches becoming invalid due to a prior local push item being rejected.
37
+ * Solution: Introduce a generation number for local push batches which is used to filter out old batches items in case of rejection.
41
38
  *
42
- * Backend pushing:
43
- * - continously push to backend
44
- * - only interrupted and restarted on rebase
45
39
  */
46
- export const makeLeaderSyncProcessor = ({ schema, dbMissing, dbLog, initialBlockingSyncContext, }) => Effect.gen(function* () {
47
- const syncBackendQueue = yield* BucketQueue.make();
48
- const stateRef = yield* Ref.make({ _tag: 'init' });
49
- const semaphore = yield* Effect.makeSemaphore(1);
50
- const isLocalEvent = (mutationEventEncoded) => {
51
- const mutationDef = schema.mutations.get(mutationEventEncoded.mutation);
52
- return mutationDef.options.localOnly;
40
+ export const makeLeaderSyncProcessor = ({ schema, dbEventlogMissing, dbEventlog, dbState, dbStateMissing, initialBlockingSyncContext, onError, params, testing, }) => Effect.gen(function* () {
41
+ const syncBackendPushQueue = yield* BucketQueue.make();
42
+ const localPushBatchSize = params.localPushBatchSize ?? 10;
43
+ const backendPushBatchSize = params.backendPushBatchSize ?? 50;
44
+ const syncStateSref = yield* SubscriptionRef.make(undefined);
45
+ const isClientEvent = (eventEncoded) => {
46
+ const { eventDef } = getEventDef(schema, eventEncoded.name);
47
+ return eventDef.options.clientOnly;
53
48
  };
54
- const spanRef = { current: undefined };
55
- const applyMutationItemsRef = { current: undefined };
56
- // TODO get rid of counters once Effect semaphore ordering is fixed
57
- let counterRef = 0;
58
- let expectedCounter = 0;
59
- /*
60
- TODO: refactor
61
- - Pushes go directly into a Mailbox
62
- - Have a worker fiber that takes from the mailbox (wouldn't need a semaphore)
63
- */
64
- const waitForSyncState = (counter) => Effect.gen(function* () {
65
- // console.log('waitForSyncState: waiting for semaphore', counter)
66
- yield* semaphore.take(1);
67
- // NOTE this is a workaround to ensure the semaphore take-order is respected
68
- // TODO this needs to be fixed upstream in Effect
69
- if (counter !== expectedCounter) {
70
- console.log(`waitForSyncState: counter mismatch (expected: ${expectedCounter}, got: ${counter}), releasing semaphore`);
71
- yield* semaphore.release(1);
72
- yield* Effect.yieldNow();
73
- // Retrying...
74
- return yield* waitForSyncState(counter);
75
- }
76
- // console.log('waitForSyncState: took semaphore', counter)
77
- const state = yield* Ref.get(stateRef);
78
- if (state._tag !== 'in-sync') {
79
- return shouldNeverHappen('Expected to be in-sync but got ' + state._tag);
80
- }
81
- expectedCounter = counter + 1;
82
- return state;
83
- }).pipe(Effect.withSpan(`@livestore/common:leader-thread:syncing:waitForSyncState(${counter})`));
84
- const push = (newEvents) => Effect.gen(function* () {
85
- const counter = counterRef;
86
- counterRef++;
87
- // TODO validate batch
49
+ const connectedClientSessionPullQueues = yield* makePullQueueSet;
50
+ /**
51
+ * Tracks generations of queued local push events.
52
+ * If a local-push batch is rejected, all subsequent push queue items with the same generation are also rejected,
53
+ * even if they would be valid on their own.
54
+ */
55
+ // TODO get rid of this in favour of the `mergeGeneration` event sequence number field
56
+ const currentLocalPushGenerationRef = { current: 0 };
57
+ const mergeCounterRef = { current: dbStateMissing ? 0 : yield* getMergeCounterFromDb(dbState) };
58
+ const mergePayloads = new Map();
59
+ // This context depends on data from `boot`, we should find a better implementation to avoid this ref indirection.
60
+ const ctxRef = {
61
+ current: undefined,
62
+ };
63
+ const localPushesQueue = yield* BucketQueue.make();
64
+ const localPushesLatch = yield* Effect.makeLatch(true);
65
+ const pullLatch = yield* Effect.makeLatch(true);
66
+ /**
67
+ * Additionally to the `syncStateSref` we also need the `pushHeadRef` in order to prevent old/duplicate
68
+ * events from being pushed in a scenario like this:
69
+ * - client session A pushes e1
70
+ * - leader sync processor takes a bit and hasn't yet taken e1 from the localPushesQueue
71
+ * - client session B also pushes e1 (which should be rejected)
72
+ *
73
+ * Thus the purpoe of the pushHeadRef is the guard the integrity of the local push queue
74
+ */
75
+ const pushHeadRef = { current: EventSequenceNumber.ROOT };
76
+ const advancePushHead = (eventNum) => {
77
+ pushHeadRef.current = EventSequenceNumber.max(pushHeadRef.current, eventNum);
78
+ };
79
+ // NOTE: New events are only pushed to sync backend after successful local push processing
80
+ const push = (newEvents, options) => Effect.gen(function* () {
88
81
  if (newEvents.length === 0)
89
82
  return;
90
- const { connectedClientSessionPullQueues } = yield* LeaderThreadCtx;
91
- // TODO if there are multiple pending pushes, we should batch them together
92
- const state = yield* waitForSyncState(counter);
93
- const updateResult = SyncState.updateSyncState({
94
- syncState: state.syncState,
95
- payload: { _tag: 'local-push', newEvents },
96
- isLocalEvent,
97
- isEqualEvent: MutationEvent.isEqualEncoded,
98
- });
99
- if (updateResult._tag === 'rebase') {
100
- return shouldNeverHappen('The leader thread should never have to rebase due to a local push');
83
+ yield* validatePushBatch(newEvents, pushHeadRef.current);
84
+ advancePushHead(newEvents.at(-1).seqNum);
85
+ const waitForProcessing = options?.waitForProcessing ?? false;
86
+ const generation = currentLocalPushGenerationRef.current;
87
+ if (waitForProcessing) {
88
+ const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make());
89
+ const items = newEvents.map((eventEncoded, i) => [eventEncoded, deferreds[i], generation]);
90
+ yield* BucketQueue.offerAll(localPushesQueue, items);
91
+ yield* Effect.all(deferreds);
101
92
  }
102
- else if (updateResult._tag === 'reject') {
103
- return yield* Effect.fail(InvalidPushError.make({
104
- reason: {
105
- _tag: 'LeaderAhead',
106
- minimumExpectedId: updateResult.expectedMinimumId,
107
- providedId: newEvents.at(0).id,
108
- },
109
- }));
93
+ else {
94
+ const items = newEvents.map((eventEncoded) => [eventEncoded, undefined, generation]);
95
+ yield* BucketQueue.offerAll(localPushesQueue, items);
110
96
  }
111
- const fiber = yield* applyMutationItemsRef.current({ batchItems: updateResult.newEvents }).pipe(Effect.fork);
112
- yield* Ref.set(stateRef, {
113
- _tag: 'applying-syncstate-advance',
114
- origin: 'push',
115
- syncState: updateResult.newSyncState,
116
- fiber,
117
- });
118
- // console.log('setRef:applying-syncstate-advance after push', counter)
119
- yield* connectedClientSessionPullQueues.offer({
120
- payload: { _tag: 'upstream-advance', newEvents: updateResult.newEvents },
121
- remaining: 0,
122
- });
123
- spanRef.current?.addEvent('local-push', {
124
- batchSize: newEvents.length,
125
- updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
126
- });
127
- // Don't sync localOnly mutations
128
- const filteredBatch = updateResult.newEvents.filter((mutationEventEncoded) => {
129
- const mutationDef = schema.mutations.get(mutationEventEncoded.mutation);
130
- return mutationDef.options.localOnly === false;
131
- });
132
- yield* BucketQueue.offerAll(syncBackendQueue, filteredBatch);
133
- yield* fiber; // Waiting for the mutation to be applied
134
- }).pipe(Effect.withSpan('@livestore/common:leader-thread:syncing:local-push', {
97
+ }).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:push', {
135
98
  attributes: {
136
99
  batchSize: newEvents.length,
137
100
  batch: TRACE_VERBOSE ? newEvents : undefined,
138
101
  },
139
- links: spanRef.current
140
- ? [{ _tag: 'SpanLink', span: OtelTracer.makeExternalSpan(spanRef.current.spanContext()), attributes: {} }]
141
- : undefined,
102
+ links: ctxRef.current?.span ? [{ _tag: 'SpanLink', span: ctxRef.current.span, attributes: {} }] : undefined,
142
103
  }));
143
- const pushPartial = (mutationEventEncoded_) => Effect.gen(function* () {
144
- const state = yield* Ref.get(stateRef);
145
- if (state._tag === 'init')
104
+ const pushPartial = ({ event: { name, args }, clientId, sessionId }) => Effect.gen(function* () {
105
+ const syncState = yield* syncStateSref;
106
+ if (syncState === undefined)
146
107
  return shouldNeverHappen('Not initialized');
147
- const mutationDef = schema.mutations.get(mutationEventEncoded_.mutation) ??
148
- shouldNeverHappen(`Unknown mutation: ${mutationEventEncoded_.mutation}`);
149
- const mutationEventEncoded = new MutationEvent.EncodedWithMeta({
150
- ...mutationEventEncoded_,
151
- ...EventId.nextPair(state.syncState.localHead, mutationDef.options.localOnly),
108
+ const { eventDef } = getEventDef(schema, name);
109
+ const eventEncoded = new LiveStoreEvent.EncodedWithMeta({
110
+ name,
111
+ args,
112
+ clientId,
113
+ sessionId,
114
+ ...EventSequenceNumber.nextPair(syncState.localHead, eventDef.options.clientOnly),
152
115
  });
153
- yield* push([mutationEventEncoded]);
154
- }).pipe(Effect.catchTag('InvalidPushError', Effect.orDie));
116
+ yield* push([eventEncoded]);
117
+ }).pipe(Effect.catchTag('LeaderAheadError', Effect.orDie));
155
118
  // Starts various background loops
156
- const boot = ({ dbReady }) => Effect.gen(function* () {
157
- const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)));
158
- spanRef.current = span;
159
- const initialBackendHead = dbMissing ? EventId.ROOT.global : getBackendHeadFromDb(dbLog);
160
- const initialLocalHead = dbMissing ? EventId.ROOT : getLocalHeadFromDb(dbLog);
119
+ const boot = Effect.gen(function* () {
120
+ const span = yield* Effect.currentSpan.pipe(Effect.orDie);
121
+ const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)));
122
+ const { devtools, shutdownChannel } = yield* LeaderThreadCtx;
123
+ const runtime = yield* Effect.runtime();
124
+ ctxRef.current = {
125
+ otelSpan,
126
+ span,
127
+ devtoolsLatch: devtools.enabled ? devtools.syncBackendLatch : undefined,
128
+ runtime,
129
+ };
130
+ const initialLocalHead = dbEventlogMissing ? EventSequenceNumber.ROOT : Eventlog.getClientHeadFromDb(dbEventlog);
131
+ const initialBackendHead = dbEventlogMissing
132
+ ? EventSequenceNumber.ROOT.global
133
+ : Eventlog.getBackendHeadFromDb(dbEventlog);
161
134
  if (initialBackendHead > initialLocalHead.global) {
162
135
  return shouldNeverHappen(`During boot the backend head (${initialBackendHead}) should never be greater than the local head (${initialLocalHead.global})`);
163
136
  }
164
- const pendingMutationEvents = yield* getMutationEventsSince({
165
- global: initialBackendHead,
166
- local: EventId.localDefault,
167
- }).pipe(Effect.map(ReadonlyArray.map((_) => new MutationEvent.EncodedWithMeta(_))));
168
- const initialSyncState = {
169
- pending: pendingMutationEvents,
170
- // On the leader we don't need a rollback tail beyond `pending` items
171
- rollbackTail: [],
172
- upstreamHead: { global: initialBackendHead, local: EventId.localDefault },
137
+ const pendingEvents = dbEventlogMissing
138
+ ? []
139
+ : yield* Eventlog.getEventsSince({ global: initialBackendHead, client: EventSequenceNumber.clientDefault });
140
+ const initialSyncState = new SyncState.SyncState({
141
+ pending: pendingEvents,
142
+ upstreamHead: { global: initialBackendHead, client: EventSequenceNumber.clientDefault },
173
143
  localHead: initialLocalHead,
174
- };
144
+ });
175
145
  /** State transitions need to happen atomically, so we use a Ref to track the state */
176
- yield* Ref.set(stateRef, { _tag: 'in-sync', syncState: initialSyncState });
177
- applyMutationItemsRef.current = yield* makeApplyMutationItems({ stateRef, semaphore });
146
+ yield* SubscriptionRef.set(syncStateSref, initialSyncState);
178
147
  // Rehydrate sync queue
179
- if (pendingMutationEvents.length > 0) {
180
- const filteredBatch = pendingMutationEvents
181
- // Don't sync localOnly mutations
182
- .filter((mutationEventEncoded) => {
183
- const mutationDef = schema.mutations.get(mutationEventEncoded.mutation);
184
- return mutationDef.options.localOnly === false;
148
+ if (pendingEvents.length > 0) {
149
+ const globalPendingEvents = pendingEvents
150
+ // Don't sync clientOnly events
151
+ .filter((eventEncoded) => {
152
+ const { eventDef } = getEventDef(schema, eventEncoded.name);
153
+ return eventDef.options.clientOnly === false;
185
154
  });
186
- yield* BucketQueue.offerAll(syncBackendQueue, filteredBatch);
155
+ if (globalPendingEvents.length > 0) {
156
+ yield* BucketQueue.offerAll(syncBackendPushQueue, globalPendingEvents);
157
+ }
187
158
  }
159
+ const shutdownOnError = (cause) => Effect.gen(function* () {
160
+ if (onError === 'shutdown') {
161
+ yield* shutdownChannel.send(UnexpectedError.make({ cause }));
162
+ yield* Effect.die(cause);
163
+ }
164
+ });
165
+ yield* backgroundApplyLocalPushes({
166
+ localPushesLatch,
167
+ localPushesQueue,
168
+ pullLatch,
169
+ syncStateSref,
170
+ syncBackendPushQueue,
171
+ schema,
172
+ isClientEvent,
173
+ otelSpan,
174
+ currentLocalPushGenerationRef,
175
+ connectedClientSessionPullQueues,
176
+ mergeCounterRef,
177
+ mergePayloads,
178
+ localPushBatchSize,
179
+ testing: {
180
+ delay: testing?.delays?.localPushProcessing,
181
+ },
182
+ }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped);
188
183
  const backendPushingFiberHandle = yield* FiberHandle.make();
189
- yield* FiberHandle.run(backendPushingFiberHandle, backgroundBackendPushing({ dbReady, syncBackendQueue, span }).pipe(Effect.tapCauseLogPretty));
184
+ const backendPushingEffect = backgroundBackendPushing({
185
+ syncBackendPushQueue,
186
+ otelSpan,
187
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
188
+ backendPushBatchSize,
189
+ }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError));
190
+ yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect);
190
191
  yield* backgroundBackendPulling({
191
- dbReady,
192
192
  initialBackendHead,
193
- isLocalEvent,
193
+ isClientEvent,
194
194
  restartBackendPushing: (filteredRebasedPending) => Effect.gen(function* () {
195
195
  // Stop current pushing fiber
196
196
  yield* FiberHandle.clear(backendPushingFiberHandle);
197
- // Reset the sync queue
198
- yield* BucketQueue.clear(syncBackendQueue);
199
- yield* BucketQueue.offerAll(syncBackendQueue, filteredRebasedPending);
197
+ // Reset the sync backend push queue
198
+ yield* BucketQueue.clear(syncBackendPushQueue);
199
+ yield* BucketQueue.offerAll(syncBackendPushQueue, filteredRebasedPending);
200
200
  // Restart pushing fiber
201
- yield* FiberHandle.run(backendPushingFiberHandle, backgroundBackendPushing({ dbReady, syncBackendQueue, span }).pipe(Effect.tapCauseLogPretty));
201
+ yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect);
202
202
  }),
203
- applyMutationItemsRef,
204
- stateRef,
205
- semaphore,
206
- span,
203
+ syncStateSref,
204
+ localPushesLatch,
205
+ pullLatch,
206
+ otelSpan,
207
207
  initialBlockingSyncContext,
208
- }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped);
209
- }).pipe(Effect.withSpanScoped('@livestore/common:leader-thread:syncing'));
208
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
209
+ connectedClientSessionPullQueues,
210
+ mergeCounterRef,
211
+ mergePayloads,
212
+ advancePushHead,
213
+ }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped);
214
+ return { initialLeaderHead: initialLocalHead };
215
+ }).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'));
216
+ const pull = ({ cursor }) => Effect.gen(function* () {
217
+ const queue = yield* pullQueue({ cursor });
218
+ return Stream.fromQueue(queue);
219
+ }).pipe(Stream.unwrapScoped);
220
+ const pullQueue = ({ cursor }) => {
221
+ const runtime = ctxRef.current?.runtime ?? shouldNeverHappen('Not initialized');
222
+ return Effect.gen(function* () {
223
+ const queue = yield* connectedClientSessionPullQueues.makeQueue;
224
+ const payloadsSinceCursor = Array.from(mergePayloads.entries())
225
+ .map(([mergeCounter, payload]) => ({ payload, mergeCounter }))
226
+ .filter(({ mergeCounter }) => mergeCounter > cursor.mergeCounter)
227
+ .toSorted((a, b) => a.mergeCounter - b.mergeCounter)
228
+ .map(({ payload, mergeCounter }) => {
229
+ if (payload._tag === 'upstream-advance') {
230
+ return {
231
+ payload: {
232
+ _tag: 'upstream-advance',
233
+ newEvents: ReadonlyArray.dropWhile(payload.newEvents, (eventEncoded) => EventSequenceNumber.isGreaterThanOrEqual(cursor.eventNum, eventEncoded.seqNum)),
234
+ },
235
+ mergeCounter,
236
+ };
237
+ }
238
+ else {
239
+ return { payload, mergeCounter };
240
+ }
241
+ });
242
+ yield* queue.offerAll(payloadsSinceCursor);
243
+ return queue;
244
+ }).pipe(Effect.provide(runtime));
245
+ };
246
+ const syncState = Subscribable.make({
247
+ get: Effect.gen(function* () {
248
+ const syncState = yield* syncStateSref;
249
+ if (syncState === undefined)
250
+ return shouldNeverHappen('Not initialized');
251
+ return syncState;
252
+ }),
253
+ changes: syncStateSref.changes.pipe(Stream.filter(isNotUndefined)),
254
+ });
210
255
  return {
256
+ pull,
257
+ pullQueue,
211
258
  push,
212
259
  pushPartial,
213
260
  boot,
214
- syncState: Effect.gen(function* () {
215
- const state = yield* Ref.get(stateRef);
216
- if (state._tag === 'init')
217
- return shouldNeverHappen('Not initialized');
218
- return state.syncState;
219
- }),
261
+ syncState,
262
+ getMergeCounter: () => mergeCounterRef.current,
220
263
  };
221
264
  });
222
- // TODO how to handle errors gracefully
223
- const makeApplyMutationItems = ({ stateRef, semaphore, }) => Effect.gen(function* () {
224
- const leaderThreadCtx = yield* LeaderThreadCtx;
225
- const { db, dbLog } = leaderThreadCtx;
226
- const applyMutation = yield* makeApplyMutation;
227
- return ({ batchItems }) => Effect.gen(function* () {
228
- const state = yield* Ref.get(stateRef);
229
- if (state._tag !== 'applying-syncstate-advance') {
230
- // console.log('applyMutationItems: counter', counter)
231
- return shouldNeverHappen(`Expected to be applying-syncstate-advance but got ${state._tag}`);
265
+ const backgroundApplyLocalPushes = ({ localPushesLatch, localPushesQueue, pullLatch, syncStateSref, syncBackendPushQueue, schema, isClientEvent, otelSpan, currentLocalPushGenerationRef, connectedClientSessionPullQueues, mergeCounterRef, mergePayloads, localPushBatchSize, testing, }) => Effect.gen(function* () {
266
+ while (true) {
267
+ if (testing.delay !== undefined) {
268
+ yield* testing.delay.pipe(Effect.withSpan('localPushProcessingDelay'));
232
269
  }
233
- db.execute('BEGIN TRANSACTION', undefined); // Start the transaction
234
- dbLog.execute('BEGIN TRANSACTION', undefined); // Start the transaction
235
- yield* Effect.addFinalizer((exit) => Effect.gen(function* () {
236
- if (Exit.isSuccess(exit))
237
- return;
238
- // Rollback in case of an error
239
- db.execute('ROLLBACK', undefined);
240
- dbLog.execute('ROLLBACK', undefined);
241
- }));
242
- for (let i = 0; i < batchItems.length; i++) {
243
- const { meta, ...mutationEventEncoded } = batchItems[i];
244
- yield* applyMutation(mutationEventEncoded);
245
- if (meta?.deferred) {
246
- yield* Deferred.succeed(meta.deferred, void 0);
270
+ const batchItems = yield* BucketQueue.takeBetween(localPushesQueue, 1, localPushBatchSize);
271
+ // Wait for the backend pulling to finish
272
+ yield* localPushesLatch.await;
273
+ // Prevent backend pull processing until this local push is finished
274
+ yield* pullLatch.close;
275
+ // Since the generation might have changed since enqueuing, we need to filter out items with older generation
276
+ // It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
277
+ const filteredBatchItems = batchItems
278
+ .filter(([_1, _2, generation]) => generation === currentLocalPushGenerationRef.current)
279
+ .map(([eventEncoded, deferred]) => [eventEncoded, deferred]);
280
+ if (filteredBatchItems.length === 0) {
281
+ // console.log('dropping old-gen batch', currentLocalPushGenerationRef.current)
282
+ // Allow the backend pulling to start
283
+ yield* pullLatch.open;
284
+ continue;
285
+ }
286
+ const [newEvents, deferreds] = ReadonlyArray.unzip(filteredBatchItems);
287
+ const syncState = yield* syncStateSref;
288
+ if (syncState === undefined)
289
+ return shouldNeverHappen('Not initialized');
290
+ const mergeResult = SyncState.merge({
291
+ syncState,
292
+ payload: { _tag: 'local-push', newEvents },
293
+ isClientEvent,
294
+ isEqualEvent: LiveStoreEvent.isEqualEncoded,
295
+ });
296
+ const mergeCounter = yield* incrementMergeCounter(mergeCounterRef);
297
+ switch (mergeResult._tag) {
298
+ case 'unexpected-error': {
299
+ otelSpan?.addEvent(`[${mergeCounter}]:push:unexpected-error`, {
300
+ batchSize: newEvents.length,
301
+ newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
302
+ });
303
+ return yield* Effect.fail(mergeResult.cause);
304
+ }
305
+ case 'rebase': {
306
+ return shouldNeverHappen('The leader thread should never have to rebase due to a local push');
307
+ }
308
+ case 'reject': {
309
+ otelSpan?.addEvent(`[${mergeCounter}]:push:reject`, {
310
+ batchSize: newEvents.length,
311
+ mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
312
+ });
313
+ // TODO: how to test this?
314
+ currentLocalPushGenerationRef.current++;
315
+ const nextGeneration = currentLocalPushGenerationRef.current;
316
+ const providedNum = newEvents.at(0).seqNum;
317
+ // All subsequent pushes with same generation should be rejected as well
318
+ // We're also handling the case where the localPushQueue already contains events
319
+ // from the next generation which we preserve in the queue
320
+ const remainingEventsMatchingGeneration = yield* BucketQueue.takeSplitWhere(localPushesQueue, (item) => item[2] >= nextGeneration);
321
+ // TODO we still need to better understand and handle this scenario
322
+ if (LS_DEV && (yield* BucketQueue.size(localPushesQueue)) > 0) {
323
+ console.log('localPushesQueue is not empty', yield* BucketQueue.size(localPushesQueue));
324
+ debugger;
325
+ }
326
+ const allDeferredsToReject = [
327
+ ...deferreds,
328
+ ...remainingEventsMatchingGeneration.map(([_, deferred]) => deferred),
329
+ ].filter(isNotUndefined);
330
+ yield* Effect.forEach(allDeferredsToReject, (deferred) => Deferred.fail(deferred, LeaderAheadError.make({
331
+ minimumExpectedNum: mergeResult.expectedMinimumId,
332
+ providedNum,
333
+ // nextGeneration,
334
+ })));
335
+ // Allow the backend pulling to start
336
+ yield* pullLatch.open;
337
+ // In this case we're skipping state update and down/upstream processing
338
+ // We've cleared the local push queue and are now waiting for new local pushes / backend pulls
339
+ continue;
340
+ }
341
+ case 'advance': {
342
+ break;
343
+ }
344
+ default: {
345
+ casesHandled(mergeResult);
247
346
  }
248
- // TODO re-introduce this
249
- // if (i < batchItems.length - 1) {
250
- // yield* Ref.set(stateRef, { ...state, proccesHead: batchItems[i + 1]!.id })
251
- // }
252
347
  }
253
- db.execute('COMMIT', undefined); // Commit the transaction
254
- dbLog.execute('COMMIT', undefined); // Commit the transaction
255
- yield* Ref.set(stateRef, { _tag: 'in-sync', syncState: state.syncState });
256
- // console.log('setRef:sync after applyMutationItems', counter)
257
- yield* semaphore.release(1);
258
- }).pipe(Effect.scoped, Effect.withSpan('@livestore/common:leader-thread:syncing:applyMutationItems', {
259
- attributes: { count: batchItems.length },
260
- }), Effect.tapCauseLogPretty, UnexpectedError.mapToUnexpectedError);
348
+ yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState);
349
+ yield* connectedClientSessionPullQueues.offer({
350
+ payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
351
+ mergeCounter,
352
+ });
353
+ mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }));
354
+ otelSpan?.addEvent(`[${mergeCounter}]:push:advance`, {
355
+ batchSize: newEvents.length,
356
+ mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
357
+ });
358
+ // Don't sync clientOnly events
359
+ const filteredBatch = mergeResult.newEvents.filter((eventEncoded) => {
360
+ const { eventDef } = getEventDef(schema, eventEncoded.name);
361
+ return eventDef.options.clientOnly === false;
362
+ });
363
+ yield* BucketQueue.offerAll(syncBackendPushQueue, filteredBatch);
364
+ yield* materializeEventsBatch({ batchItems: mergeResult.newEvents, deferreds });
365
+ // Allow the backend pulling to start
366
+ yield* pullLatch.open;
367
+ }
261
368
  });
262
- const backgroundBackendPulling = ({ dbReady, initialBackendHead, isLocalEvent, restartBackendPushing, span, stateRef, applyMutationItemsRef, semaphore, initialBlockingSyncContext, }) => Effect.gen(function* () {
263
- const { syncBackend, db, dbLog, connectedClientSessionPullQueues, schema } = yield* LeaderThreadCtx;
369
+ // TODO how to handle errors gracefully
370
+ const materializeEventsBatch = ({ batchItems, deferreds }) => Effect.gen(function* () {
371
+ const { dbState: db, dbEventlog, materializeEvent } = yield* LeaderThreadCtx;
372
+ // NOTE We always start a transaction to ensure consistency between db and eventlog (even for single-item batches)
373
+ db.execute('BEGIN TRANSACTION', undefined); // Start the transaction
374
+ dbEventlog.execute('BEGIN TRANSACTION', undefined); // Start the transaction
375
+ yield* Effect.addFinalizer((exit) => Effect.gen(function* () {
376
+ if (Exit.isSuccess(exit))
377
+ return;
378
+ // Rollback in case of an error
379
+ db.execute('ROLLBACK', undefined);
380
+ dbEventlog.execute('ROLLBACK', undefined);
381
+ }));
382
+ for (let i = 0; i < batchItems.length; i++) {
383
+ const { sessionChangeset } = yield* materializeEvent(batchItems[i]);
384
+ batchItems[i].meta.sessionChangeset = sessionChangeset;
385
+ if (deferreds?.[i] !== undefined) {
386
+ yield* Deferred.succeed(deferreds[i], void 0);
387
+ }
388
+ }
389
+ db.execute('COMMIT', undefined); // Commit the transaction
390
+ dbEventlog.execute('COMMIT', undefined); // Commit the transaction
391
+ }).pipe(Effect.uninterruptible, Effect.scoped, Effect.withSpan('@livestore/common:LeaderSyncProcessor:materializeEventItems', {
392
+ attributes: { batchSize: batchItems.length },
393
+ }), Effect.tapCauseLogPretty, UnexpectedError.mapToUnexpectedError);
394
+ const backgroundBackendPulling = ({ initialBackendHead, isClientEvent, restartBackendPushing, otelSpan, syncStateSref, localPushesLatch, pullLatch, devtoolsLatch, initialBlockingSyncContext, connectedClientSessionPullQueues, mergeCounterRef, mergePayloads, advancePushHead, }) => Effect.gen(function* () {
395
+ const { syncBackend, dbState: db, dbEventlog, schema } = yield* LeaderThreadCtx;
264
396
  if (syncBackend === undefined)
265
397
  return;
266
- const cursorInfo = yield* getCursorInfo(initialBackendHead);
267
398
  const onNewPullChunk = (newEvents, remaining) => Effect.gen(function* () {
268
399
  if (newEvents.length === 0)
269
400
  return;
270
- const state = yield* Ref.get(stateRef);
271
- if (state._tag === 'init')
272
- return shouldNeverHappen('Not initialized');
273
- // const counter = state.counter + 1
274
- if (state._tag === 'applying-syncstate-advance') {
275
- if (state.origin === 'push') {
276
- yield* Fiber.interrupt(state.fiber);
277
- // In theory we should force-take the semaphore here, but as it's still taken,
278
- // it's already in the right state we want it to be in
279
- }
280
- else {
281
- // Wait for previous advance to finish
282
- yield* semaphore.take(1);
283
- }
401
+ if (devtoolsLatch !== undefined) {
402
+ yield* devtoolsLatch.await;
284
403
  }
285
- const trimRollbackUntil = newEvents.at(-1).id;
286
- const updateResult = SyncState.updateSyncState({
287
- syncState: state.syncState,
288
- payload: { _tag: 'upstream-advance', newEvents, trimRollbackUntil },
289
- isLocalEvent,
290
- isEqualEvent: MutationEvent.isEqualEncoded,
291
- ignoreLocalEvents: true,
404
+ // Prevent more local pushes from being processed until this pull is finished
405
+ yield* localPushesLatch.close;
406
+ // Wait for pending local pushes to finish
407
+ yield* pullLatch.await;
408
+ const syncState = yield* syncStateSref;
409
+ if (syncState === undefined)
410
+ return shouldNeverHappen('Not initialized');
411
+ const mergeResult = SyncState.merge({
412
+ syncState,
413
+ payload: SyncState.PayloadUpstreamAdvance.make({ newEvents }),
414
+ isClientEvent,
415
+ isEqualEvent: LiveStoreEvent.isEqualEncoded,
416
+ ignoreClientEvents: true,
292
417
  });
293
- if (updateResult._tag === 'reject') {
418
+ const mergeCounter = yield* incrementMergeCounter(mergeCounterRef);
419
+ if (mergeResult._tag === 'reject') {
294
420
  return shouldNeverHappen('The leader thread should never reject upstream advances');
295
421
  }
296
- const newBackendHead = newEvents.at(-1).id;
297
- updateBackendHead(dbLog, newBackendHead);
298
- if (updateResult._tag === 'rebase') {
299
- span?.addEvent('backend-pull:rebase', {
422
+ else if (mergeResult._tag === 'unexpected-error') {
423
+ otelSpan?.addEvent(`[${mergeCounter}]:pull:unexpected-error`, {
424
+ newEventsCount: newEvents.length,
425
+ newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
426
+ });
427
+ return yield* Effect.fail(mergeResult.cause);
428
+ }
429
+ const newBackendHead = newEvents.at(-1).seqNum;
430
+ Eventlog.updateBackendHead(dbEventlog, newBackendHead);
431
+ if (mergeResult._tag === 'rebase') {
432
+ otelSpan?.addEvent(`[${mergeCounter}]:pull:rebase`, {
300
433
  newEventsCount: newEvents.length,
301
434
  newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
302
- rollbackCount: updateResult.eventsToRollback.length,
303
- updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
435
+ rollbackCount: mergeResult.rollbackEvents.length,
436
+ mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
304
437
  });
305
- const filteredRebasedPending = updateResult.newSyncState.pending.filter((mutationEvent) => {
306
- const mutationDef = schema.mutations.get(mutationEvent.mutation);
307
- return mutationDef.options.localOnly === false;
438
+ const globalRebasedPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
439
+ const { eventDef } = getEventDef(schema, event.name);
440
+ return eventDef.options.clientOnly === false;
308
441
  });
309
- yield* restartBackendPushing(filteredRebasedPending);
310
- if (updateResult.eventsToRollback.length > 0) {
311
- yield* rollback({ db, dbLog, eventIdsToRollback: updateResult.eventsToRollback.map((_) => _.id) });
442
+ yield* restartBackendPushing(globalRebasedPendingEvents);
443
+ if (mergeResult.rollbackEvents.length > 0) {
444
+ yield* rollback({
445
+ dbState: db,
446
+ dbEventlog,
447
+ eventNumsToRollback: mergeResult.rollbackEvents.map((_) => _.seqNum),
448
+ });
312
449
  }
313
450
  yield* connectedClientSessionPullQueues.offer({
314
- payload: {
315
- _tag: 'upstream-rebase',
316
- newEvents: updateResult.newEvents,
317
- rollbackUntil: updateResult.eventsToRollback.at(0).id,
318
- trimRollbackUntil,
319
- },
320
- remaining,
451
+ payload: SyncState.PayloadUpstreamRebase.make({
452
+ newEvents: mergeResult.newEvents,
453
+ rollbackEvents: mergeResult.rollbackEvents,
454
+ }),
455
+ mergeCounter,
321
456
  });
457
+ mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamRebase.make({
458
+ newEvents: mergeResult.newEvents,
459
+ rollbackEvents: mergeResult.rollbackEvents,
460
+ }));
322
461
  }
323
462
  else {
324
- span?.addEvent('backend-pull:advance', {
463
+ otelSpan?.addEvent(`[${mergeCounter}]:pull:advance`, {
325
464
  newEventsCount: newEvents.length,
326
- updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
465
+ mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
327
466
  });
328
467
  yield* connectedClientSessionPullQueues.offer({
329
- payload: { _tag: 'upstream-advance', newEvents: updateResult.newEvents, trimRollbackUntil },
330
- remaining,
468
+ payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
469
+ mergeCounter,
331
470
  });
471
+ mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }));
472
+ if (mergeResult.confirmedEvents.length > 0) {
473
+ // `mergeResult.confirmedEvents` don't contain the correct sync metadata, so we need to use
474
+ // `newEvents` instead which we filter via `mergeResult.confirmedEvents`
475
+ const confirmedNewEvents = newEvents.filter((event) => mergeResult.confirmedEvents.some((confirmedEvent) => EventSequenceNumber.isEqual(event.seqNum, confirmedEvent.seqNum)));
476
+ yield* Eventlog.updateSyncMetadata(confirmedNewEvents);
477
+ }
332
478
  }
479
+ // Removes the changeset rows which are no longer needed as we'll never have to rollback beyond this point
333
480
  trimChangesetRows(db, newBackendHead);
334
- const fiber = yield* applyMutationItemsRef.current({ batchItems: updateResult.newEvents }).pipe(Effect.fork);
335
- yield* Ref.set(stateRef, {
336
- _tag: 'applying-syncstate-advance',
337
- origin: 'pull',
338
- syncState: updateResult.newSyncState,
339
- fiber,
340
- });
481
+ advancePushHead(mergeResult.newSyncState.localHead);
482
+ yield* materializeEventsBatch({ batchItems: mergeResult.newEvents, deferreds: undefined });
483
+ yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState);
484
+ // Allow local pushes to be processed again
485
+ if (remaining === 0) {
486
+ yield* localPushesLatch.open;
487
+ }
341
488
  });
489
+ const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo(initialBackendHead);
342
490
  yield* syncBackend.pull(cursorInfo).pipe(
343
491
  // TODO only take from queue while connected
344
492
  Stream.tap(({ batch, remaining }) => Effect.gen(function* () {
@@ -348,85 +496,100 @@ const backgroundBackendPulling = ({ dbReady, initialBackendHead, isLocalEvent, r
348
496
  // batch: TRACE_VERBOSE ? batch : undefined,
349
497
  // },
350
498
  // })
351
- // Wait for the db to be initially created
352
- yield* dbReady;
353
- // NOTE we only want to take process mutations when the sync backend is connected
499
+ // NOTE we only want to take process events when the sync backend is connected
354
500
  // (e.g. needed for simulating being offline)
355
501
  // TODO remove when there's a better way to handle this in stream above
356
502
  yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true);
357
- yield* onNewPullChunk(batch.map((_) => MutationEvent.EncodedWithMeta.fromGlobal(_.mutationEventEncoded)), remaining);
503
+ yield* onNewPullChunk(batch.map((_) => LiveStoreEvent.EncodedWithMeta.fromGlobal(_.eventEncoded, _.metadata)), remaining);
358
504
  yield* initialBlockingSyncContext.update({ processed: batch.length, remaining });
359
505
  })), Stream.runDrain, Effect.interruptible);
360
- }).pipe(Effect.withSpan('@livestore/common:leader-thread:syncing:backend-pulling'));
361
- const rollback = ({ db, dbLog, eventIdsToRollback, }) => Effect.gen(function* () {
362
- const rollbackEvents = db
363
- .select(sql `SELECT * FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idLocal) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.local})`).join(', ')})`)
364
- .map((_) => ({ id: { global: _.idGlobal, local: _.idLocal }, changeset: _.changeset, debug: _.debug }))
365
- .toSorted((a, b) => EventId.compare(a.id, b.id));
366
- // Apply changesets in reverse order
367
- for (let i = rollbackEvents.length - 1; i >= 0; i--) {
368
- const { changeset } = rollbackEvents[i];
369
- if (changeset !== null) {
370
- db.makeChangeset(changeset).invert().apply();
371
- }
372
- }
373
- // Delete the changeset rows
374
- db.execute(sql `DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idLocal) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.local})`).join(', ')})`);
375
- // Delete the mutation log rows
376
- dbLog.execute(sql `DELETE FROM ${MUTATION_LOG_META_TABLE} WHERE (idGlobal, idLocal) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.local})`).join(', ')})`);
377
- }).pipe(Effect.withSpan('@livestore/common:leader-thread:syncing:rollback', {
378
- attributes: { count: eventIdsToRollback.length },
379
- }));
380
- const getCursorInfo = (remoteHead) => Effect.gen(function* () {
381
- const { dbLog } = yield* LeaderThreadCtx;
382
- if (remoteHead === EventId.ROOT.global)
383
- return Option.none();
384
- const MutationlogQuerySchema = Schema.Struct({
385
- syncMetadataJson: Schema.parseJson(Schema.Option(Schema.JsonValue)),
386
- }).pipe(Schema.pluck('syncMetadataJson'), Schema.Array, Schema.head);
387
- const syncMetadataOption = yield* Effect.sync(() => dbLog.select(sql `SELECT syncMetadataJson FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ${remoteHead} ORDER BY idLocal ASC LIMIT 1`)).pipe(Effect.andThen(Schema.decode(MutationlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie);
388
- return Option.some({
389
- cursor: { global: remoteHead, local: EventId.localDefault },
390
- metadata: syncMetadataOption,
391
- });
392
- }).pipe(Effect.withSpan('@livestore/common:leader-thread:syncing:getCursorInfo', { attributes: { remoteHead } }));
393
- const backgroundBackendPushing = ({ dbReady, syncBackendQueue, span, }) => Effect.gen(function* () {
394
- const { syncBackend, dbLog } = yield* LeaderThreadCtx;
506
+ }).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pulling'));
507
+ const backgroundBackendPushing = ({ syncBackendPushQueue, otelSpan, devtoolsLatch, backendPushBatchSize, }) => Effect.gen(function* () {
508
+ const { syncBackend } = yield* LeaderThreadCtx;
395
509
  if (syncBackend === undefined)
396
510
  return;
397
- yield* dbReady;
398
511
  while (true) {
399
512
  yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true);
400
- // TODO make batch size configurable
401
- const queueItems = yield* BucketQueue.takeBetween(syncBackendQueue, 1, 50);
513
+ const queueItems = yield* BucketQueue.takeBetween(syncBackendPushQueue, 1, backendPushBatchSize);
402
514
  yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true);
403
- span?.addEvent('backend-push', {
515
+ if (devtoolsLatch !== undefined) {
516
+ yield* devtoolsLatch.await;
517
+ }
518
+ otelSpan?.addEvent('backend-push', {
404
519
  batchSize: queueItems.length,
405
520
  batch: TRACE_VERBOSE ? JSON.stringify(queueItems) : undefined,
406
521
  });
407
522
  // TODO handle push errors (should only happen during concurrent pull+push)
408
523
  const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either);
409
524
  if (pushResult._tag === 'Left') {
410
- span?.addEvent('backend-push-error', { error: pushResult.left.toString() });
525
+ if (LS_DEV) {
526
+ yield* Effect.logDebug('handled backend-push-error', { error: pushResult.left.toString() });
527
+ }
528
+ otelSpan?.addEvent('backend-push-error', { error: pushResult.left.toString() });
411
529
  // wait for interrupt caused by background pulling which will then restart pushing
412
530
  return yield* Effect.never;
413
531
  }
414
- const { metadata } = pushResult.right;
415
- // TODO try to do this in a single query
416
- for (let i = 0; i < queueItems.length; i++) {
417
- const mutationEventEncoded = queueItems[i];
418
- yield* execSql(dbLog, ...updateRows({
419
- tableName: MUTATION_LOG_META_TABLE,
420
- columns: mutationLogMetaTable.sqliteDef.columns,
421
- where: { idGlobal: mutationEventEncoded.id.global, idLocal: mutationEventEncoded.id.local },
422
- updateValues: { syncMetadataJson: metadata[i] },
423
- }));
424
- }
425
532
  }
426
- }).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:leader-thread:syncing:backend-pushing'));
533
+ }).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pushing'));
427
534
  const trimChangesetRows = (db, newHead) => {
428
535
  // Since we're using the session changeset rows to query for the current head,
429
536
  // we're keeping at least one row for the current head, and thus are using `<` instead of `<=`
430
- db.execute(sql `DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE idGlobal < ${newHead.global}`);
537
+ db.execute(sql `DELETE FROM ${SystemTables.SESSION_CHANGESET_META_TABLE} WHERE seqNumGlobal < ${newHead.global}`);
431
538
  };
539
+ const makePullQueueSet = Effect.gen(function* () {
540
+ const set = new Set();
541
+ yield* Effect.addFinalizer(() => Effect.gen(function* () {
542
+ for (const queue of set) {
543
+ yield* Queue.shutdown(queue);
544
+ }
545
+ set.clear();
546
+ }));
547
+ const makeQueue = Effect.gen(function* () {
548
+ const queue = yield* Queue.unbounded().pipe(Effect.acquireRelease(Queue.shutdown));
549
+ yield* Effect.addFinalizer(() => Effect.sync(() => set.delete(queue)));
550
+ set.add(queue);
551
+ return queue;
552
+ });
553
+ const offer = (item) => Effect.gen(function* () {
554
+ // Short-circuit if the payload is an empty upstream advance
555
+ if (item.payload._tag === 'upstream-advance' && item.payload.newEvents.length === 0) {
556
+ return;
557
+ }
558
+ for (const queue of set) {
559
+ yield* Queue.offer(queue, item);
560
+ }
561
+ });
562
+ return {
563
+ makeQueue,
564
+ offer,
565
+ };
566
+ });
567
+ const incrementMergeCounter = (mergeCounterRef) => Effect.gen(function* () {
568
+ const { dbState } = yield* LeaderThreadCtx;
569
+ mergeCounterRef.current++;
570
+ dbState.execute(sql `INSERT OR REPLACE INTO ${SystemTables.LEADER_MERGE_COUNTER_TABLE} (id, mergeCounter) VALUES (0, ${mergeCounterRef.current})`);
571
+ return mergeCounterRef.current;
572
+ });
573
+ const getMergeCounterFromDb = (dbState) => Effect.gen(function* () {
574
+ const result = dbState.select(sql `SELECT mergeCounter FROM ${SystemTables.LEADER_MERGE_COUNTER_TABLE} WHERE id = 0`);
575
+ return result[0]?.mergeCounter ?? 0;
576
+ });
577
+ const validatePushBatch = (batch, pushHead) => Effect.gen(function* () {
578
+ if (batch.length === 0) {
579
+ return;
580
+ }
581
+ // Make sure batch is monotonically increasing
582
+ for (let i = 1; i < batch.length; i++) {
583
+ if (EventSequenceNumber.isGreaterThanOrEqual(batch[i - 1].seqNum, batch[i].seqNum)) {
584
+ shouldNeverHappen(`Events must be ordered in monotonically ascending order by eventNum. Received: [${batch.map((e) => EventSequenceNumber.toString(e.seqNum)).join(', ')}]`);
585
+ }
586
+ }
587
+ // Make sure smallest sequence number is > pushHead
588
+ if (EventSequenceNumber.isGreaterThanOrEqual(pushHead, batch[0].seqNum)) {
589
+ return yield* LeaderAheadError.make({
590
+ minimumExpectedNum: pushHead,
591
+ providedNum: batch[0].seqNum,
592
+ });
593
+ }
594
+ });
432
595
  //# sourceMappingURL=LeaderSyncProcessor.js.map