@livestore/common 0.3.0-dev.2 → 0.3.0-dev.21

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 (332) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixture.d.ts +21 -21
  3. package/dist/adapter-types.d.ts +97 -53
  4. package/dist/adapter-types.d.ts.map +1 -1
  5. package/dist/adapter-types.js +17 -0
  6. package/dist/adapter-types.js.map +1 -1
  7. package/dist/bounded-collections.d.ts +1 -1
  8. package/dist/bounded-collections.d.ts.map +1 -1
  9. package/dist/debug-info.d.ts +1 -1
  10. package/dist/debug-info.d.ts.map +1 -1
  11. package/dist/derived-mutations.d.ts +5 -5
  12. package/dist/derived-mutations.d.ts.map +1 -1
  13. package/dist/derived-mutations.js +3 -3
  14. package/dist/derived-mutations.js.map +1 -1
  15. package/dist/derived-mutations.test.js.map +1 -1
  16. package/dist/devtools/devtools-messages-client-session.d.ts +389 -0
  17. package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -0
  18. package/dist/devtools/devtools-messages-client-session.js +96 -0
  19. package/dist/devtools/devtools-messages-client-session.js.map +1 -0
  20. package/dist/devtools/devtools-messages-common.d.ts +61 -0
  21. package/dist/devtools/devtools-messages-common.d.ts.map +1 -0
  22. package/dist/devtools/devtools-messages-common.js +54 -0
  23. package/dist/devtools/devtools-messages-common.js.map +1 -0
  24. package/dist/devtools/devtools-messages-leader.d.ts +393 -0
  25. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -0
  26. package/dist/devtools/devtools-messages-leader.js +148 -0
  27. package/dist/devtools/devtools-messages-leader.js.map +1 -0
  28. package/dist/devtools/devtools-messages.d.ts +3 -592
  29. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  30. package/dist/devtools/devtools-messages.js +3 -171
  31. package/dist/devtools/devtools-messages.js.map +1 -1
  32. package/dist/devtools/devtools-sessioninfo.d.ts +28 -0
  33. package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -0
  34. package/dist/devtools/devtools-sessioninfo.js +34 -0
  35. package/dist/devtools/devtools-sessioninfo.js.map +1 -0
  36. package/dist/devtools/devtools-sessions-channel.d.ts +28 -0
  37. package/dist/devtools/devtools-sessions-channel.d.ts.map +1 -0
  38. package/dist/devtools/devtools-sessions-channel.js +34 -0
  39. package/dist/devtools/devtools-sessions-channel.js.map +1 -0
  40. package/dist/devtools/index.d.ts +35 -38
  41. package/dist/devtools/index.d.ts.map +1 -1
  42. package/dist/devtools/index.js +20 -45
  43. package/dist/devtools/index.js.map +1 -1
  44. package/dist/devtools/mod.d.ts +39 -0
  45. package/dist/devtools/mod.d.ts.map +1 -0
  46. package/dist/devtools/mod.js +27 -0
  47. package/dist/devtools/mod.js.map +1 -0
  48. package/dist/index.d.ts +2 -6
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +2 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/init-singleton-tables.d.ts +2 -2
  53. package/dist/init-singleton-tables.d.ts.map +1 -1
  54. package/dist/init-singleton-tables.js.map +1 -1
  55. package/dist/leader-thread/LeaderSyncProcessor.d.ts +39 -0
  56. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -0
  57. package/dist/leader-thread/LeaderSyncProcessor.js +527 -0
  58. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -0
  59. package/dist/leader-thread/apply-mutation.d.ts +5 -2
  60. package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
  61. package/dist/leader-thread/apply-mutation.js +55 -35
  62. package/dist/leader-thread/apply-mutation.js.map +1 -1
  63. package/dist/leader-thread/connection.d.ts +34 -6
  64. package/dist/leader-thread/connection.d.ts.map +1 -1
  65. package/dist/leader-thread/connection.js +22 -7
  66. package/dist/leader-thread/connection.js.map +1 -1
  67. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  68. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  69. package/dist/leader-thread/leader-worker-devtools.js +147 -124
  70. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  71. package/dist/leader-thread/make-leader-thread-layer.d.ts +12 -11
  72. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  73. package/dist/leader-thread/make-leader-thread-layer.js +55 -18
  74. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  75. package/dist/leader-thread/mutationlog.d.ts +6 -19
  76. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  77. package/dist/leader-thread/mutationlog.js +12 -9
  78. package/dist/leader-thread/mutationlog.js.map +1 -1
  79. package/dist/leader-thread/pull-queue-set.d.ts +3 -3
  80. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  81. package/dist/leader-thread/pull-queue-set.js +9 -0
  82. package/dist/leader-thread/pull-queue-set.js.map +1 -1
  83. package/dist/leader-thread/recreate-db.d.ts +4 -2
  84. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  85. package/dist/leader-thread/recreate-db.js +32 -21
  86. package/dist/leader-thread/recreate-db.js.map +1 -1
  87. package/dist/leader-thread/shutdown-channel.d.ts +2 -5
  88. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  89. package/dist/leader-thread/shutdown-channel.js +2 -4
  90. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  91. package/dist/leader-thread/types.d.ts +58 -26
  92. package/dist/leader-thread/types.d.ts.map +1 -1
  93. package/dist/leader-thread/types.js +1 -3
  94. package/dist/leader-thread/types.js.map +1 -1
  95. package/dist/mutation.d.ts +9 -2
  96. package/dist/mutation.d.ts.map +1 -1
  97. package/dist/mutation.js +5 -5
  98. package/dist/mutation.js.map +1 -1
  99. package/dist/otel.d.ts +2 -0
  100. package/dist/otel.d.ts.map +1 -1
  101. package/dist/otel.js +5 -0
  102. package/dist/otel.js.map +1 -1
  103. package/dist/query-builder/api.d.ts +3 -3
  104. package/dist/query-builder/api.d.ts.map +1 -1
  105. package/dist/query-builder/impl.d.ts +4 -4
  106. package/dist/query-builder/impl.d.ts.map +1 -1
  107. package/dist/query-builder/impl.js.map +1 -1
  108. package/dist/query-builder/impl.test.js +16 -1
  109. package/dist/query-builder/impl.test.js.map +1 -1
  110. package/dist/query-info.d.ts +3 -3
  111. package/dist/query-info.d.ts.map +1 -1
  112. package/dist/rehydrate-from-mutationlog.d.ts +5 -5
  113. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  114. package/dist/rehydrate-from-mutationlog.js +23 -27
  115. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  116. package/dist/schema/EventId.d.ts +27 -16
  117. package/dist/schema/EventId.d.ts.map +1 -1
  118. package/dist/schema/EventId.js +36 -11
  119. package/dist/schema/EventId.js.map +1 -1
  120. package/dist/schema/EventId.test.d.ts +2 -0
  121. package/dist/schema/EventId.test.d.ts.map +1 -0
  122. package/dist/schema/EventId.test.js +11 -0
  123. package/dist/schema/EventId.test.js.map +1 -0
  124. package/dist/schema/MutationEvent.d.ts +76 -82
  125. package/dist/schema/MutationEvent.d.ts.map +1 -1
  126. package/dist/schema/MutationEvent.js +53 -20
  127. package/dist/schema/MutationEvent.js.map +1 -1
  128. package/dist/schema/db-schema/ast/sqlite.d.ts +69 -0
  129. package/dist/schema/db-schema/ast/sqlite.d.ts.map +1 -0
  130. package/dist/schema/db-schema/ast/sqlite.js +71 -0
  131. package/dist/schema/db-schema/ast/sqlite.js.map +1 -0
  132. package/dist/schema/db-schema/ast/validate.d.ts +3 -0
  133. package/dist/schema/db-schema/ast/validate.d.ts.map +1 -0
  134. package/dist/schema/db-schema/ast/validate.js +12 -0
  135. package/dist/schema/db-schema/ast/validate.js.map +1 -0
  136. package/dist/schema/db-schema/dsl/field-defs.d.ts +90 -0
  137. package/dist/schema/db-schema/dsl/field-defs.d.ts.map +1 -0
  138. package/dist/schema/db-schema/dsl/field-defs.js +87 -0
  139. package/dist/schema/db-schema/dsl/field-defs.js.map +1 -0
  140. package/dist/schema/db-schema/dsl/field-defs.test.d.ts +2 -0
  141. package/dist/schema/db-schema/dsl/field-defs.test.d.ts.map +1 -0
  142. package/dist/schema/db-schema/dsl/field-defs.test.js +29 -0
  143. package/dist/schema/db-schema/dsl/field-defs.test.js.map +1 -0
  144. package/dist/schema/db-schema/dsl/mod.d.ts +88 -0
  145. package/dist/schema/db-schema/dsl/mod.d.ts.map +1 -0
  146. package/dist/schema/db-schema/dsl/mod.js +35 -0
  147. package/dist/schema/db-schema/dsl/mod.js.map +1 -0
  148. package/dist/schema/db-schema/hash.d.ts +2 -0
  149. package/dist/schema/db-schema/hash.d.ts.map +1 -0
  150. package/dist/schema/db-schema/hash.js +14 -0
  151. package/dist/schema/db-schema/hash.js.map +1 -0
  152. package/dist/schema/db-schema/mod.d.ts +3 -0
  153. package/dist/schema/db-schema/mod.d.ts.map +1 -0
  154. package/dist/schema/db-schema/mod.js +3 -0
  155. package/dist/schema/db-schema/mod.js.map +1 -0
  156. package/dist/schema/mod.d.ts +1 -0
  157. package/dist/schema/mod.d.ts.map +1 -1
  158. package/dist/schema/mod.js +1 -0
  159. package/dist/schema/mod.js.map +1 -1
  160. package/dist/schema/mutations.d.ts +8 -9
  161. package/dist/schema/mutations.d.ts.map +1 -1
  162. package/dist/schema/mutations.js +2 -2
  163. package/dist/schema/mutations.js.map +1 -1
  164. package/dist/schema/schema-helpers.d.ts.map +1 -1
  165. package/dist/schema/schema-helpers.js +1 -1
  166. package/dist/schema/schema-helpers.js.map +1 -1
  167. package/dist/schema/schema.d.ts +5 -2
  168. package/dist/schema/schema.d.ts.map +1 -1
  169. package/dist/schema/schema.js +20 -9
  170. package/dist/schema/schema.js.map +1 -1
  171. package/dist/schema/system-tables.d.ts +65 -47
  172. package/dist/schema/system-tables.d.ts.map +1 -1
  173. package/dist/schema/system-tables.js +24 -13
  174. package/dist/schema/system-tables.js.map +1 -1
  175. package/dist/schema/table-def.d.ts +18 -24
  176. package/dist/schema/table-def.d.ts.map +1 -1
  177. package/dist/schema/table-def.js +3 -4
  178. package/dist/schema/table-def.js.map +1 -1
  179. package/dist/schema-management/common.d.ts +3 -3
  180. package/dist/schema-management/common.d.ts.map +1 -1
  181. package/dist/schema-management/common.js.map +1 -1
  182. package/dist/schema-management/migrations.d.ts +6 -6
  183. package/dist/schema-management/migrations.d.ts.map +1 -1
  184. package/dist/schema-management/migrations.js +13 -8
  185. package/dist/schema-management/migrations.js.map +1 -1
  186. package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -1
  187. package/dist/schema-management/validate-mutation-defs.js +2 -2
  188. package/dist/schema-management/validate-mutation-defs.js.map +1 -1
  189. package/dist/sql-queries/misc.d.ts.map +1 -1
  190. package/dist/sql-queries/sql-queries.d.ts +1 -1
  191. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  192. package/dist/sql-queries/sql-queries.js.map +1 -1
  193. package/dist/sql-queries/sql-query-builder.d.ts +1 -1
  194. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  195. package/dist/sql-queries/sql-query-builder.js.map +1 -1
  196. package/dist/sql-queries/types.d.ts +2 -1
  197. package/dist/sql-queries/types.d.ts.map +1 -1
  198. package/dist/sql-queries/types.js.map +1 -1
  199. package/dist/sync/{client-session-sync-processor.d.ts → ClientSessionSyncProcessor.d.ts} +25 -14
  200. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -0
  201. package/dist/sync/ClientSessionSyncProcessor.js +199 -0
  202. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -0
  203. package/dist/sync/index.d.ts +1 -1
  204. package/dist/sync/index.d.ts.map +1 -1
  205. package/dist/sync/index.js +1 -1
  206. package/dist/sync/index.js.map +1 -1
  207. package/dist/sync/next/compact-events.d.ts.map +1 -1
  208. package/dist/sync/next/facts.d.ts.map +1 -1
  209. package/dist/sync/next/facts.js +1 -1
  210. package/dist/sync/next/facts.js.map +1 -1
  211. package/dist/sync/next/history-dag-common.d.ts +3 -4
  212. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  213. package/dist/sync/next/history-dag-common.js +3 -1
  214. package/dist/sync/next/history-dag-common.js.map +1 -1
  215. package/dist/sync/next/history-dag.d.ts.map +1 -1
  216. package/dist/sync/next/history-dag.js +1 -1
  217. package/dist/sync/next/history-dag.js.map +1 -1
  218. package/dist/sync/next/rebase-events.d.ts +6 -4
  219. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  220. package/dist/sync/next/rebase-events.js +6 -3
  221. package/dist/sync/next/rebase-events.js.map +1 -1
  222. package/dist/sync/next/test/compact-events.calculator.test.js +12 -12
  223. package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -1
  224. package/dist/sync/next/test/compact-events.test.js +43 -43
  225. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  226. package/dist/sync/next/test/mutation-fixtures.d.ts +4 -4
  227. package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
  228. package/dist/sync/next/test/mutation-fixtures.js +12 -16
  229. package/dist/sync/next/test/mutation-fixtures.js.map +1 -1
  230. package/dist/sync/sync.d.ts +31 -16
  231. package/dist/sync/sync.d.ts.map +1 -1
  232. package/dist/sync/sync.js +7 -3
  233. package/dist/sync/sync.js.map +1 -1
  234. package/dist/sync/syncstate.d.ts +177 -44
  235. package/dist/sync/syncstate.d.ts.map +1 -1
  236. package/dist/sync/syncstate.js +188 -56
  237. package/dist/sync/syncstate.js.map +1 -1
  238. package/dist/sync/syncstate.test.js +162 -92
  239. package/dist/sync/syncstate.test.js.map +1 -1
  240. package/dist/sync/validate-push-payload.d.ts +2 -2
  241. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  242. package/dist/sync/validate-push-payload.js +2 -2
  243. package/dist/sync/validate-push-payload.js.map +1 -1
  244. package/dist/util.d.ts +2 -2
  245. package/dist/util.d.ts.map +1 -1
  246. package/dist/version.d.ts +1 -1
  247. package/dist/version.d.ts.map +1 -1
  248. package/dist/version.js +1 -1
  249. package/dist/version.js.map +1 -1
  250. package/package.json +6 -6
  251. package/src/adapter-types.ts +80 -56
  252. package/src/derived-mutations.test.ts +1 -1
  253. package/src/derived-mutations.ts +13 -9
  254. package/src/devtools/devtools-messages-client-session.ts +141 -0
  255. package/src/devtools/devtools-messages-common.ts +106 -0
  256. package/src/devtools/devtools-messages-leader.ts +192 -0
  257. package/src/devtools/devtools-messages.ts +3 -243
  258. package/src/devtools/devtools-sessioninfo.ts +99 -0
  259. package/src/devtools/mod.ts +36 -0
  260. package/src/index.ts +2 -8
  261. package/src/init-singleton-tables.ts +2 -2
  262. package/src/leader-thread/LeaderSyncProcessor.ts +833 -0
  263. package/src/leader-thread/apply-mutation.ts +87 -43
  264. package/src/leader-thread/connection.ts +54 -9
  265. package/src/leader-thread/leader-worker-devtools.ts +199 -174
  266. package/src/leader-thread/make-leader-thread-layer.ts +89 -35
  267. package/src/leader-thread/mutationlog.ts +20 -14
  268. package/src/leader-thread/pull-queue-set.ts +10 -1
  269. package/src/leader-thread/recreate-db.ts +38 -25
  270. package/src/leader-thread/shutdown-channel.ts +2 -4
  271. package/src/leader-thread/types.ts +68 -34
  272. package/src/mutation.ts +17 -7
  273. package/src/otel.ts +8 -0
  274. package/src/query-builder/api.ts +4 -4
  275. package/src/query-builder/impl.test.ts +22 -1
  276. package/src/query-builder/impl.ts +2 -2
  277. package/src/query-info.ts +3 -3
  278. package/src/rehydrate-from-mutationlog.ts +28 -34
  279. package/src/schema/EventId.test.ts +12 -0
  280. package/src/schema/EventId.ts +49 -15
  281. package/src/schema/MutationEvent.ts +78 -31
  282. package/src/schema/db-schema/ast/sqlite.ts +142 -0
  283. package/src/schema/db-schema/ast/validate.ts +13 -0
  284. package/src/schema/db-schema/dsl/__snapshots__/field-defs.test.ts.snap +206 -0
  285. package/src/schema/db-schema/dsl/field-defs.test.ts +35 -0
  286. package/src/schema/db-schema/dsl/field-defs.ts +242 -0
  287. package/src/schema/db-schema/dsl/mod.ts +195 -0
  288. package/src/schema/db-schema/hash.ts +14 -0
  289. package/src/schema/db-schema/mod.ts +2 -0
  290. package/src/schema/mod.ts +1 -0
  291. package/src/schema/mutations.ts +10 -20
  292. package/src/schema/schema-helpers.ts +1 -1
  293. package/src/schema/schema.ts +22 -10
  294. package/src/schema/system-tables.ts +24 -13
  295. package/src/schema/table-def.ts +17 -17
  296. package/src/schema-management/common.ts +3 -3
  297. package/src/schema-management/migrations.ts +19 -15
  298. package/src/schema-management/validate-mutation-defs.ts +2 -2
  299. package/src/sql-queries/sql-queries.ts +1 -1
  300. package/src/sql-queries/sql-query-builder.ts +1 -2
  301. package/src/sql-queries/types.ts +3 -1
  302. package/src/sync/ClientSessionSyncProcessor.ts +313 -0
  303. package/src/sync/index.ts +1 -1
  304. package/src/sync/next/facts.ts +1 -1
  305. package/src/sync/next/history-dag-common.ts +5 -1
  306. package/src/sync/next/history-dag.ts +1 -1
  307. package/src/sync/next/rebase-events.ts +13 -7
  308. package/src/sync/next/test/compact-events.calculator.test.ts +12 -12
  309. package/src/sync/next/test/compact-events.test.ts +43 -43
  310. package/src/sync/next/test/mutation-fixtures.ts +16 -19
  311. package/src/sync/sync.ts +26 -10
  312. package/src/sync/syncstate.test.ts +178 -98
  313. package/src/sync/syncstate.ts +170 -83
  314. package/src/sync/validate-push-payload.ts +7 -4
  315. package/src/version.ts +1 -1
  316. package/tmp/pack.tgz +0 -0
  317. package/tsconfig.json +1 -1
  318. package/dist/devtools/devtools-bridge.d.ts +0 -12
  319. package/dist/devtools/devtools-bridge.d.ts.map +0 -1
  320. package/dist/devtools/devtools-bridge.js +0 -2
  321. package/dist/devtools/devtools-bridge.js.map +0 -1
  322. package/dist/leader-thread/leader-sync-processor.d.ts +0 -47
  323. package/dist/leader-thread/leader-sync-processor.d.ts.map +0 -1
  324. package/dist/leader-thread/leader-sync-processor.js +0 -422
  325. package/dist/leader-thread/leader-sync-processor.js.map +0 -1
  326. package/dist/sync/client-session-sync-processor.d.ts.map +0 -1
  327. package/dist/sync/client-session-sync-processor.js +0 -131
  328. package/dist/sync/client-session-sync-processor.js.map +0 -1
  329. package/src/devtools/devtools-bridge.ts +0 -13
  330. package/src/devtools/index.ts +0 -48
  331. package/src/leader-thread/leader-sync-processor.ts +0 -666
  332. package/src/sync/client-session-sync-processor.ts +0 -207
@@ -0,0 +1,833 @@
1
+ import { casesHandled, isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
2
+ import type { HttpClient, Scope, Tracer } from '@livestore/utils/effect'
3
+ import {
4
+ BucketQueue,
5
+ Deferred,
6
+ Effect,
7
+ Exit,
8
+ FiberHandle,
9
+ Option,
10
+ OtelTracer,
11
+ ReadonlyArray,
12
+ Schema,
13
+ Stream,
14
+ Subscribable,
15
+ SubscriptionRef,
16
+ } from '@livestore/utils/effect'
17
+ import type * as otel from '@opentelemetry/api'
18
+
19
+ import type { SqliteDb } from '../adapter-types.js'
20
+ import { UnexpectedError } from '../adapter-types.js'
21
+ import type { LiveStoreSchema, SessionChangesetMetaRow } from '../schema/mod.js'
22
+ import {
23
+ EventId,
24
+ getMutationDef,
25
+ MUTATION_LOG_META_TABLE,
26
+ MutationEvent,
27
+ mutationLogMetaTable,
28
+ SESSION_CHANGESET_META_TABLE,
29
+ } from '../schema/mod.js'
30
+ import { updateRows } from '../sql-queries/index.js'
31
+ import { LeaderAheadError } from '../sync/sync.js'
32
+ import * as SyncState from '../sync/syncstate.js'
33
+ import { sql } from '../util.js'
34
+ import { makeApplyMutation } from './apply-mutation.js'
35
+ import { execSql } from './connection.js'
36
+ import { getBackendHeadFromDb, getClientHeadFromDb, getMutationEventsSince, updateBackendHead } from './mutationlog.js'
37
+ import type { InitialBlockingSyncContext, InitialSyncInfo, LeaderSyncProcessor } from './types.js'
38
+ import { LeaderThreadCtx } from './types.js'
39
+
40
+ export const BACKEND_PUSH_BATCH_SIZE = 50
41
+
42
+ type LocalPushQueueItem = [
43
+ mutationEvent: MutationEvent.EncodedWithMeta,
44
+ deferred: Deferred.Deferred<void, LeaderAheadError> | undefined,
45
+ generation: number,
46
+ ]
47
+
48
+ /**
49
+ * The LeaderSyncProcessor manages synchronization of mutations between
50
+ * the local state and the sync backend, ensuring efficient and orderly processing.
51
+ *
52
+ * In the LeaderSyncProcessor, pulling always has precedence over pushing.
53
+ *
54
+ * Responsibilities:
55
+ * - Queueing incoming local mutations in a localPushMailbox.
56
+ * - Broadcasting mutations to client sessions via pull queues.
57
+ * - Pushing mutations to the sync backend.
58
+ *
59
+ * Notes:
60
+ *
61
+ * local push processing:
62
+ * - localPushMailbox:
63
+ * - Maintains events in ascending order.
64
+ * - Uses `Deferred` objects to resolve/reject events based on application success.
65
+ * - Processes events from the mailbox, applying mutations in batches.
66
+ * - Controlled by a `Latch` to manage execution flow.
67
+ * - The latch closes on pull receipt and re-opens post-pull completion.
68
+ * - Processes up to `maxBatchSize` events per cycle.
69
+ *
70
+ */
71
+ export const makeLeaderSyncProcessor = ({
72
+ schema,
73
+ dbMissing,
74
+ dbMutationLog,
75
+ clientId,
76
+ initialBlockingSyncContext,
77
+ }: {
78
+ schema: LiveStoreSchema
79
+ /** Only used to know whether we can safely query dbMutationLog during setup execution */
80
+ dbMissing: boolean
81
+ dbMutationLog: SqliteDb
82
+ clientId: string
83
+ initialBlockingSyncContext: InitialBlockingSyncContext
84
+ }): Effect.Effect<LeaderSyncProcessor, UnexpectedError, Scope.Scope> =>
85
+ Effect.gen(function* () {
86
+ const syncBackendQueue = yield* BucketQueue.make<MutationEvent.EncodedWithMeta>()
87
+
88
+ const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
89
+
90
+ const isLocalEvent = (mutationEventEncoded: MutationEvent.EncodedWithMeta) => {
91
+ const mutationDef = getMutationDef(schema, mutationEventEncoded.mutation)
92
+ return mutationDef.options.clientOnly
93
+ }
94
+
95
+ /**
96
+ * Tracks generations of queued local push events.
97
+ * If a batch is rejected, all subsequent push queue items with the same generation are also rejected,
98
+ * even if they would be valid on their own.
99
+ */
100
+ const currentLocalPushGenerationRef = { current: 0 }
101
+
102
+ // This context depends on data from `boot`, we should find a better implementation to avoid this ref indirection.
103
+ const ctxRef = {
104
+ current: undefined as
105
+ | undefined
106
+ | {
107
+ otelSpan: otel.Span | undefined
108
+ span: Tracer.Span
109
+ devtoolsLatch: Effect.Latch | undefined
110
+ },
111
+ }
112
+
113
+ const localPushesQueue = yield* BucketQueue.make<LocalPushQueueItem>()
114
+ const localPushesLatch = yield* Effect.makeLatch(true)
115
+ const pullLatch = yield* Effect.makeLatch(true)
116
+
117
+ const push: LeaderSyncProcessor['push'] = (newEvents, options) =>
118
+ Effect.gen(function* () {
119
+ // TODO validate batch
120
+ if (newEvents.length === 0) return
121
+
122
+ // if (options.generation < currentLocalPushGenerationRef.current) {
123
+ // debugger
124
+ // // We can safely drop this batch as it's from a previous push generation
125
+ // return
126
+ // }
127
+
128
+ if (clientId === 'client-b') {
129
+ // console.log(
130
+ // 'push from client session',
131
+ // newEvents.map((item) => item.toJSON()),
132
+ // )
133
+ }
134
+
135
+ const waitForProcessing = options?.waitForProcessing ?? false
136
+ const generation = currentLocalPushGenerationRef.current
137
+
138
+ if (waitForProcessing) {
139
+ const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void, LeaderAheadError>())
140
+
141
+ const items = newEvents.map(
142
+ (mutationEventEncoded, i) => [mutationEventEncoded, deferreds[i], generation] as LocalPushQueueItem,
143
+ )
144
+
145
+ yield* BucketQueue.offerAll(localPushesQueue, items)
146
+
147
+ yield* Effect.all(deferreds)
148
+ } else {
149
+ const items = newEvents.map(
150
+ (mutationEventEncoded) => [mutationEventEncoded, undefined, generation] as LocalPushQueueItem,
151
+ )
152
+ yield* BucketQueue.offerAll(localPushesQueue, items)
153
+ }
154
+ }).pipe(
155
+ Effect.withSpan('@livestore/common:leader-thread:syncing:local-push', {
156
+ attributes: {
157
+ batchSize: newEvents.length,
158
+ batch: TRACE_VERBOSE ? newEvents : undefined,
159
+ },
160
+ links: ctxRef.current?.span ? [{ _tag: 'SpanLink', span: ctxRef.current.span, attributes: {} }] : undefined,
161
+ }),
162
+ )
163
+
164
+ const pushPartial: LeaderSyncProcessor['pushPartial'] = ({
165
+ mutationEvent: partialMutationEvent,
166
+ clientId,
167
+ sessionId,
168
+ }) =>
169
+ Effect.gen(function* () {
170
+ const syncState = yield* syncStateSref
171
+ if (syncState === undefined) return shouldNeverHappen('Not initialized')
172
+
173
+ const mutationDef = getMutationDef(schema, partialMutationEvent.mutation)
174
+
175
+ const mutationEventEncoded = new MutationEvent.EncodedWithMeta({
176
+ ...partialMutationEvent,
177
+ clientId,
178
+ sessionId,
179
+ ...EventId.nextPair(syncState.localHead, mutationDef.options.clientOnly),
180
+ })
181
+
182
+ yield* push([mutationEventEncoded])
183
+ }).pipe(Effect.catchTag('LeaderAheadError', Effect.orDie))
184
+
185
+ // Starts various background loops
186
+ const boot: LeaderSyncProcessor['boot'] = ({ dbReady }) =>
187
+ Effect.gen(function* () {
188
+ const span = yield* Effect.currentSpan.pipe(Effect.orDie)
189
+ const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)))
190
+ const { devtools, shutdownChannel } = yield* LeaderThreadCtx
191
+
192
+ ctxRef.current = {
193
+ otelSpan,
194
+ span,
195
+ devtoolsLatch: devtools.enabled ? devtools.syncBackendLatch : undefined,
196
+ }
197
+
198
+ const initialBackendHead = dbMissing ? EventId.ROOT.global : getBackendHeadFromDb(dbMutationLog)
199
+ const initialLocalHead = dbMissing ? EventId.ROOT : getClientHeadFromDb(dbMutationLog)
200
+
201
+ if (initialBackendHead > initialLocalHead.global) {
202
+ return shouldNeverHappen(
203
+ `During boot the backend head (${initialBackendHead}) should never be greater than the local head (${initialLocalHead.global})`,
204
+ )
205
+ }
206
+
207
+ const pendingMutationEvents = yield* getMutationEventsSince({
208
+ global: initialBackendHead,
209
+ client: EventId.clientDefault,
210
+ }).pipe(Effect.map(ReadonlyArray.map((_) => new MutationEvent.EncodedWithMeta(_))))
211
+
212
+ const initialSyncState = new SyncState.SyncState({
213
+ pending: pendingMutationEvents,
214
+ // On the leader we don't need a rollback tail beyond `pending` items
215
+ rollbackTail: [],
216
+ upstreamHead: { global: initialBackendHead, client: EventId.clientDefault },
217
+ localHead: initialLocalHead,
218
+ })
219
+
220
+ /** State transitions need to happen atomically, so we use a Ref to track the state */
221
+ yield* SubscriptionRef.set(syncStateSref, initialSyncState)
222
+
223
+ // Rehydrate sync queue
224
+ if (pendingMutationEvents.length > 0) {
225
+ const filteredBatch = pendingMutationEvents
226
+ // Don't sync clientOnly mutations
227
+ .filter((mutationEventEncoded) => {
228
+ const mutationDef = getMutationDef(schema, mutationEventEncoded.mutation)
229
+ return mutationDef.options.clientOnly === false
230
+ })
231
+
232
+ yield* BucketQueue.offerAll(syncBackendQueue, filteredBatch)
233
+ }
234
+
235
+ const shutdownOnError = (cause: unknown) =>
236
+ Effect.gen(function* () {
237
+ yield* shutdownChannel.send(UnexpectedError.make({ cause }))
238
+ yield* Effect.die(cause)
239
+ })
240
+
241
+ yield* backgroundApplyLocalPushes({
242
+ localPushesLatch,
243
+ localPushesQueue,
244
+ pullLatch,
245
+ syncStateSref,
246
+ syncBackendQueue,
247
+ schema,
248
+ isLocalEvent,
249
+ otelSpan,
250
+ currentLocalPushGenerationRef,
251
+ }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
252
+
253
+ const backendPushingFiberHandle = yield* FiberHandle.make()
254
+
255
+ yield* FiberHandle.run(
256
+ backendPushingFiberHandle,
257
+ backgroundBackendPushing({
258
+ dbReady,
259
+ syncBackendQueue,
260
+ otelSpan,
261
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
262
+ }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError)),
263
+ )
264
+
265
+ yield* backgroundBackendPulling({
266
+ dbReady,
267
+ initialBackendHead,
268
+ isLocalEvent,
269
+ restartBackendPushing: (filteredRebasedPending) =>
270
+ Effect.gen(function* () {
271
+ // Stop current pushing fiber
272
+ yield* FiberHandle.clear(backendPushingFiberHandle)
273
+
274
+ // Reset the sync queue
275
+ yield* BucketQueue.clear(syncBackendQueue)
276
+ yield* BucketQueue.offerAll(syncBackendQueue, filteredRebasedPending)
277
+
278
+ // Restart pushing fiber
279
+ yield* FiberHandle.run(
280
+ backendPushingFiberHandle,
281
+ backgroundBackendPushing({
282
+ dbReady,
283
+ syncBackendQueue,
284
+ otelSpan,
285
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
286
+ }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError)),
287
+ )
288
+ }),
289
+ syncStateSref,
290
+ localPushesLatch,
291
+ pullLatch,
292
+ otelSpan,
293
+ initialBlockingSyncContext,
294
+ devtoolsLatch: ctxRef.current?.devtoolsLatch,
295
+ }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
296
+
297
+ return { initialLeaderHead: initialLocalHead }
298
+ }).pipe(Effect.withSpanScoped('@livestore/common:leader-thread:syncing'))
299
+
300
+ return {
301
+ push,
302
+ pushPartial,
303
+ boot,
304
+ syncState: Subscribable.make({
305
+ get: Effect.gen(function* () {
306
+ const syncState = yield* syncStateSref
307
+ if (syncState === undefined) return shouldNeverHappen('Not initialized')
308
+ return syncState
309
+ }),
310
+ changes: syncStateSref.changes.pipe(Stream.filter(isNotUndefined)),
311
+ }),
312
+ } satisfies LeaderSyncProcessor
313
+ })
314
+
315
+ const backgroundApplyLocalPushes = ({
316
+ localPushesLatch,
317
+ localPushesQueue,
318
+ pullLatch,
319
+ syncStateSref,
320
+ syncBackendQueue,
321
+ schema,
322
+ isLocalEvent,
323
+ otelSpan,
324
+ currentLocalPushGenerationRef,
325
+ }: {
326
+ pullLatch: Effect.Latch
327
+ localPushesLatch: Effect.Latch
328
+ localPushesQueue: BucketQueue.BucketQueue<LocalPushQueueItem>
329
+ syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
330
+ syncBackendQueue: BucketQueue.BucketQueue<MutationEvent.EncodedWithMeta>
331
+ schema: LiveStoreSchema
332
+ isLocalEvent: (mutationEventEncoded: MutationEvent.EncodedWithMeta) => boolean
333
+ otelSpan: otel.Span | undefined
334
+ currentLocalPushGenerationRef: { current: number }
335
+ }) =>
336
+ Effect.gen(function* () {
337
+ const { connectedClientSessionPullQueues, clientId } = yield* LeaderThreadCtx
338
+
339
+ const applyMutationItems = yield* makeApplyMutationItems
340
+
341
+ while (true) {
342
+ // TODO make batch size configurable
343
+ const batchItems = yield* BucketQueue.takeBetween(localPushesQueue, 1, 10)
344
+
345
+ // Wait for the backend pulling to finish
346
+ yield* localPushesLatch.await
347
+
348
+ // Prevent backend pull processing until this local push is finished
349
+ yield* pullLatch.close
350
+
351
+ // Since the generation might have changed since enqueuing, we need to filter out items with older generation
352
+ // It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
353
+ const filteredBatchItems = batchItems
354
+ .filter(([_1, _2, generation]) => generation === currentLocalPushGenerationRef.current)
355
+ .map(([mutationEventEncoded, deferred]) => [mutationEventEncoded, deferred] as const)
356
+
357
+ if (filteredBatchItems.length === 0) {
358
+ // console.log('dropping old-gen batch', currentLocalPushGenerationRef.current)
359
+ // Allow the backend pulling to start
360
+ yield* pullLatch.open
361
+ continue
362
+ }
363
+
364
+ const [newEvents, deferreds] = ReadonlyArray.unzip(filteredBatchItems)
365
+
366
+ const syncState = yield* syncStateSref
367
+ if (syncState === undefined) return shouldNeverHappen('Not initialized')
368
+
369
+ const updateResult = SyncState.updateSyncState({
370
+ syncState,
371
+ payload: { _tag: 'local-push', newEvents },
372
+ isLocalEvent,
373
+ isEqualEvent: MutationEvent.isEqualEncoded,
374
+ })
375
+
376
+ switch (updateResult._tag) {
377
+ case 'unexpected-error': {
378
+ otelSpan?.addEvent('local-push:unexpected-error', {
379
+ batchSize: newEvents.length,
380
+ newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
381
+ })
382
+ return yield* Effect.fail(updateResult.cause)
383
+ }
384
+ case 'rebase': {
385
+ return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
386
+ }
387
+ case 'reject': {
388
+ otelSpan?.addEvent('local-push:reject', {
389
+ batchSize: newEvents.length,
390
+ updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
391
+ })
392
+
393
+ /*
394
+
395
+ TODO: how to test this?
396
+ */
397
+ currentLocalPushGenerationRef.current++
398
+
399
+ const nextGeneration = currentLocalPushGenerationRef.current
400
+
401
+ const providedId = newEvents.at(0)!.id
402
+ // All subsequent pushes with same generation should be rejected as well
403
+ // We're also handling the case where the localPushQueue already contains events
404
+ // from the next generation which we preserve in the queue
405
+ const remainingEventsMatchingGeneration = yield* BucketQueue.takeSplitWhere(
406
+ localPushesQueue,
407
+ (item) => item[2] >= nextGeneration,
408
+ )
409
+
410
+ if ((yield* BucketQueue.size(localPushesQueue)) > 0) {
411
+ console.log('localPushesQueue is not empty', yield* BucketQueue.size(localPushesQueue))
412
+ debugger
413
+ }
414
+
415
+ const allDeferredsToReject = [
416
+ ...deferreds,
417
+ ...remainingEventsMatchingGeneration.map(([_, deferred]) => deferred),
418
+ ].filter(isNotUndefined)
419
+
420
+ yield* Effect.forEach(allDeferredsToReject, (deferred) =>
421
+ Deferred.fail(
422
+ deferred,
423
+ LeaderAheadError.make({
424
+ minimumExpectedId: updateResult.expectedMinimumId,
425
+ providedId,
426
+ // nextGeneration,
427
+ }),
428
+ ),
429
+ )
430
+
431
+ // Allow the backend pulling to start
432
+ yield* pullLatch.open
433
+
434
+ // In this case we're skipping state update and down/upstream processing
435
+ // We've cleared the local push queue and are now waiting for new local pushes / backend pulls
436
+ continue
437
+ }
438
+ case 'advance': {
439
+ break
440
+ }
441
+ default: {
442
+ casesHandled(updateResult)
443
+ }
444
+ }
445
+
446
+ yield* SubscriptionRef.set(syncStateSref, updateResult.newSyncState)
447
+
448
+ if (clientId === 'client-b') {
449
+ // yield* Effect.log('offer upstream-advance due to local-push')
450
+ // debugger
451
+ }
452
+ yield* connectedClientSessionPullQueues.offer({
453
+ payload: { _tag: 'upstream-advance', newEvents: updateResult.newEvents },
454
+ remaining: 0,
455
+ })
456
+
457
+ otelSpan?.addEvent('local-push', {
458
+ batchSize: newEvents.length,
459
+ updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
460
+ })
461
+
462
+ // Don't sync clientOnly mutations
463
+ const filteredBatch = updateResult.newEvents.filter((mutationEventEncoded) => {
464
+ const mutationDef = getMutationDef(schema, mutationEventEncoded.mutation)
465
+ return mutationDef.options.clientOnly === false
466
+ })
467
+
468
+ yield* BucketQueue.offerAll(syncBackendQueue, filteredBatch)
469
+
470
+ yield* applyMutationItems({ batchItems: newEvents, deferreds })
471
+
472
+ // Allow the backend pulling to start
473
+ yield* pullLatch.open
474
+ }
475
+ })
476
+
477
+ type ApplyMutationItems = (_: {
478
+ batchItems: ReadonlyArray<MutationEvent.EncodedWithMeta>
479
+ /** Indexes are aligned with `batchItems` */
480
+ deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError> | undefined> | undefined
481
+ }) => Effect.Effect<void, UnexpectedError>
482
+
483
+ // TODO how to handle errors gracefully
484
+ const makeApplyMutationItems: Effect.Effect<ApplyMutationItems, UnexpectedError, LeaderThreadCtx | Scope.Scope> =
485
+ Effect.gen(function* () {
486
+ const leaderThreadCtx = yield* LeaderThreadCtx
487
+ const { dbReadModel: db, dbMutationLog } = leaderThreadCtx
488
+
489
+ const applyMutation = yield* makeApplyMutation
490
+
491
+ return ({ batchItems, deferreds }) =>
492
+ Effect.gen(function* () {
493
+ db.execute('BEGIN TRANSACTION', undefined) // Start the transaction
494
+ dbMutationLog.execute('BEGIN TRANSACTION', undefined) // Start the transaction
495
+
496
+ yield* Effect.addFinalizer((exit) =>
497
+ Effect.gen(function* () {
498
+ if (Exit.isSuccess(exit)) return
499
+
500
+ // Rollback in case of an error
501
+ db.execute('ROLLBACK', undefined)
502
+ dbMutationLog.execute('ROLLBACK', undefined)
503
+ }),
504
+ )
505
+
506
+ for (let i = 0; i < batchItems.length; i++) {
507
+ yield* applyMutation(batchItems[i]!)
508
+
509
+ if (deferreds?.[i] !== undefined) {
510
+ yield* Deferred.succeed(deferreds[i]!, void 0)
511
+ }
512
+ }
513
+
514
+ db.execute('COMMIT', undefined) // Commit the transaction
515
+ dbMutationLog.execute('COMMIT', undefined) // Commit the transaction
516
+ }).pipe(
517
+ Effect.uninterruptible,
518
+ Effect.scoped,
519
+ Effect.withSpan('@livestore/common:leader-thread:syncing:applyMutationItems', {
520
+ attributes: { count: batchItems.length },
521
+ }),
522
+ Effect.tapCauseLogPretty,
523
+ UnexpectedError.mapToUnexpectedError,
524
+ )
525
+ })
526
+
527
+ const backgroundBackendPulling = ({
528
+ dbReady,
529
+ initialBackendHead,
530
+ isLocalEvent,
531
+ restartBackendPushing,
532
+ otelSpan,
533
+ syncStateSref,
534
+ localPushesLatch,
535
+ pullLatch,
536
+ devtoolsLatch,
537
+ initialBlockingSyncContext,
538
+ }: {
539
+ dbReady: Deferred.Deferred<void>
540
+ initialBackendHead: EventId.GlobalEventId
541
+ isLocalEvent: (mutationEventEncoded: MutationEvent.EncodedWithMeta) => boolean
542
+ restartBackendPushing: (
543
+ filteredRebasedPending: ReadonlyArray<MutationEvent.EncodedWithMeta>,
544
+ ) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx | HttpClient.HttpClient>
545
+ otelSpan: otel.Span | undefined
546
+ syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
547
+ localPushesLatch: Effect.Latch
548
+ pullLatch: Effect.Latch
549
+ devtoolsLatch: Effect.Latch | undefined
550
+ initialBlockingSyncContext: InitialBlockingSyncContext
551
+ }) =>
552
+ Effect.gen(function* () {
553
+ const {
554
+ syncBackend,
555
+ dbReadModel: db,
556
+ dbMutationLog,
557
+ connectedClientSessionPullQueues,
558
+ schema,
559
+ clientId,
560
+ } = yield* LeaderThreadCtx
561
+
562
+ if (syncBackend === undefined) return
563
+
564
+ const cursorInfo = yield* getCursorInfo(initialBackendHead)
565
+
566
+ const applyMutationItems = yield* makeApplyMutationItems
567
+
568
+ const onNewPullChunk = (newEvents: MutationEvent.EncodedWithMeta[], remaining: number) =>
569
+ Effect.gen(function* () {
570
+ if (newEvents.length === 0) return
571
+
572
+ if (devtoolsLatch !== undefined) {
573
+ yield* devtoolsLatch.await
574
+ }
575
+
576
+ // Prevent more local pushes from being processed until this pull is finished
577
+ yield* localPushesLatch.close
578
+
579
+ // Wait for pending local pushes to finish
580
+ yield* pullLatch.await
581
+
582
+ const syncState = yield* syncStateSref
583
+ if (syncState === undefined) return shouldNeverHappen('Not initialized')
584
+
585
+ const trimRollbackUntil = newEvents.at(-1)!.id
586
+
587
+ const updateResult = SyncState.updateSyncState({
588
+ syncState,
589
+ payload: { _tag: 'upstream-advance', newEvents, trimRollbackUntil },
590
+ isLocalEvent,
591
+ isEqualEvent: MutationEvent.isEqualEncoded,
592
+ ignoreLocalEvents: true,
593
+ })
594
+
595
+ if (updateResult._tag === 'reject') {
596
+ return shouldNeverHappen('The leader thread should never reject upstream advances')
597
+ } else if (updateResult._tag === 'unexpected-error') {
598
+ otelSpan?.addEvent('backend-pull:unexpected-error', {
599
+ newEventsCount: newEvents.length,
600
+ newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
601
+ })
602
+ return yield* Effect.fail(updateResult.cause)
603
+ }
604
+
605
+ const newBackendHead = newEvents.at(-1)!.id
606
+
607
+ updateBackendHead(dbMutationLog, newBackendHead)
608
+
609
+ if (updateResult._tag === 'rebase') {
610
+ otelSpan?.addEvent('backend-pull:rebase', {
611
+ newEventsCount: newEvents.length,
612
+ newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
613
+ rollbackCount: updateResult.eventsToRollback.length,
614
+ updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
615
+ })
616
+
617
+ const filteredRebasedPending = updateResult.newSyncState.pending.filter((mutationEvent) => {
618
+ const mutationDef = getMutationDef(schema, mutationEvent.mutation)
619
+ return mutationDef.options.clientOnly === false
620
+ })
621
+ yield* restartBackendPushing(filteredRebasedPending)
622
+
623
+ if (updateResult.eventsToRollback.length > 0) {
624
+ yield* rollback({ db, dbMutationLog, eventIdsToRollback: updateResult.eventsToRollback.map((_) => _.id) })
625
+ }
626
+
627
+ yield* connectedClientSessionPullQueues.offer({
628
+ payload: {
629
+ _tag: 'upstream-rebase',
630
+ newEvents: updateResult.newEvents,
631
+ rollbackUntil: updateResult.eventsToRollback.at(0)!.id,
632
+ trimRollbackUntil,
633
+ },
634
+ remaining,
635
+ })
636
+ } else {
637
+ otelSpan?.addEvent('backend-pull:advance', {
638
+ newEventsCount: newEvents.length,
639
+ updateResult: TRACE_VERBOSE ? JSON.stringify(updateResult) : undefined,
640
+ })
641
+
642
+ if (clientId === 'client-b') {
643
+ // yield* Effect.log('offer upstream-advance due to pull')
644
+ }
645
+ yield* connectedClientSessionPullQueues.offer({
646
+ payload: { _tag: 'upstream-advance', newEvents: updateResult.newEvents, trimRollbackUntil },
647
+ remaining,
648
+ })
649
+ }
650
+
651
+ trimChangesetRows(db, newBackendHead)
652
+
653
+ yield* applyMutationItems({ batchItems: updateResult.newEvents, deferreds: undefined })
654
+
655
+ yield* SubscriptionRef.set(syncStateSref, updateResult.newSyncState)
656
+
657
+ if (remaining === 0) {
658
+ // Allow local pushes to be processed again
659
+ yield* localPushesLatch.open
660
+ }
661
+ })
662
+
663
+ yield* syncBackend.pull(cursorInfo).pipe(
664
+ // TODO only take from queue while connected
665
+ Stream.tap(({ batch, remaining }) =>
666
+ Effect.gen(function* () {
667
+ // yield* Effect.spanEvent('batch', {
668
+ // attributes: {
669
+ // batchSize: batch.length,
670
+ // batch: TRACE_VERBOSE ? batch : undefined,
671
+ // },
672
+ // })
673
+
674
+ // Wait for the db to be initially created
675
+ yield* dbReady
676
+
677
+ // NOTE we only want to take process mutations when the sync backend is connected
678
+ // (e.g. needed for simulating being offline)
679
+ // TODO remove when there's a better way to handle this in stream above
680
+ yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
681
+
682
+ yield* onNewPullChunk(
683
+ batch.map((_) => MutationEvent.EncodedWithMeta.fromGlobal(_.mutationEventEncoded)),
684
+ remaining,
685
+ )
686
+
687
+ yield* initialBlockingSyncContext.update({ processed: batch.length, remaining })
688
+ }),
689
+ ),
690
+ Stream.runDrain,
691
+ Effect.interruptible,
692
+ )
693
+ }).pipe(Effect.withSpan('@livestore/common:leader-thread:syncing:backend-pulling'))
694
+
695
+ const rollback = ({
696
+ db,
697
+ dbMutationLog,
698
+ eventIdsToRollback,
699
+ }: {
700
+ db: SqliteDb
701
+ dbMutationLog: SqliteDb
702
+ eventIdsToRollback: EventId.EventId[]
703
+ }) =>
704
+ Effect.gen(function* () {
705
+ const rollbackEvents = db
706
+ .select<SessionChangesetMetaRow>(
707
+ sql`SELECT * FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`).join(', ')})`,
708
+ )
709
+ .map((_) => ({ id: { global: _.idGlobal, client: _.idClient }, changeset: _.changeset, debug: _.debug }))
710
+ .toSorted((a, b) => EventId.compare(a.id, b.id))
711
+
712
+ // Apply changesets in reverse order
713
+ for (let i = rollbackEvents.length - 1; i >= 0; i--) {
714
+ const { changeset } = rollbackEvents[i]!
715
+ if (changeset !== null) {
716
+ db.makeChangeset(changeset).invert().apply()
717
+ }
718
+ }
719
+
720
+ const eventIdPairChunks = ReadonlyArray.chunksOf(100)(
721
+ eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`),
722
+ )
723
+
724
+ // Delete the changeset rows
725
+ for (const eventIdPairChunk of eventIdPairChunks) {
726
+ db.execute(
727
+ sql`DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdPairChunk.join(', ')})`,
728
+ )
729
+ }
730
+
731
+ // Delete the mutation log rows
732
+ for (const eventIdPairChunk of eventIdPairChunks) {
733
+ dbMutationLog.execute(
734
+ sql`DELETE FROM ${MUTATION_LOG_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdPairChunk.join(', ')})`,
735
+ )
736
+ }
737
+ }).pipe(
738
+ Effect.withSpan('@livestore/common:leader-thread:syncing:rollback', {
739
+ attributes: { count: eventIdsToRollback.length },
740
+ }),
741
+ )
742
+
743
+ const getCursorInfo = (remoteHead: EventId.GlobalEventId) =>
744
+ Effect.gen(function* () {
745
+ const { dbMutationLog } = yield* LeaderThreadCtx
746
+
747
+ if (remoteHead === EventId.ROOT.global) return Option.none()
748
+
749
+ const MutationlogQuerySchema = Schema.Struct({
750
+ syncMetadataJson: Schema.parseJson(Schema.Option(Schema.JsonValue)),
751
+ }).pipe(Schema.pluck('syncMetadataJson'), Schema.Array, Schema.head)
752
+
753
+ const syncMetadataOption = yield* Effect.sync(() =>
754
+ dbMutationLog.select<{ syncMetadataJson: string }>(
755
+ sql`SELECT syncMetadataJson FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ${remoteHead} ORDER BY idClient ASC LIMIT 1`,
756
+ ),
757
+ ).pipe(Effect.andThen(Schema.decode(MutationlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
758
+
759
+ return Option.some({
760
+ cursor: { global: remoteHead, client: EventId.clientDefault },
761
+ metadata: syncMetadataOption,
762
+ }) satisfies InitialSyncInfo
763
+ }).pipe(Effect.withSpan('@livestore/common:leader-thread:syncing:getCursorInfo', { attributes: { remoteHead } }))
764
+
765
+ const backgroundBackendPushing = ({
766
+ dbReady,
767
+ syncBackendQueue,
768
+ otelSpan,
769
+ devtoolsLatch,
770
+ }: {
771
+ dbReady: Deferred.Deferred<void>
772
+ syncBackendQueue: BucketQueue.BucketQueue<MutationEvent.EncodedWithMeta>
773
+ otelSpan: otel.Span | undefined
774
+ devtoolsLatch: Effect.Latch | undefined
775
+ }) =>
776
+ Effect.gen(function* () {
777
+ const { syncBackend, dbMutationLog } = yield* LeaderThreadCtx
778
+ if (syncBackend === undefined) return
779
+
780
+ yield* dbReady
781
+
782
+ while (true) {
783
+ yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
784
+
785
+ // TODO make batch size configurable
786
+ const queueItems = yield* BucketQueue.takeBetween(syncBackendQueue, 1, BACKEND_PUSH_BATCH_SIZE)
787
+
788
+ yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
789
+
790
+ if (devtoolsLatch !== undefined) {
791
+ yield* devtoolsLatch.await
792
+ }
793
+
794
+ otelSpan?.addEvent('backend-push', {
795
+ batchSize: queueItems.length,
796
+ batch: TRACE_VERBOSE ? JSON.stringify(queueItems) : undefined,
797
+ })
798
+
799
+ // TODO handle push errors (should only happen during concurrent pull+push)
800
+ const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
801
+
802
+ if (pushResult._tag === 'Left') {
803
+ if (LS_DEV) {
804
+ yield* Effect.logDebug('handled backend-push-error', { error: pushResult.left.toString() })
805
+ }
806
+ otelSpan?.addEvent('backend-push-error', { error: pushResult.left.toString() })
807
+ // wait for interrupt caused by background pulling which will then restart pushing
808
+ return yield* Effect.never
809
+ }
810
+
811
+ const { metadata } = pushResult.right
812
+
813
+ // TODO try to do this in a single query
814
+ for (let i = 0; i < queueItems.length; i++) {
815
+ const mutationEventEncoded = queueItems[i]!
816
+ yield* execSql(
817
+ dbMutationLog,
818
+ ...updateRows({
819
+ tableName: MUTATION_LOG_META_TABLE,
820
+ columns: mutationLogMetaTable.sqliteDef.columns,
821
+ where: { idGlobal: mutationEventEncoded.id.global, idClient: mutationEventEncoded.id.client },
822
+ updateValues: { syncMetadataJson: metadata[i]! },
823
+ }),
824
+ )
825
+ }
826
+ }
827
+ }).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:leader-thread:syncing:backend-pushing'))
828
+
829
+ const trimChangesetRows = (db: SqliteDb, newHead: EventId.EventId) => {
830
+ // Since we're using the session changeset rows to query for the current head,
831
+ // we're keeping at least one row for the current head, and thus are using `<` instead of `<=`
832
+ db.execute(sql`DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE idGlobal < ${newHead.global}`)
833
+ }