@livestore/common 0.4.0-dev.1 → 0.4.0-dev.11

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 (263) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts +8 -3
  3. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
  4. package/dist/ClientSessionLeaderThreadProxy.js.map +1 -1
  5. package/dist/adapter-types.d.ts +9 -3
  6. package/dist/adapter-types.d.ts.map +1 -1
  7. package/dist/adapter-types.js.map +1 -1
  8. package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
  9. package/dist/devtools/devtools-messages-common.d.ts +7 -14
  10. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  11. package/dist/devtools/devtools-messages-common.js +1 -6
  12. package/dist/devtools/devtools-messages-common.js.map +1 -1
  13. package/dist/devtools/devtools-messages-leader.d.ts +27 -25
  14. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  15. package/dist/errors.d.ts +47 -5
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +22 -3
  18. package/dist/errors.js.map +1 -1
  19. package/dist/leader-thread/LeaderSyncProcessor.d.ts +7 -3
  20. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  21. package/dist/leader-thread/LeaderSyncProcessor.js +135 -52
  22. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  23. package/dist/leader-thread/eventlog.d.ts +4 -10
  24. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  25. package/dist/leader-thread/eventlog.js +4 -6
  26. package/dist/leader-thread/eventlog.js.map +1 -1
  27. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  28. package/dist/leader-thread/leader-worker-devtools.js +6 -2
  29. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  30. package/dist/leader-thread/make-leader-thread-layer.d.ts +1 -2
  31. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  32. package/dist/leader-thread/make-leader-thread-layer.js +68 -19
  33. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  34. package/dist/leader-thread/make-leader-thread-layer.test.d.ts +2 -0
  35. package/dist/leader-thread/make-leader-thread-layer.test.d.ts.map +1 -0
  36. package/dist/leader-thread/make-leader-thread-layer.test.js +32 -0
  37. package/dist/leader-thread/make-leader-thread-layer.test.js.map +1 -0
  38. package/dist/leader-thread/materialize-event.d.ts +2 -2
  39. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  40. package/dist/leader-thread/materialize-event.js +23 -9
  41. package/dist/leader-thread/materialize-event.js.map +1 -1
  42. package/dist/leader-thread/recreate-db.d.ts +2 -3
  43. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  44. package/dist/leader-thread/recreate-db.js +1 -1
  45. package/dist/leader-thread/recreate-db.js.map +1 -1
  46. package/dist/leader-thread/shutdown-channel.d.ts +2 -2
  47. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  48. package/dist/leader-thread/shutdown-channel.js +2 -2
  49. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  50. package/dist/leader-thread/types.d.ts +7 -5
  51. package/dist/leader-thread/types.d.ts.map +1 -1
  52. package/dist/leader-thread/types.js.map +1 -1
  53. package/dist/materializer-helper.d.ts +1 -1
  54. package/dist/materializer-helper.d.ts.map +1 -1
  55. package/dist/materializer-helper.js +20 -4
  56. package/dist/materializer-helper.js.map +1 -1
  57. package/dist/rematerialize-from-eventlog.d.ts +1 -1
  58. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  59. package/dist/rematerialize-from-eventlog.js +25 -16
  60. package/dist/rematerialize-from-eventlog.js.map +1 -1
  61. package/dist/schema/EventDef.d.ts +3 -0
  62. package/dist/schema/EventDef.d.ts.map +1 -1
  63. package/dist/schema/EventDef.js.map +1 -1
  64. package/dist/schema/EventSequenceNumber.d.ts +4 -1
  65. package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
  66. package/dist/schema/EventSequenceNumber.js +4 -1
  67. package/dist/schema/EventSequenceNumber.js.map +1 -1
  68. package/dist/schema/EventSequenceNumber.test.js +2 -2
  69. package/dist/schema/LiveStoreEvent.d.ts +1 -1
  70. package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
  71. package/dist/schema/LiveStoreEvent.js +1 -2
  72. package/dist/schema/LiveStoreEvent.js.map +1 -1
  73. package/dist/schema/mod.d.ts +2 -0
  74. package/dist/schema/mod.d.ts.map +1 -1
  75. package/dist/schema/mod.js +1 -0
  76. package/dist/schema/mod.js.map +1 -1
  77. package/dist/schema/schema.d.ts +15 -0
  78. package/dist/schema/schema.d.ts.map +1 -1
  79. package/dist/schema/schema.js +26 -1
  80. package/dist/schema/schema.js.map +1 -1
  81. package/dist/schema/state/sqlite/client-document-def.d.ts +35 -5
  82. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
  83. package/dist/schema/state/sqlite/client-document-def.js +95 -4
  84. package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
  85. package/dist/schema/state/sqlite/client-document-def.test.js +16 -0
  86. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
  87. package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
  88. package/dist/schema/state/sqlite/column-annotations.js +14 -6
  89. package/dist/schema/state/sqlite/column-annotations.js.map +1 -1
  90. package/dist/schema/state/sqlite/column-def.d.ts +6 -2
  91. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
  92. package/dist/schema/state/sqlite/column-def.js +128 -185
  93. package/dist/schema/state/sqlite/column-def.js.map +1 -1
  94. package/dist/schema/state/sqlite/column-def.test.js +116 -73
  95. package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
  96. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -1
  97. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  98. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +23 -6
  99. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  100. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  101. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +2 -1
  102. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
  103. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  104. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  105. package/dist/schema/state/sqlite/mod.js +1 -1
  106. package/dist/schema/state/sqlite/mod.js.map +1 -1
  107. package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
  108. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  109. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  110. package/dist/schema/state/sqlite/query-builder/impl.js +6 -2
  111. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  112. package/dist/schema/state/sqlite/query-builder/impl.test.js +137 -2
  113. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  114. package/dist/schema/state/sqlite/system-tables.d.ts +42 -6
  115. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  116. package/dist/schema/state/sqlite/system-tables.js +2 -0
  117. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  118. package/dist/schema/state/sqlite/table-def.d.ts +4 -4
  119. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  120. package/dist/schema/state/sqlite/table-def.js +2 -2
  121. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  122. package/dist/schema/state/sqlite/table-def.test.js +87 -2
  123. package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
  124. package/dist/schema/unknown-events.d.ts +47 -0
  125. package/dist/schema/unknown-events.d.ts.map +1 -0
  126. package/dist/schema/unknown-events.js +69 -0
  127. package/dist/schema/unknown-events.js.map +1 -0
  128. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  129. package/dist/sql-queries/sql-query-builder.js +2 -1
  130. package/dist/sql-queries/sql-query-builder.js.map +1 -1
  131. package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -11
  132. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  133. package/dist/sync/ClientSessionSyncProcessor.js +36 -33
  134. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  135. package/dist/sync/errors.d.ts +61 -0
  136. package/dist/sync/errors.d.ts.map +1 -0
  137. package/dist/sync/errors.js +36 -0
  138. package/dist/sync/errors.js.map +1 -0
  139. package/dist/sync/index.d.ts +3 -0
  140. package/dist/sync/index.d.ts.map +1 -1
  141. package/dist/sync/index.js +3 -0
  142. package/dist/sync/index.js.map +1 -1
  143. package/dist/sync/mock-sync-backend.d.ts +23 -0
  144. package/dist/sync/mock-sync-backend.d.ts.map +1 -0
  145. package/dist/sync/mock-sync-backend.js +114 -0
  146. package/dist/sync/mock-sync-backend.js.map +1 -0
  147. package/dist/sync/next/compact-events.d.ts.map +1 -1
  148. package/dist/sync/next/compact-events.js +4 -5
  149. package/dist/sync/next/compact-events.js.map +1 -1
  150. package/dist/sync/next/facts.d.ts.map +1 -1
  151. package/dist/sync/next/facts.js +1 -2
  152. package/dist/sync/next/facts.js.map +1 -1
  153. package/dist/sync/next/history-dag-common.d.ts +50 -11
  154. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  155. package/dist/sync/next/history-dag-common.js +193 -4
  156. package/dist/sync/next/history-dag-common.js.map +1 -1
  157. package/dist/sync/next/history-dag.d.ts.map +1 -1
  158. package/dist/sync/next/history-dag.js +3 -1
  159. package/dist/sync/next/history-dag.js.map +1 -1
  160. package/dist/sync/sync-backend-kv.d.ts +7 -0
  161. package/dist/sync/sync-backend-kv.d.ts.map +1 -0
  162. package/dist/sync/sync-backend-kv.js +18 -0
  163. package/dist/sync/sync-backend-kv.js.map +1 -0
  164. package/dist/sync/sync-backend.d.ts +105 -0
  165. package/dist/sync/sync-backend.d.ts.map +1 -0
  166. package/dist/sync/sync-backend.js +61 -0
  167. package/dist/sync/sync-backend.js.map +1 -0
  168. package/dist/sync/sync.d.ts +6 -84
  169. package/dist/sync/sync.d.ts.map +1 -1
  170. package/dist/sync/sync.js +2 -27
  171. package/dist/sync/sync.js.map +1 -1
  172. package/dist/sync/syncstate.test.js +16 -15
  173. package/dist/sync/syncstate.test.js.map +1 -1
  174. package/dist/sync/transport-chunking.d.ts +36 -0
  175. package/dist/sync/transport-chunking.d.ts.map +1 -0
  176. package/dist/sync/transport-chunking.js +56 -0
  177. package/dist/sync/transport-chunking.js.map +1 -0
  178. package/dist/sync/validate-push-payload.d.ts +1 -1
  179. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  180. package/dist/sync/validate-push-payload.js +6 -6
  181. package/dist/sync/validate-push-payload.js.map +1 -1
  182. package/dist/testing/event-factory.d.ts +68 -0
  183. package/dist/testing/event-factory.d.ts.map +1 -0
  184. package/dist/testing/event-factory.js +80 -0
  185. package/dist/testing/event-factory.js.map +1 -0
  186. package/dist/testing/mod.d.ts +2 -0
  187. package/dist/testing/mod.d.ts.map +1 -0
  188. package/dist/testing/mod.js +2 -0
  189. package/dist/testing/mod.js.map +1 -0
  190. package/dist/version.d.ts +2 -2
  191. package/dist/version.d.ts.map +1 -1
  192. package/dist/version.js +2 -2
  193. package/dist/version.js.map +1 -1
  194. package/package.json +7 -8
  195. package/src/ClientSessionLeaderThreadProxy.ts +8 -3
  196. package/src/adapter-types.ts +13 -3
  197. package/src/devtools/devtools-messages-common.ts +1 -8
  198. package/src/errors.ts +33 -4
  199. package/src/leader-thread/LeaderSyncProcessor.ts +204 -63
  200. package/src/leader-thread/eventlog.ts +10 -6
  201. package/src/leader-thread/leader-worker-devtools.ts +6 -2
  202. package/src/leader-thread/make-leader-thread-layer.test.ts +44 -0
  203. package/src/leader-thread/make-leader-thread-layer.ts +137 -26
  204. package/src/leader-thread/materialize-event.ts +34 -9
  205. package/src/leader-thread/recreate-db.ts +11 -3
  206. package/src/leader-thread/shutdown-channel.ts +16 -2
  207. package/src/leader-thread/types.ts +7 -5
  208. package/src/materializer-helper.ts +22 -5
  209. package/src/rematerialize-from-eventlog.ts +33 -23
  210. package/src/schema/EventDef.ts +3 -0
  211. package/src/schema/EventSequenceNumber.test.ts +2 -2
  212. package/src/schema/EventSequenceNumber.ts +5 -2
  213. package/src/schema/LiveStoreEvent.ts +1 -2
  214. package/src/schema/mod.ts +2 -0
  215. package/src/schema/schema.ts +37 -1
  216. package/src/schema/state/sqlite/client-document-def.test.ts +17 -0
  217. package/src/schema/state/sqlite/client-document-def.ts +117 -5
  218. package/src/schema/state/sqlite/column-annotations.ts +16 -6
  219. package/src/schema/state/sqlite/column-def.test.ts +150 -93
  220. package/src/schema/state/sqlite/column-def.ts +136 -203
  221. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +26 -6
  222. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +2 -1
  223. package/src/schema/state/sqlite/mod.ts +1 -0
  224. package/src/schema/state/sqlite/query-builder/api.ts +7 -2
  225. package/src/schema/state/sqlite/query-builder/impl.test.ts +187 -6
  226. package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
  227. package/src/schema/state/sqlite/system-tables.ts +2 -0
  228. package/src/schema/state/sqlite/table-def.test.ts +112 -2
  229. package/src/schema/state/sqlite/table-def.ts +9 -8
  230. package/src/schema/unknown-events.ts +131 -0
  231. package/src/sql-queries/sql-query-builder.ts +2 -1
  232. package/src/sync/ClientSessionSyncProcessor.ts +56 -49
  233. package/src/sync/errors.ts +38 -0
  234. package/src/sync/index.ts +3 -0
  235. package/src/sync/mock-sync-backend.ts +184 -0
  236. package/src/sync/next/compact-events.ts +4 -5
  237. package/src/sync/next/facts.ts +1 -3
  238. package/src/sync/next/history-dag-common.ts +272 -21
  239. package/src/sync/next/history-dag.ts +3 -1
  240. package/src/sync/sync-backend-kv.ts +22 -0
  241. package/src/sync/sync-backend.ts +185 -0
  242. package/src/sync/sync.ts +6 -89
  243. package/src/sync/syncstate.test.ts +17 -15
  244. package/src/sync/transport-chunking.ts +90 -0
  245. package/src/sync/validate-push-payload.ts +6 -7
  246. package/src/testing/event-factory.ts +133 -0
  247. package/src/testing/mod.ts +1 -0
  248. package/src/version.ts +2 -2
  249. package/dist/schema-management/migrations.test.d.ts +0 -2
  250. package/dist/schema-management/migrations.test.d.ts.map +0 -1
  251. package/dist/schema-management/migrations.test.js +0 -52
  252. package/dist/schema-management/migrations.test.js.map +0 -1
  253. package/dist/sync/next/graphology.d.ts +0 -8
  254. package/dist/sync/next/graphology.d.ts.map +0 -1
  255. package/dist/sync/next/graphology.js +0 -30
  256. package/dist/sync/next/graphology.js.map +0 -1
  257. package/dist/sync/next/graphology_.d.ts +0 -3
  258. package/dist/sync/next/graphology_.d.ts.map +0 -1
  259. package/dist/sync/next/graphology_.js +0 -3
  260. package/dist/sync/next/graphology_.js.map +0 -1
  261. package/src/sync/next/ambient.d.ts +0 -3
  262. package/src/sync/next/graphology.ts +0 -41
  263. package/src/sync/next/graphology_.ts +0 -2
@@ -2,14 +2,7 @@ import { Schema } from '@livestore/utils/effect'
2
2
 
3
3
  import { liveStoreVersion as pkgVersion } from '../version.ts'
4
4
 
5
- export const NetworkStatus = Schema.Struct({
6
- isConnected: Schema.Boolean,
7
- timestampMs: Schema.Number,
8
- /** Whether the network status devtools latch is closed. Used to simulate network disconnection. */
9
- latchClosed: Schema.Boolean,
10
- })
11
-
12
- export type NetworkStatus = typeof NetworkStatus.Type
5
+ export { NetworkStatus } from '../sync/sync-backend.ts'
13
6
 
14
7
  export const requestId = Schema.String
15
8
  export const clientId = Schema.String
package/src/errors.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { Effect, Schema, Stream } from '@livestore/utils/effect'
1
+ import { Cause, Effect, Layer, Schema, Stream } from '@livestore/utils/effect'
2
+
3
+ import * as LiveStoreEvent from './schema/LiveStoreEvent.ts'
2
4
 
3
5
  export class UnexpectedError extends Schema.TaggedError<UnexpectedError>()('LiveStore.UnexpectedError', {
4
6
  cause: Schema.Defect,
@@ -11,15 +13,30 @@ export class UnexpectedError extends Schema.TaggedError<UnexpectedError>()('Live
11
13
  Effect.catchAllDefect((cause) => new UnexpectedError({ cause })),
12
14
  )
13
15
 
16
+ static mapToUnexpectedErrorLayer = <A, E, R>(layer: Layer.Layer<A, E, R>) =>
17
+ layer.pipe(
18
+ Layer.catchAllCause((cause) =>
19
+ Cause.isFailType(cause) && Schema.is(UnexpectedError)(cause.error)
20
+ ? Layer.fail(cause.error)
21
+ : Layer.fail(new UnexpectedError({ cause: cause })),
22
+ ),
23
+ )
24
+
14
25
  static mapToUnexpectedErrorStream = <A, E, R>(stream: Stream.Stream<A, E, R>) =>
15
26
  stream.pipe(
16
27
  Stream.mapError((cause) => (Schema.is(UnexpectedError)(cause) ? cause : new UnexpectedError({ cause }))),
17
28
  )
18
29
  }
19
30
 
20
- export class SyncError extends Schema.TaggedError<SyncError>()('LiveStore.SyncError', {
21
- cause: Schema.Defect,
22
- }) {}
31
+ export class MaterializerHashMismatchError extends Schema.TaggedError<MaterializerHashMismatchError>()(
32
+ 'LiveStore.MaterializerHashMismatchError',
33
+ {
34
+ eventName: Schema.String,
35
+ note: Schema.optionalWith(Schema.String, {
36
+ default: () => 'Please make sure your event materializer is a pure function without side effects.',
37
+ }),
38
+ },
39
+ ) {}
23
40
 
24
41
  export class IntentionalShutdownCause extends Schema.TaggedError<IntentionalShutdownCause>()(
25
42
  'LiveStore.IntentionalShutdownCause',
@@ -47,3 +64,15 @@ export class SqliteError extends Schema.TaggedError<SqliteError>()('LiveStore.Sq
47
64
  cause: Schema.Defect,
48
65
  note: Schema.optional(Schema.String),
49
66
  }) {}
67
+
68
+ export class UnknownEventError extends Schema.TaggedError<UnknownEventError>()('LiveStore.UnknownEventError', {
69
+ event: LiveStoreEvent.AnyEncoded.pipe(Schema.pick('name', 'args', 'seqNum', 'clientId', 'sessionId')),
70
+ reason: Schema.Literal('event-definition-missing', 'materializer-missing'),
71
+ operation: Schema.String,
72
+ note: Schema.optional(Schema.String),
73
+ }) {}
74
+
75
+ export class MaterializeError extends Schema.TaggedError<MaterializeError>()('LiveStore.MaterializeError', {
76
+ cause: Schema.Union(MaterializerHashMismatchError, SqliteError, UnknownEventError),
77
+ note: Schema.optional(Schema.String),
78
+ }) {}
@@ -2,27 +2,39 @@ import { casesHandled, isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE
2
2
  import type { HttpClient, Runtime, Scope, Tracer } from '@livestore/utils/effect'
3
3
  import {
4
4
  BucketQueue,
5
+ Cause,
5
6
  Deferred,
7
+ Duration,
6
8
  Effect,
7
9
  Exit,
8
10
  FiberHandle,
11
+ Layer,
9
12
  Option,
10
13
  OtelTracer,
11
- pipe,
12
14
  Queue,
13
15
  ReadonlyArray,
16
+ Schedule,
14
17
  Stream,
15
18
  Subscribable,
16
19
  SubscriptionRef,
17
20
  } from '@livestore/utils/effect'
18
21
  import type * as otel from '@opentelemetry/api'
19
-
20
- import type { SqliteDb } from '../adapter-types.ts'
21
- import { SyncError, UnexpectedError } from '../adapter-types.ts'
22
+ import {
23
+ type IntentionalShutdownCause,
24
+ type MaterializeError,
25
+ type SqliteDb,
26
+ UnexpectedError,
27
+ } from '../adapter-types.ts'
22
28
  import { makeMaterializerHash } from '../materializer-helper.ts'
23
29
  import type { LiveStoreSchema } from '../schema/mod.ts'
24
- import { EventSequenceNumber, getEventDef, LiveStoreEvent, SystemTables } from '../schema/mod.ts'
25
- import { LeaderAheadError } from '../sync/sync.ts'
30
+ import { EventSequenceNumber, LiveStoreEvent, resolveEventDef, SystemTables } from '../schema/mod.ts'
31
+ import {
32
+ type InvalidPullError,
33
+ type InvalidPushError,
34
+ type IsOfflineError,
35
+ LeaderAheadError,
36
+ type SyncBackend,
37
+ } from '../sync/sync.ts'
26
38
  import * as SyncState from '../sync/syncstate.ts'
27
39
  import { sql } from '../util.ts'
28
40
  import * as Eventlog from './eventlog.ts'
@@ -71,6 +83,7 @@ export const makeLeaderSyncProcessor = ({
71
83
  initialBlockingSyncContext,
72
84
  initialSyncState,
73
85
  onError,
86
+ livePull,
74
87
  params,
75
88
  testing,
76
89
  }: {
@@ -90,6 +103,11 @@ export const makeLeaderSyncProcessor = ({
90
103
  */
91
104
  backendPushBatchSize?: number
92
105
  }
106
+ /**
107
+ * Whether the sync backend should reactively pull new events from the sync backend
108
+ * When `false`, the sync processor will only do an initial pull
109
+ */
110
+ livePull: boolean
93
111
  testing: {
94
112
  delays?: {
95
113
  localPushProcessing?: Effect.Effect<void>
@@ -103,10 +121,8 @@ export const makeLeaderSyncProcessor = ({
103
121
 
104
122
  const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
105
123
 
106
- const isClientEvent = (eventEncoded: LiveStoreEvent.EncodedWithMeta) => {
107
- const { eventDef } = getEventDef(schema, eventEncoded.name)
108
- return eventDef.options.clientOnly
109
- }
124
+ const isClientEvent = (eventEncoded: LiveStoreEvent.EncodedWithMeta) =>
125
+ schema.eventsDefsMap.get(eventEncoded.name)?.options.clientOnly ?? false
110
126
 
111
127
  const connectedClientSessionPullQueues = yield* makePullQueueSet
112
128
 
@@ -180,14 +196,32 @@ export const makeLeaderSyncProcessor = ({
180
196
  const syncState = yield* syncStateSref
181
197
  if (syncState === undefined) return shouldNeverHappen('Not initialized')
182
198
 
183
- const { eventDef } = getEventDef(schema, name)
199
+ const resolution = yield* resolveEventDef(schema, {
200
+ operation: '@livestore/common:LeaderSyncProcessor:pushPartial',
201
+ event: {
202
+ name,
203
+ args,
204
+ clientId,
205
+ sessionId,
206
+ seqNum: syncState.localHead,
207
+ },
208
+ }).pipe(UnexpectedError.mapToUnexpectedError)
209
+
210
+ if (resolution._tag === 'unknown') {
211
+ // Ignore partial pushes for unrecognised events – they are still
212
+ // persisted server-side once a schema update ships.
213
+ return
214
+ }
184
215
 
185
216
  const eventEncoded = new LiveStoreEvent.EncodedWithMeta({
186
217
  name,
187
218
  args,
188
219
  clientId,
189
220
  sessionId,
190
- ...EventSequenceNumber.nextPair({ seqNum: syncState.localHead, isClient: eventDef.options.clientOnly }),
221
+ ...EventSequenceNumber.nextPair({
222
+ seqNum: syncState.localHead,
223
+ isClient: resolution.eventDef.options.clientOnly,
224
+ }),
191
225
  })
192
226
 
193
227
  yield* push([eventEncoded])
@@ -215,8 +249,8 @@ export const makeLeaderSyncProcessor = ({
215
249
  const globalPendingEvents = initialSyncState.pending
216
250
  // Don't sync clientOnly events
217
251
  .filter((eventEncoded) => {
218
- const { eventDef } = getEventDef(schema, eventEncoded.name)
219
- return eventDef.options.clientOnly === false
252
+ const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
253
+ return eventDef === undefined ? true : eventDef.options.clientOnly === false
220
254
  })
221
255
 
222
256
  if (globalPendingEvents.length > 0) {
@@ -224,12 +258,31 @@ export const makeLeaderSyncProcessor = ({
224
258
  }
225
259
  }
226
260
 
227
- const shutdownOnError = (cause: unknown) =>
261
+ const maybeShutdownOnError = (
262
+ cause: Cause.Cause<
263
+ | UnexpectedError
264
+ | IntentionalShutdownCause
265
+ | IsOfflineError
266
+ | InvalidPushError
267
+ | InvalidPullError
268
+ | MaterializeError
269
+ >,
270
+ ) =>
228
271
  Effect.gen(function* () {
229
- if (onError === 'shutdown') {
230
- yield* shutdownChannel.send(UnexpectedError.make({ cause }))
231
- yield* Effect.die(cause)
272
+ if (onError === 'ignore') {
273
+ if (LS_DEV) {
274
+ yield* Effect.logDebug(
275
+ `Ignoring sync error (${cause._tag === 'Fail' ? cause.error._tag : cause._tag})`,
276
+ Cause.pretty(cause),
277
+ )
278
+ }
279
+ return
232
280
  }
281
+
282
+ const errorToSend = Cause.isFailType(cause) ? cause.error : UnexpectedError.make({ cause })
283
+ yield* shutdownChannel.send(errorToSend).pipe(Effect.orDie)
284
+
285
+ return yield* Effect.die(cause)
233
286
  })
234
287
 
235
288
  yield* backgroundApplyLocalPushes({
@@ -246,20 +299,19 @@ export const makeLeaderSyncProcessor = ({
246
299
  testing: {
247
300
  delay: testing?.delays?.localPushProcessing,
248
301
  },
249
- }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
302
+ }).pipe(Effect.catchAllCause(maybeShutdownOnError), Effect.forkScoped)
250
303
 
251
- const backendPushingFiberHandle = yield* FiberHandle.make()
304
+ const backendPushingFiberHandle = yield* FiberHandle.make<void, never>()
252
305
  const backendPushingEffect = backgroundBackendPushing({
253
306
  syncBackendPushQueue,
254
307
  otelSpan,
255
308
  devtoolsLatch: ctxRef.current?.devtoolsLatch,
256
309
  backendPushBatchSize,
257
- }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError))
310
+ }).pipe(Effect.catchAllCause(maybeShutdownOnError))
258
311
 
259
312
  yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
260
313
 
261
314
  yield* backgroundBackendPulling({
262
- initialBackendHead: initialSyncState.upstreamHead.global,
263
315
  isClientEvent,
264
316
  restartBackendPushing: (filteredRebasedPending) =>
265
317
  Effect.gen(function* () {
@@ -276,13 +328,24 @@ export const makeLeaderSyncProcessor = ({
276
328
  syncStateSref,
277
329
  localPushesLatch,
278
330
  pullLatch,
331
+ livePull,
279
332
  dbState,
280
333
  otelSpan,
281
334
  initialBlockingSyncContext,
282
335
  devtoolsLatch: ctxRef.current?.devtoolsLatch,
283
336
  connectedClientSessionPullQueues,
284
337
  advancePushHead,
285
- }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
338
+ }).pipe(
339
+ Effect.retry({
340
+ // We want to retry pulling if we've lost connection to the sync backend
341
+ while: (cause) => cause._tag === 'IsOfflineError',
342
+ }),
343
+ Effect.catchAllCause(maybeShutdownOnError),
344
+ // Needed to avoid `Fiber terminated with an unhandled error` logs which seem to happen because of the `Effect.retry` above.
345
+ // This might be a bug in Effect. Only seems to happen in the browser.
346
+ Effect.provide(Layer.setUnhandledErrorLogLevel(Option.none())),
347
+ Effect.forkScoped,
348
+ )
286
349
 
287
350
  return { initialLeaderHead: initialSyncState.localHead }
288
351
  }).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'))
@@ -379,19 +442,44 @@ const backgroundApplyLocalPushes = ({
379
442
 
380
443
  // Since the rebase generation might have changed since enqueuing, we need to filter out items with older generation
381
444
  // It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
382
- const [newEvents, deferreds] = pipe(
445
+ const [droppedItems, filteredItems] = ReadonlyArray.partition(
383
446
  batchItems,
384
- ReadonlyArray.filter(([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration === currentRebaseGeneration),
385
- ReadonlyArray.unzip,
447
+ ([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration >= currentRebaseGeneration,
386
448
  )
387
449
 
388
- if (newEvents.length === 0) {
389
- // console.log('dropping old-gen batch', currentLocalPushGenerationRef.current)
390
- // Allow the backend pulling to start
450
+ if (droppedItems.length > 0) {
451
+ otelSpan?.addEvent(`push:drop-old-generation`, {
452
+ droppedCount: droppedItems.length,
453
+ currentRebaseGeneration,
454
+ })
455
+
456
+ /**
457
+ * Dropped pushes may still have a deferred awaiting completion.
458
+ * Fail it so the caller learns the leader advanced and resubmits with the updated generation.
459
+ */
460
+ yield* Effect.forEach(
461
+ droppedItems.filter(
462
+ (item): item is [LiveStoreEvent.EncodedWithMeta, Deferred.Deferred<void, LeaderAheadError>] =>
463
+ item[1] !== undefined,
464
+ ),
465
+ ([eventEncoded, deferred]) =>
466
+ Deferred.fail(
467
+ deferred,
468
+ LeaderAheadError.make({
469
+ minimumExpectedNum: syncState.localHead,
470
+ providedNum: eventEncoded.seqNum,
471
+ }),
472
+ ),
473
+ )
474
+ }
475
+
476
+ if (filteredItems.length === 0) {
391
477
  yield* pullLatch.open
392
478
  continue
393
479
  }
394
480
 
481
+ const [newEvents, deferreds] = ReadonlyArray.unzip(filteredItems)
482
+
395
483
  const mergeResult = SyncState.merge({
396
484
  syncState,
397
485
  payload: { _tag: 'local-push', newEvents },
@@ -405,7 +493,7 @@ const backgroundApplyLocalPushes = ({
405
493
  batchSize: newEvents.length,
406
494
  newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
407
495
  })
408
- return yield* new SyncError({ cause: mergeResult.message })
496
+ return yield* new UnexpectedError({ cause: mergeResult.message })
409
497
  }
410
498
  case 'rebase': {
411
499
  return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
@@ -476,8 +564,8 @@ const backgroundApplyLocalPushes = ({
476
564
 
477
565
  // Don't sync clientOnly events
478
566
  const filteredBatch = mergeResult.newEvents.filter((eventEncoded) => {
479
- const { eventDef } = getEventDef(schema, eventEncoded.name)
480
- return eventDef.options.clientOnly === false
567
+ const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
568
+ return eventDef === undefined ? true : eventDef.options.clientOnly === false
481
569
  })
482
570
 
483
571
  yield* BucketQueue.offerAll(syncBackendPushQueue, filteredBatch)
@@ -496,7 +584,7 @@ type MaterializeEventsBatch = (_: {
496
584
  * Indexes are aligned with `batchItems`
497
585
  */
498
586
  deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError> | undefined> | undefined
499
- }) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
587
+ }) => Effect.Effect<void, MaterializeError, LeaderThreadCtx>
500
588
 
501
589
  // TODO how to handle errors gracefully
502
590
  const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds }) =>
@@ -536,24 +624,22 @@ const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds
536
624
  attributes: { batchSize: batchItems.length },
537
625
  }),
538
626
  Effect.tapCauseLogPretty,
539
- UnexpectedError.mapToUnexpectedError,
540
627
  )
541
628
 
542
629
  const backgroundBackendPulling = ({
543
- initialBackendHead,
544
630
  isClientEvent,
545
631
  restartBackendPushing,
546
632
  otelSpan,
547
633
  dbState,
548
634
  syncStateSref,
549
635
  localPushesLatch,
636
+ livePull,
550
637
  pullLatch,
551
638
  devtoolsLatch,
552
639
  initialBlockingSyncContext,
553
640
  connectedClientSessionPullQueues,
554
641
  advancePushHead,
555
642
  }: {
556
- initialBackendHead: EventSequenceNumber.GlobalEventSequenceNumber
557
643
  isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
558
644
  restartBackendPushing: (
559
645
  filteredRebasedPending: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
@@ -563,6 +649,7 @@ const backgroundBackendPulling = ({
563
649
  dbState: SqliteDb
564
650
  localPushesLatch: Effect.Latch
565
651
  pullLatch: Effect.Latch
652
+ livePull: boolean
566
653
  devtoolsLatch: Effect.Latch | undefined
567
654
  initialBlockingSyncContext: InitialBlockingSyncContext
568
655
  connectedClientSessionPullQueues: PullQueueSet
@@ -573,7 +660,7 @@ const backgroundBackendPulling = ({
573
660
 
574
661
  if (syncBackend === undefined) return
575
662
 
576
- const onNewPullChunk = (newEvents: LiveStoreEvent.EncodedWithMeta[], remaining: number) =>
663
+ const onNewPullChunk = (newEvents: LiveStoreEvent.EncodedWithMeta[], pageInfo: SyncBackend.PullResPageInfo) =>
577
664
  Effect.gen(function* () {
578
665
  if (newEvents.length === 0) return
579
666
 
@@ -605,7 +692,7 @@ const backgroundBackendPulling = ({
605
692
  newEventsCount: newEvents.length,
606
693
  newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
607
694
  })
608
- return yield* new SyncError({ cause: mergeResult.message })
695
+ return yield* new UnexpectedError({ cause: mergeResult.message })
609
696
  }
610
697
 
611
698
  const newBackendHead = newEvents.at(-1)!.seqNum
@@ -621,8 +708,8 @@ const backgroundBackendPulling = ({
621
708
  })
622
709
 
623
710
  const globalRebasedPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
624
- const { eventDef } = getEventDef(schema, event.name)
625
- return eventDef.options.clientOnly === false
711
+ const eventDef = schema.eventsDefsMap.get(event.name)
712
+ return eventDef === undefined ? true : eventDef.options.clientOnly === false
626
713
  })
627
714
  yield* restartBackendPushing(globalRebasedPendingEvents)
628
715
 
@@ -644,6 +731,13 @@ const backgroundBackendPulling = ({
644
731
  mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
645
732
  })
646
733
 
734
+ // Ensure push fiber is active after advance by restarting with current pending (non-client) events
735
+ const globalPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
736
+ const eventDef = schema.eventsDefsMap.get(event.name)
737
+ return eventDef === undefined ? true : eventDef.options.clientOnly === false
738
+ })
739
+ yield* restartBackendPushing(globalPendingEvents)
740
+
647
741
  yield* connectedClientSessionPullQueues.offer({
648
742
  payload: SyncState.payloadFromMergeResult(mergeResult),
649
743
  leaderHead: mergeResult.newSyncState.localHead,
@@ -657,7 +751,7 @@ const backgroundBackendPulling = ({
657
751
  EventSequenceNumber.isEqual(event.seqNum, confirmedEvent.seqNum),
658
752
  ),
659
753
  )
660
- yield* Eventlog.updateSyncMetadata(confirmedNewEvents)
754
+ yield* Eventlog.updateSyncMetadata(confirmedNewEvents).pipe(UnexpectedError.mapToUnexpectedError)
661
755
  }
662
756
  }
663
757
 
@@ -671,18 +765,20 @@ const backgroundBackendPulling = ({
671
765
  yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
672
766
 
673
767
  // Allow local pushes to be processed again
674
- if (remaining === 0) {
768
+ if (pageInfo._tag === 'NoMore') {
675
769
  yield* localPushesLatch.open
676
770
  }
677
771
  })
678
772
 
679
- const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: initialBackendHead })
773
+ const syncState = yield* syncStateSref
774
+ if (syncState === undefined) return shouldNeverHappen('Not initialized')
775
+ const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: syncState.upstreamHead.global })
680
776
 
681
777
  const hashMaterializerResult = makeMaterializerHash({ schema, dbState })
682
778
 
683
- yield* syncBackend.pull(cursorInfo).pipe(
779
+ yield* syncBackend.pull(cursorInfo, { live: livePull }).pipe(
684
780
  // TODO only take from queue while connected
685
- Stream.tap(({ batch, remaining }) =>
781
+ Stream.tap(({ batch, pageInfo }) =>
686
782
  Effect.gen(function* () {
687
783
  // yield* Effect.spanEvent('batch', {
688
784
  // attributes: {
@@ -690,12 +786,10 @@ const backgroundBackendPulling = ({
690
786
  // batch: TRACE_VERBOSE ? batch : undefined,
691
787
  // },
692
788
  // })
693
-
694
789
  // NOTE we only want to take process events when the sync backend is connected
695
790
  // (e.g. needed for simulating being offline)
696
791
  // TODO remove when there's a better way to handle this in stream above
697
792
  yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
698
-
699
793
  yield* onNewPullChunk(
700
794
  batch.map((_) =>
701
795
  LiveStoreEvent.EncodedWithMeta.fromGlobal(_.eventEncoded, {
@@ -706,15 +800,17 @@ const backgroundBackendPulling = ({
706
800
  materializerHashSession: Option.none(),
707
801
  }),
708
802
  ),
709
- remaining,
803
+ pageInfo,
710
804
  )
711
-
712
- yield* initialBlockingSyncContext.update({ processed: batch.length, remaining })
805
+ yield* initialBlockingSyncContext.update({ processed: batch.length, pageInfo })
713
806
  }),
714
807
  ),
715
808
  Stream.runDrain,
716
809
  Effect.interruptible,
717
810
  )
811
+
812
+ // Should only ever happen when livePull is false
813
+ yield* Effect.logDebug('backend-pulling finished', { livePull })
718
814
  }).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pulling'))
719
815
 
720
816
  const backgroundBackendPushing = ({
@@ -748,17 +844,53 @@ const backgroundBackendPushing = ({
748
844
  batch: TRACE_VERBOSE ? JSON.stringify(queueItems) : undefined,
749
845
  })
750
846
 
751
- // TODO handle push errors (should only happen during concurrent pull+push)
752
- const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
847
+ // Push with declarative retry/backoff using Effect schedules
848
+ // - Exponential backoff starting at 1s and doubling (1s, 2s, 4s, 8s, 16s, 30s ...)
849
+ // - Delay clamped at 30s (continues retrying at 30s)
850
+ // - Resets automatically after successful push
851
+ // TODO(metrics): expose counters/gauges for retry attempts and queue health via devtools/metrics
852
+
853
+ // Only retry for transient UnexpectedError cases
854
+ const isRetryable = (err: InvalidPushError | IsOfflineError) =>
855
+ err._tag === 'InvalidPushError' && err.cause._tag === 'LiveStore.UnexpectedError'
856
+
857
+ // Input: InvalidPushError | IsOfflineError, Output: Duration
858
+ const retrySchedule: Schedule.Schedule<Duration.DurationInput, InvalidPushError | IsOfflineError> =
859
+ Schedule.exponential(Duration.seconds(1)).pipe(
860
+ Schedule.andThenEither(Schedule.spaced(Duration.seconds(30))), // clamp at 30 second intervals
861
+ Schedule.compose(Schedule.elapsed),
862
+ Schedule.whileInput(isRetryable),
863
+ )
864
+
865
+ yield* Effect.gen(function* () {
866
+ const iteration = yield* Schedule.CurrentIterationMetadata
753
867
 
754
- if (pushResult._tag === 'Left') {
755
- if (LS_DEV) {
756
- yield* Effect.logDebug('handled backend-push-error', { error: pushResult.left.toString() })
868
+ const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
869
+
870
+ const retries = iteration.recurrence
871
+ if (retries > 0 && pushResult._tag === 'Right') {
872
+ otelSpan?.addEvent('backend-push-retry-success', { retries, batchSize: queueItems.length })
757
873
  }
758
- otelSpan?.addEvent('backend-push-error', { error: pushResult.left.toString() })
759
- // wait for interrupt caused by background pulling which will then restart pushing
760
- return yield* Effect.never
761
- }
874
+
875
+ if (pushResult._tag === 'Left') {
876
+ otelSpan?.addEvent('backend-push-error', {
877
+ error: pushResult.left.toString(),
878
+ retries,
879
+ batchSize: queueItems.length,
880
+ })
881
+ const error = pushResult.left
882
+ if (
883
+ error._tag === 'IsOfflineError' ||
884
+ (error._tag === 'InvalidPushError' && error.cause._tag === 'ServerAheadError')
885
+ ) {
886
+ // It's a core part of the sync protocol that the sync backend will emit a new pull chunk alongside the ServerAheadError
887
+ yield* Effect.logDebug('handled backend-push-error (waiting for interupt caused by pull)', { error })
888
+ return yield* Effect.never
889
+ }
890
+
891
+ return yield* error
892
+ }
893
+ }).pipe(Effect.retry(retrySchedule))
762
894
  }
763
895
  }).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pushing'))
764
896
 
@@ -890,6 +1022,11 @@ const makePullQueueSet = Effect.gen(function* () {
890
1022
  }
891
1023
  })
892
1024
 
1025
+ /**
1026
+ * Validate a client-provided batch before it is admitted to the leader queue.
1027
+ * Ensures the numbers form a strictly increasing chain and that the first
1028
+ * event sits ahead of the current push head.
1029
+ */
893
1030
  const validatePushBatch = (
894
1031
  batch: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
895
1032
  pushHead: EventSequenceNumber.EventSequenceNumber,
@@ -899,12 +1036,16 @@ const validatePushBatch = (
899
1036
  return
900
1037
  }
901
1038
 
902
- // Make sure batch is monotonically increasing
1039
+ // Example: session A already enqueued e1…e6 while session B (same client, different
1040
+ // session) still believes the head is e1 and submits [e2, e7, e8]. The numbers look
1041
+ // monotonic from B’s perspective, but we must reject and force B to rebase locally
1042
+ // so the leader never regresses.
903
1043
  for (let i = 1; i < batch.length; i++) {
904
1044
  if (EventSequenceNumber.isGreaterThanOrEqual(batch[i - 1]!.seqNum, batch[i]!.seqNum)) {
905
- shouldNeverHappen(
906
- `Events must be ordered in monotonically ascending order by eventNum. Received: [${batch.map((e) => EventSequenceNumber.toString(e.seqNum)).join(', ')}]`,
907
- )
1045
+ return yield* LeaderAheadError.make({
1046
+ minimumExpectedNum: batch[i - 1]!.seqNum,
1047
+ providedNum: batch[i]!.seqNum,
1048
+ })
908
1049
  }
909
1050
  }
910
1051
 
@@ -123,6 +123,14 @@ export const getBackendHeadFromDb = (dbEventlog: SqliteDb): EventSequenceNumber.
123
123
  export const updateBackendHead = (dbEventlog: SqliteDb, head: EventSequenceNumber.EventSequenceNumber) =>
124
124
  dbEventlog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET head = ${head.global}`)
125
125
 
126
+ export const getBackendIdFromDb = (dbEventlog: SqliteDb): Option.Option<string> =>
127
+ Option.fromNullable(
128
+ dbEventlog.select<{ backendId: string | null }>(sql`select backendId from ${SYNC_STATUS_TABLE}`)[0]?.backendId,
129
+ )
130
+
131
+ export const updateBackendId = (dbEventlog: SqliteDb, backendId: string) =>
132
+ dbEventlog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET backendId = '${backendId}'`)
133
+
126
134
  export const insertIntoEventlog = (
127
135
  eventEncoded: LiveStoreEvent.EncodedWithMeta,
128
136
  dbEventlog: SqliteDb,
@@ -141,7 +149,7 @@ export const insertIntoEventlog = (
141
149
 
142
150
  if (parentEventExists === false) {
143
151
  shouldNeverHappen(
144
- `Parent mutation ${eventEncoded.parentSeqNum.global},${eventEncoded.parentSeqNum.client} does not exist`,
152
+ `Parent event ${eventEncoded.parentSeqNum.global},${eventEncoded.parentSeqNum.client} does not exist in eventlog`,
145
153
  )
146
154
  }
147
155
  }
@@ -213,11 +221,7 @@ export const getSyncBackendCursorInfo = ({
213
221
  ).pipe(Effect.andThen(Schema.decode(EventlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
214
222
 
215
223
  return Option.some({
216
- cursor: {
217
- global: remoteHead,
218
- client: EventSequenceNumber.clientDefault,
219
- rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
220
- },
224
+ eventSequenceNumber: remoteHead,
221
225
  metadata: syncMetadataOption,
222
226
  }) satisfies InitialSyncInfo
223
227
  }).pipe(Effect.withSpan('@livestore/common:eventlog:getSyncBackendCursorInfo', { attributes: { remoteHead } }))
@@ -262,7 +262,7 @@ const listenToDevtools = ({
262
262
 
263
263
  if (syncBackend !== undefined) {
264
264
  // TODO consider piggybacking on the existing leader-thread sync-pulling
265
- yield* syncBackend.pull(Option.none()).pipe(
265
+ yield* syncBackend.pull(Option.none(), { live: true }).pipe(
266
266
  Stream.map((_) => _.batch),
267
267
  Stream.flattenIterables,
268
268
  Stream.tap(({ eventEncoded, metadata }) =>
@@ -319,7 +319,11 @@ const listenToDevtools = ({
319
319
  Stream.tap(([isConnected, { latchClosed }]) =>
320
320
  sendMessage(
321
321
  Devtools.Leader.NetworkStatusRes.make({
322
- networkStatus: { isConnected, timestampMs: Date.now(), latchClosed },
322
+ networkStatus: {
323
+ isConnected,
324
+ timestampMs: Date.now(),
325
+ devtools: { latchClosed },
326
+ },
323
327
  subscriptionId,
324
328
  ...reqPayload,
325
329
  requestId: nanoid(10),