@livestore/common 0.3.1 → 0.3.2-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts +35 -0
  3. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -0
  4. package/dist/ClientSessionLeaderThreadProxy.js +6 -0
  5. package/dist/ClientSessionLeaderThreadProxy.js.map +1 -0
  6. package/dist/adapter-types.d.ts +10 -161
  7. package/dist/adapter-types.d.ts.map +1 -1
  8. package/dist/adapter-types.js +5 -49
  9. package/dist/adapter-types.js.map +1 -1
  10. package/dist/defs.d.ts +20 -0
  11. package/dist/defs.d.ts.map +1 -0
  12. package/dist/defs.js +12 -0
  13. package/dist/defs.js.map +1 -0
  14. package/dist/devtools/devtools-messages-client-session.d.ts +23 -21
  15. package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
  16. package/dist/devtools/devtools-messages-common.d.ts +6 -6
  17. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  18. package/dist/devtools/devtools-messages-leader.d.ts +26 -24
  19. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  20. package/dist/errors.d.ts +50 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.js +36 -0
  23. package/dist/errors.js.map +1 -0
  24. package/dist/leader-thread/LeaderSyncProcessor.d.ts +6 -7
  25. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  26. package/dist/leader-thread/LeaderSyncProcessor.js +112 -122
  27. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  28. package/dist/leader-thread/eventlog.d.ts +17 -6
  29. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  30. package/dist/leader-thread/eventlog.js +32 -17
  31. package/dist/leader-thread/eventlog.js.map +1 -1
  32. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  33. package/dist/leader-thread/leader-worker-devtools.js +1 -2
  34. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  35. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  36. package/dist/leader-thread/make-leader-thread-layer.js +37 -7
  37. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  38. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  39. package/dist/leader-thread/materialize-event.js +7 -1
  40. package/dist/leader-thread/materialize-event.js.map +1 -1
  41. package/dist/leader-thread/mod.d.ts +1 -0
  42. package/dist/leader-thread/mod.d.ts.map +1 -1
  43. package/dist/leader-thread/mod.js +1 -0
  44. package/dist/leader-thread/mod.js.map +1 -1
  45. package/dist/leader-thread/recreate-db.d.ts +13 -6
  46. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  47. package/dist/leader-thread/recreate-db.js +1 -3
  48. package/dist/leader-thread/recreate-db.js.map +1 -1
  49. package/dist/leader-thread/types.d.ts +5 -7
  50. package/dist/leader-thread/types.d.ts.map +1 -1
  51. package/dist/make-client-session.d.ts +1 -1
  52. package/dist/make-client-session.d.ts.map +1 -1
  53. package/dist/make-client-session.js +1 -1
  54. package/dist/make-client-session.js.map +1 -1
  55. package/dist/rematerialize-from-eventlog.d.ts +1 -1
  56. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  57. package/dist/rematerialize-from-eventlog.js +10 -2
  58. package/dist/rematerialize-from-eventlog.js.map +1 -1
  59. package/dist/schema/EventDef.d.ts +2 -2
  60. package/dist/schema/EventDef.d.ts.map +1 -1
  61. package/dist/schema/EventDef.js +2 -2
  62. package/dist/schema/EventDef.js.map +1 -1
  63. package/dist/schema/EventSequenceNumber.d.ts +20 -2
  64. package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
  65. package/dist/schema/EventSequenceNumber.js +71 -19
  66. package/dist/schema/EventSequenceNumber.js.map +1 -1
  67. package/dist/schema/EventSequenceNumber.test.js +88 -3
  68. package/dist/schema/EventSequenceNumber.test.js.map +1 -1
  69. package/dist/schema/LiveStoreEvent.d.ts +25 -11
  70. package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
  71. package/dist/schema/LiveStoreEvent.js +12 -4
  72. package/dist/schema/LiveStoreEvent.js.map +1 -1
  73. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -2
  74. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
  75. package/dist/schema/state/sqlite/db-schema/hash.js +3 -1
  76. package/dist/schema/state/sqlite/db-schema/hash.js.map +1 -1
  77. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  78. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  79. package/dist/schema/state/sqlite/query-builder/api.d.ts +35 -8
  80. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  81. package/dist/schema/state/sqlite/query-builder/api.js.map +1 -1
  82. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  83. package/dist/schema/state/sqlite/query-builder/impl.js +16 -11
  84. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  85. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +1 -81
  86. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -1
  87. package/dist/schema/state/sqlite/query-builder/impl.test.js +34 -20
  88. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  89. package/dist/schema/state/sqlite/system-tables.d.ts +67 -62
  90. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  91. package/dist/schema/state/sqlite/system-tables.js +8 -17
  92. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  93. package/dist/schema/state/sqlite/table-def.d.ts +1 -1
  94. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  95. package/dist/schema-management/migrations.d.ts +3 -1
  96. package/dist/schema-management/migrations.d.ts.map +1 -1
  97. package/dist/schema-management/migrations.js.map +1 -1
  98. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  99. package/dist/sql-queries/sql-queries.js +2 -0
  100. package/dist/sql-queries/sql-queries.js.map +1 -1
  101. package/dist/sqlite-types.d.ts +72 -0
  102. package/dist/sqlite-types.d.ts.map +1 -0
  103. package/dist/sqlite-types.js +5 -0
  104. package/dist/sqlite-types.js.map +1 -0
  105. package/dist/sync/ClientSessionSyncProcessor.d.ts +6 -2
  106. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  107. package/dist/sync/ClientSessionSyncProcessor.js +16 -13
  108. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  109. package/dist/sync/next/graphology.d.ts.map +1 -1
  110. package/dist/sync/next/graphology.js +0 -6
  111. package/dist/sync/next/graphology.js.map +1 -1
  112. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  113. package/dist/sync/next/rebase-events.js +1 -0
  114. package/dist/sync/next/rebase-events.js.map +1 -1
  115. package/dist/sync/next/test/compact-events.test.js +1 -1
  116. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  117. package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
  118. package/dist/sync/next/test/event-fixtures.js +12 -3
  119. package/dist/sync/next/test/event-fixtures.js.map +1 -1
  120. package/dist/sync/sync.d.ts +2 -0
  121. package/dist/sync/sync.d.ts.map +1 -1
  122. package/dist/sync/sync.js +3 -0
  123. package/dist/sync/sync.js.map +1 -1
  124. package/dist/sync/syncstate.d.ts +13 -4
  125. package/dist/sync/syncstate.d.ts.map +1 -1
  126. package/dist/sync/syncstate.js +23 -10
  127. package/dist/sync/syncstate.js.map +1 -1
  128. package/dist/sync/syncstate.test.js +17 -17
  129. package/dist/sync/syncstate.test.js.map +1 -1
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.d.ts.map +1 -1
  132. package/dist/version.js +1 -1
  133. package/dist/version.js.map +1 -1
  134. package/package.json +7 -6
  135. package/src/ClientSessionLeaderThreadProxy.ts +40 -0
  136. package/src/adapter-types.ts +19 -166
  137. package/src/defs.ts +17 -0
  138. package/src/errors.ts +49 -0
  139. package/src/leader-thread/LeaderSyncProcessor.ts +141 -180
  140. package/src/leader-thread/eventlog.ts +78 -56
  141. package/src/leader-thread/leader-worker-devtools.ts +1 -2
  142. package/src/leader-thread/make-leader-thread-layer.ts +52 -8
  143. package/src/leader-thread/materialize-event.ts +8 -1
  144. package/src/leader-thread/mod.ts +1 -0
  145. package/src/leader-thread/recreate-db.ts +99 -91
  146. package/src/leader-thread/types.ts +6 -11
  147. package/src/make-client-session.ts +2 -2
  148. package/src/rematerialize-from-eventlog.ts +10 -2
  149. package/src/schema/EventDef.ts +5 -3
  150. package/src/schema/EventSequenceNumber.test.ts +120 -3
  151. package/src/schema/EventSequenceNumber.ts +95 -23
  152. package/src/schema/LiveStoreEvent.ts +20 -4
  153. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +2 -2
  154. package/src/schema/state/sqlite/db-schema/hash.ts +3 -3
  155. package/src/schema/state/sqlite/mod.ts +1 -1
  156. package/src/schema/state/sqlite/query-builder/api.ts +38 -8
  157. package/src/schema/state/sqlite/query-builder/impl.test.ts +60 -20
  158. package/src/schema/state/sqlite/query-builder/impl.ts +15 -12
  159. package/src/schema/state/sqlite/system-tables.ts +9 -22
  160. package/src/schema/state/sqlite/table-def.ts +1 -1
  161. package/src/schema-management/migrations.ts +3 -1
  162. package/src/sql-queries/sql-queries.ts +2 -0
  163. package/src/sqlite-types.ts +76 -0
  164. package/src/sync/ClientSessionSyncProcessor.ts +17 -20
  165. package/src/sync/next/graphology.ts +0 -6
  166. package/src/sync/next/rebase-events.ts +1 -0
  167. package/src/sync/next/test/compact-events.test.ts +1 -1
  168. package/src/sync/next/test/event-fixtures.ts +12 -3
  169. package/src/sync/sync.ts +3 -0
  170. package/src/sync/syncstate.test.ts +17 -17
  171. package/src/sync/syncstate.ts +31 -10
  172. package/src/version.ts +1 -1
@@ -8,6 +8,7 @@ import {
8
8
  FiberHandle,
9
9
  Option,
10
10
  OtelTracer,
11
+ pipe,
11
12
  Queue,
12
13
  ReadonlyArray,
13
14
  Stream,
@@ -17,7 +18,7 @@ import {
17
18
  import type * as otel from '@opentelemetry/api'
18
19
 
19
20
  import type { SqliteDb } from '../adapter-types.js'
20
- import { UnexpectedError } from '../adapter-types.js'
21
+ import { SyncError, UnexpectedError } from '../adapter-types.js'
21
22
  import { makeMaterializerHash } from '../materializer-helper.js'
22
23
  import type { LiveStoreSchema } from '../schema/mod.js'
23
24
  import { EventSequenceNumber, getEventDef, LiveStoreEvent, SystemTables } from '../schema/mod.js'
@@ -32,8 +33,6 @@ import { LeaderThreadCtx } from './types.js'
32
33
  type LocalPushQueueItem = [
33
34
  event: LiveStoreEvent.EncodedWithMeta,
34
35
  deferred: Deferred.Deferred<void, LeaderAheadError> | undefined,
35
- /** Used to determine whether the batch has become invalid due to a rejected local push batch */
36
- generation: number,
37
36
  ]
38
37
 
39
38
  /**
@@ -58,32 +57,28 @@ type LocalPushQueueItem = [
58
57
  * - The latch closes on pull receipt and re-opens post-pull completion.
59
58
  * - Processes up to `maxBatchSize` events per cycle.
60
59
  *
61
- * Currently we're advancing the db read model and eventlog in lockstep, but we could also decouple this in the future
60
+ * Currently we're advancing the state db and eventlog in lockstep, but we could also decouple this in the future
62
61
  *
63
62
  * Tricky concurrency scenarios:
64
63
  * - Queued local push batches becoming invalid due to a prior local push item being rejected.
65
64
  * Solution: Introduce a generation number for local push batches which is used to filter out old batches items in case of rejection.
66
65
  *
66
+ * See ClientSessionSyncProcessor for how the leader and session sync processors are similar/different.
67
67
  */
68
68
  export const makeLeaderSyncProcessor = ({
69
69
  schema,
70
- dbEventlogMissing,
71
- dbEventlog,
72
70
  dbState,
73
- dbStateMissing,
74
71
  initialBlockingSyncContext,
72
+ initialSyncState,
75
73
  onError,
76
74
  params,
77
75
  testing,
78
76
  }: {
79
77
  schema: LiveStoreSchema
80
- /** Only used to know whether we can safely query dbEventlog during setup execution */
81
- dbEventlogMissing: boolean
82
- dbEventlog: SqliteDb
83
78
  dbState: SqliteDb
84
- /** Only used to know whether we can safely query dbState during setup execution */
85
- dbStateMissing: boolean
86
79
  initialBlockingSyncContext: InitialBlockingSyncContext
80
+ /** Initial sync state rehydrated from the persisted eventlog or initial sync state */
81
+ initialSyncState: SyncState.SyncState
87
82
  onError: 'shutdown' | 'ignore'
88
83
  params: {
89
84
  /**
@@ -115,18 +110,6 @@ export const makeLeaderSyncProcessor = ({
115
110
 
116
111
  const connectedClientSessionPullQueues = yield* makePullQueueSet
117
112
 
118
- /**
119
- * Tracks generations of queued local push events.
120
- * If a local-push batch is rejected, all subsequent push queue items with the same generation are also rejected,
121
- * even if they would be valid on their own.
122
- */
123
- // TODO get rid of this in favour of the `mergeGeneration` event sequence number field
124
- const currentLocalPushGenerationRef = { current: 0 }
125
-
126
- type MergeCounter = number
127
- const mergeCounterRef = { current: dbStateMissing ? 0 : yield* getMergeCounterFromDb(dbState) }
128
- const mergePayloads = new Map<MergeCounter, typeof SyncState.PayloadUpstream.Type>()
129
-
130
113
  // This context depends on data from `boot`, we should find a better implementation to avoid this ref indirection.
131
114
  const ctxRef = {
132
115
  current: undefined as
@@ -150,7 +133,7 @@ export const makeLeaderSyncProcessor = ({
150
133
  * - leader sync processor takes a bit and hasn't yet taken e1 from the localPushesQueue
151
134
  * - client session B also pushes e1 (which should be rejected)
152
135
  *
153
- * Thus the purpoe of the pushHeadRef is the guard the integrity of the local push queue
136
+ * Thus the purpose of the pushHeadRef is the guard the integrity of the local push queue
154
137
  */
155
138
  const pushHeadRef = { current: EventSequenceNumber.ROOT }
156
139
  const advancePushHead = (eventNum: EventSequenceNumber.EventSequenceNumber) => {
@@ -162,25 +145,24 @@ export const makeLeaderSyncProcessor = ({
162
145
  Effect.gen(function* () {
163
146
  if (newEvents.length === 0) return
164
147
 
148
+ // console.debug('push', newEvents)
149
+
165
150
  yield* validatePushBatch(newEvents, pushHeadRef.current)
166
151
 
167
152
  advancePushHead(newEvents.at(-1)!.seqNum)
168
153
 
169
154
  const waitForProcessing = options?.waitForProcessing ?? false
170
- const generation = currentLocalPushGenerationRef.current
171
155
 
172
156
  if (waitForProcessing) {
173
157
  const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void, LeaderAheadError>())
174
158
 
175
- const items = newEvents.map(
176
- (eventEncoded, i) => [eventEncoded, deferreds[i], generation] as LocalPushQueueItem,
177
- )
159
+ const items = newEvents.map((eventEncoded, i) => [eventEncoded, deferreds[i]] as LocalPushQueueItem)
178
160
 
179
161
  yield* BucketQueue.offerAll(localPushesQueue, items)
180
162
 
181
163
  yield* Effect.all(deferreds)
182
164
  } else {
183
- const items = newEvents.map((eventEncoded) => [eventEncoded, undefined, generation] as LocalPushQueueItem)
165
+ const items = newEvents.map((eventEncoded) => [eventEncoded, undefined] as LocalPushQueueItem)
184
166
  yield* BucketQueue.offerAll(localPushesQueue, items)
185
167
  }
186
168
  }).pipe(
@@ -205,7 +187,7 @@ export const makeLeaderSyncProcessor = ({
205
187
  args,
206
188
  clientId,
207
189
  sessionId,
208
- ...EventSequenceNumber.nextPair(syncState.localHead, eventDef.options.clientOnly),
190
+ ...EventSequenceNumber.nextPair({ seqNum: syncState.localHead, isClient: eventDef.options.clientOnly }),
209
191
  })
210
192
 
211
193
  yield* push([eventEncoded])
@@ -225,34 +207,12 @@ export const makeLeaderSyncProcessor = ({
225
207
  runtime,
226
208
  }
227
209
 
228
- const initialLocalHead = dbEventlogMissing ? EventSequenceNumber.ROOT : Eventlog.getClientHeadFromDb(dbEventlog)
229
-
230
- const initialBackendHead = dbEventlogMissing
231
- ? EventSequenceNumber.ROOT.global
232
- : Eventlog.getBackendHeadFromDb(dbEventlog)
233
-
234
- if (initialBackendHead > initialLocalHead.global) {
235
- return shouldNeverHappen(
236
- `During boot the backend head (${initialBackendHead}) should never be greater than the local head (${initialLocalHead.global})`,
237
- )
238
- }
239
-
240
- const pendingEvents = dbEventlogMissing
241
- ? []
242
- : yield* Eventlog.getEventsSince({ global: initialBackendHead, client: EventSequenceNumber.clientDefault })
243
-
244
- const initialSyncState = new SyncState.SyncState({
245
- pending: pendingEvents,
246
- upstreamHead: { global: initialBackendHead, client: EventSequenceNumber.clientDefault },
247
- localHead: initialLocalHead,
248
- })
249
-
250
210
  /** State transitions need to happen atomically, so we use a Ref to track the state */
251
211
  yield* SubscriptionRef.set(syncStateSref, initialSyncState)
252
212
 
253
213
  // Rehydrate sync queue
254
- if (pendingEvents.length > 0) {
255
- const globalPendingEvents = pendingEvents
214
+ if (initialSyncState.pending.length > 0) {
215
+ const globalPendingEvents = initialSyncState.pending
256
216
  // Don't sync clientOnly events
257
217
  .filter((eventEncoded) => {
258
218
  const { eventDef } = getEventDef(schema, eventEncoded.name)
@@ -281,10 +241,7 @@ export const makeLeaderSyncProcessor = ({
281
241
  schema,
282
242
  isClientEvent,
283
243
  otelSpan,
284
- currentLocalPushGenerationRef,
285
244
  connectedClientSessionPullQueues,
286
- mergeCounterRef,
287
- mergePayloads,
288
245
  localPushBatchSize,
289
246
  testing: {
290
247
  delay: testing?.delays?.localPushProcessing,
@@ -302,7 +259,7 @@ export const makeLeaderSyncProcessor = ({
302
259
  yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
303
260
 
304
261
  yield* backgroundBackendPulling({
305
- initialBackendHead,
262
+ initialBackendHead: initialSyncState.upstreamHead.global,
306
263
  isClientEvent,
307
264
  restartBackendPushing: (filteredRebasedPending) =>
308
265
  Effect.gen(function* () {
@@ -324,12 +281,10 @@ export const makeLeaderSyncProcessor = ({
324
281
  initialBlockingSyncContext,
325
282
  devtoolsLatch: ctxRef.current?.devtoolsLatch,
326
283
  connectedClientSessionPullQueues,
327
- mergeCounterRef,
328
- mergePayloads,
329
284
  advancePushHead,
330
285
  }).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
331
286
 
332
- return { initialLeaderHead: initialLocalHead }
287
+ return { initialLeaderHead: initialSyncState.localHead }
333
288
  }).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'))
334
289
 
335
290
  const pull: LeaderSyncProcessor['pull'] = ({ cursor }) =>
@@ -338,34 +293,23 @@ export const makeLeaderSyncProcessor = ({
338
293
  return Stream.fromQueue(queue)
339
294
  }).pipe(Stream.unwrapScoped)
340
295
 
341
- const pullQueue: LeaderSyncProcessor['pullQueue'] = ({ cursor }) => {
342
- const runtime = ctxRef.current?.runtime ?? shouldNeverHappen('Not initialized')
343
- return Effect.gen(function* () {
344
- const queue = yield* connectedClientSessionPullQueues.makeQueue
345
- const payloadsSinceCursor = Array.from(mergePayloads.entries())
346
- .map(([mergeCounter, payload]) => ({ payload, mergeCounter }))
347
- .filter(({ mergeCounter }) => mergeCounter > cursor.mergeCounter)
348
- .toSorted((a, b) => a.mergeCounter - b.mergeCounter)
349
- .map(({ payload, mergeCounter }) => {
350
- if (payload._tag === 'upstream-advance') {
351
- return {
352
- payload: {
353
- _tag: 'upstream-advance' as const,
354
- newEvents: ReadonlyArray.dropWhile(payload.newEvents, (eventEncoded) =>
355
- EventSequenceNumber.isGreaterThanOrEqual(cursor.eventNum, eventEncoded.seqNum),
356
- ),
357
- },
358
- mergeCounter,
359
- }
360
- } else {
361
- return { payload, mergeCounter }
362
- }
363
- })
296
+ /*
297
+ Notes for a potential new `LeaderSyncProcessor.pull` implementation:
364
298
 
365
- yield* queue.offerAll(payloadsSinceCursor)
299
+ - Doesn't take cursor but is "atomically called" in the leader during the snapshot phase
300
+ - TODO: how is this done "atomically" in the web adapter where the snapshot is read optimistically?
301
+ - Would require a new kind of "boot-phase" API which is stream based:
302
+ - initial message: state snapshot + seq num head
303
+ - subsequent messages: sync state payloads
366
304
 
367
- return queue
368
- }).pipe(Effect.provide(runtime))
305
+ - alternative: instead of session pulling sync state payloads from leader, we could send
306
+ - events in the "advance" case
307
+ - full new state db snapshot in the "rebase" case
308
+ - downside: importing the snapshot is expensive
309
+ */
310
+ const pullQueue: LeaderSyncProcessor['pullQueue'] = ({ cursor }) => {
311
+ const runtime = ctxRef.current?.runtime ?? shouldNeverHappen('Not initialized')
312
+ return connectedClientSessionPullQueues.makeQueue(cursor).pipe(Effect.provide(runtime))
369
313
  }
370
314
 
371
315
  const syncState = Subscribable.make({
@@ -384,7 +328,6 @@ export const makeLeaderSyncProcessor = ({
384
328
  pushPartial,
385
329
  boot,
386
330
  syncState,
387
- getMergeCounter: () => mergeCounterRef.current,
388
331
  } satisfies LeaderSyncProcessor
389
332
  })
390
333
 
@@ -397,10 +340,7 @@ const backgroundApplyLocalPushes = ({
397
340
  schema,
398
341
  isClientEvent,
399
342
  otelSpan,
400
- currentLocalPushGenerationRef,
401
343
  connectedClientSessionPullQueues,
402
- mergeCounterRef,
403
- mergePayloads,
404
344
  localPushBatchSize,
405
345
  testing,
406
346
  }: {
@@ -412,10 +352,7 @@ const backgroundApplyLocalPushes = ({
412
352
  schema: LiveStoreSchema
413
353
  isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
414
354
  otelSpan: otel.Span | undefined
415
- currentLocalPushGenerationRef: { current: number }
416
355
  connectedClientSessionPullQueues: PullQueueSet
417
- mergeCounterRef: { current: number }
418
- mergePayloads: Map<number, typeof SyncState.PayloadUpstream.Type>
419
356
  localPushBatchSize: number
420
357
  testing: {
421
358
  delay: Effect.Effect<void> | undefined
@@ -435,24 +372,26 @@ const backgroundApplyLocalPushes = ({
435
372
  // Prevent backend pull processing until this local push is finished
436
373
  yield* pullLatch.close
437
374
 
438
- // Since the generation might have changed since enqueuing, we need to filter out items with older generation
375
+ const syncState = yield* syncStateSref
376
+ if (syncState === undefined) return shouldNeverHappen('Not initialized')
377
+
378
+ const currentRebaseGeneration = syncState.localHead.rebaseGeneration
379
+
380
+ // Since the rebase generation might have changed since enqueuing, we need to filter out items with older generation
439
381
  // It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
440
- const filteredBatchItems = batchItems
441
- .filter(([_1, _2, generation]) => generation === currentLocalPushGenerationRef.current)
442
- .map(([eventEncoded, deferred]) => [eventEncoded, deferred] as const)
382
+ const [newEvents, deferreds] = pipe(
383
+ batchItems,
384
+ ReadonlyArray.filter(([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration === currentRebaseGeneration),
385
+ ReadonlyArray.unzip,
386
+ )
443
387
 
444
- if (filteredBatchItems.length === 0) {
388
+ if (newEvents.length === 0) {
445
389
  // console.log('dropping old-gen batch', currentLocalPushGenerationRef.current)
446
390
  // Allow the backend pulling to start
447
391
  yield* pullLatch.open
448
392
  continue
449
393
  }
450
394
 
451
- const [newEvents, deferreds] = ReadonlyArray.unzip(filteredBatchItems)
452
-
453
- const syncState = yield* syncStateSref
454
- if (syncState === undefined) return shouldNeverHappen('Not initialized')
455
-
456
395
  const mergeResult = SyncState.merge({
457
396
  syncState,
458
397
  payload: { _tag: 'local-push', newEvents },
@@ -460,29 +399,25 @@ const backgroundApplyLocalPushes = ({
460
399
  isEqualEvent: LiveStoreEvent.isEqualEncoded,
461
400
  })
462
401
 
463
- const mergeCounter = yield* incrementMergeCounter(mergeCounterRef)
464
-
465
402
  switch (mergeResult._tag) {
466
403
  case 'unexpected-error': {
467
- otelSpan?.addEvent(`[${mergeCounter}]:push:unexpected-error`, {
404
+ otelSpan?.addEvent(`push:unexpected-error`, {
468
405
  batchSize: newEvents.length,
469
406
  newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
470
407
  })
471
- return yield* Effect.fail(mergeResult.cause)
408
+ return yield* new SyncError({ cause: mergeResult.message })
472
409
  }
473
410
  case 'rebase': {
474
411
  return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
475
412
  }
476
413
  case 'reject': {
477
- otelSpan?.addEvent(`[${mergeCounter}]:push:reject`, {
414
+ otelSpan?.addEvent(`push:reject`, {
478
415
  batchSize: newEvents.length,
479
416
  mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
480
417
  })
481
418
 
482
419
  // TODO: how to test this?
483
- currentLocalPushGenerationRef.current++
484
-
485
- const nextGeneration = currentLocalPushGenerationRef.current
420
+ const nextRebaseGeneration = currentRebaseGeneration + 1
486
421
 
487
422
  const providedNum = newEvents.at(0)!.seqNum
488
423
  // All subsequent pushes with same generation should be rejected as well
@@ -490,13 +425,13 @@ const backgroundApplyLocalPushes = ({
490
425
  // from the next generation which we preserve in the queue
491
426
  const remainingEventsMatchingGeneration = yield* BucketQueue.takeSplitWhere(
492
427
  localPushesQueue,
493
- (item) => item[2] >= nextGeneration,
428
+ ([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration >= nextRebaseGeneration,
494
429
  )
495
430
 
496
431
  // TODO we still need to better understand and handle this scenario
497
432
  if (LS_DEV && (yield* BucketQueue.size(localPushesQueue)) > 0) {
498
433
  console.log('localPushesQueue is not empty', yield* BucketQueue.size(localPushesQueue))
499
- // biome-ignore lint/suspicious/noDebugger: <explanation>
434
+ // biome-ignore lint/suspicious/noDebugger: debugging
500
435
  debugger
501
436
  }
502
437
 
@@ -508,11 +443,7 @@ const backgroundApplyLocalPushes = ({
508
443
  yield* Effect.forEach(allDeferredsToReject, (deferred) =>
509
444
  Deferred.fail(
510
445
  deferred,
511
- LeaderAheadError.make({
512
- minimumExpectedNum: mergeResult.expectedMinimumId,
513
- providedNum,
514
- // nextGeneration,
515
- }),
446
+ LeaderAheadError.make({ minimumExpectedNum: mergeResult.expectedMinimumId, providedNum }),
516
447
  ),
517
448
  )
518
449
 
@@ -535,11 +466,10 @@ const backgroundApplyLocalPushes = ({
535
466
 
536
467
  yield* connectedClientSessionPullQueues.offer({
537
468
  payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
538
- mergeCounter,
469
+ leaderHead: mergeResult.newSyncState.localHead,
539
470
  })
540
- mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }))
541
471
 
542
- otelSpan?.addEvent(`[${mergeCounter}]:push:advance`, {
472
+ otelSpan?.addEvent(`push:advance`, {
543
473
  batchSize: newEvents.length,
544
474
  mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
545
475
  })
@@ -621,8 +551,6 @@ const backgroundBackendPulling = ({
621
551
  devtoolsLatch,
622
552
  initialBlockingSyncContext,
623
553
  connectedClientSessionPullQueues,
624
- mergeCounterRef,
625
- mergePayloads,
626
554
  advancePushHead,
627
555
  }: {
628
556
  initialBackendHead: EventSequenceNumber.GlobalEventSequenceNumber
@@ -638,8 +566,6 @@ const backgroundBackendPulling = ({
638
566
  devtoolsLatch: Effect.Latch | undefined
639
567
  initialBlockingSyncContext: InitialBlockingSyncContext
640
568
  connectedClientSessionPullQueues: PullQueueSet
641
- mergeCounterRef: { current: number }
642
- mergePayloads: Map<number, typeof SyncState.PayloadUpstream.Type>
643
569
  advancePushHead: (eventNum: EventSequenceNumber.EventSequenceNumber) => void
644
570
  }) =>
645
571
  Effect.gen(function* () {
@@ -672,16 +598,14 @@ const backgroundBackendPulling = ({
672
598
  ignoreClientEvents: true,
673
599
  })
674
600
 
675
- const mergeCounter = yield* incrementMergeCounter(mergeCounterRef)
676
-
677
601
  if (mergeResult._tag === 'reject') {
678
602
  return shouldNeverHappen('The leader thread should never reject upstream advances')
679
603
  } else if (mergeResult._tag === 'unexpected-error') {
680
- otelSpan?.addEvent(`[${mergeCounter}]:pull:unexpected-error`, {
604
+ otelSpan?.addEvent(`pull:unexpected-error`, {
681
605
  newEventsCount: newEvents.length,
682
606
  newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
683
607
  })
684
- return yield* Effect.fail(mergeResult.cause)
608
+ return yield* new SyncError({ cause: mergeResult.message })
685
609
  }
686
610
 
687
611
  const newBackendHead = newEvents.at(-1)!.seqNum
@@ -689,7 +613,7 @@ const backgroundBackendPulling = ({
689
613
  Eventlog.updateBackendHead(dbEventlog, newBackendHead)
690
614
 
691
615
  if (mergeResult._tag === 'rebase') {
692
- otelSpan?.addEvent(`[${mergeCounter}]:pull:rebase`, {
616
+ otelSpan?.addEvent(`pull:rebase[${mergeResult.newSyncState.localHead.rebaseGeneration}]`, {
693
617
  newEventsCount: newEvents.length,
694
618
  newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
695
619
  rollbackCount: mergeResult.rollbackEvents.length,
@@ -711,30 +635,19 @@ const backgroundBackendPulling = ({
711
635
  }
712
636
 
713
637
  yield* connectedClientSessionPullQueues.offer({
714
- payload: SyncState.PayloadUpstreamRebase.make({
715
- newEvents: mergeResult.newEvents,
716
- rollbackEvents: mergeResult.rollbackEvents,
717
- }),
718
- mergeCounter,
638
+ payload: SyncState.payloadFromMergeResult(mergeResult),
639
+ leaderHead: mergeResult.newSyncState.localHead,
719
640
  })
720
- mergePayloads.set(
721
- mergeCounter,
722
- SyncState.PayloadUpstreamRebase.make({
723
- newEvents: mergeResult.newEvents,
724
- rollbackEvents: mergeResult.rollbackEvents,
725
- }),
726
- )
727
641
  } else {
728
- otelSpan?.addEvent(`[${mergeCounter}]:pull:advance`, {
642
+ otelSpan?.addEvent(`pull:advance`, {
729
643
  newEventsCount: newEvents.length,
730
644
  mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
731
645
  })
732
646
 
733
647
  yield* connectedClientSessionPullQueues.offer({
734
- payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
735
- mergeCounter,
648
+ payload: SyncState.payloadFromMergeResult(mergeResult),
649
+ leaderHead: mergeResult.newSyncState.localHead,
736
650
  })
737
- mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }))
738
651
 
739
652
  if (mergeResult.confirmedEvents.length > 0) {
740
653
  // `mergeResult.confirmedEvents` don't contain the correct sync metadata, so we need to use
@@ -763,7 +676,7 @@ const backgroundBackendPulling = ({
763
676
  }
764
677
  })
765
678
 
766
- const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo(initialBackendHead)
679
+ const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: initialBackendHead })
767
680
 
768
681
  const hashMaterializerResult = makeMaterializerHash({ schema, dbState })
769
682
 
@@ -854,19 +767,25 @@ const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.EventSeque
854
767
  }
855
768
 
856
769
  interface PullQueueSet {
857
- makeQueue: Effect.Effect<
858
- Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>,
770
+ makeQueue: (
771
+ cursor: EventSequenceNumber.EventSequenceNumber,
772
+ ) => Effect.Effect<
773
+ Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>,
859
774
  UnexpectedError,
860
775
  Scope.Scope | LeaderThreadCtx
861
776
  >
862
777
  offer: (item: {
863
778
  payload: typeof SyncState.PayloadUpstream.Type
864
- mergeCounter: number
779
+ leaderHead: EventSequenceNumber.EventSequenceNumber
865
780
  }) => Effect.Effect<void, UnexpectedError>
866
781
  }
867
782
 
868
783
  const makePullQueueSet = Effect.gen(function* () {
869
- const set = new Set<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>>()
784
+ const set = new Set<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>>()
785
+
786
+ type StringifiedSeqNum = string
787
+ // NOTE this could grow unbounded for long running sessions
788
+ const cachedPayloads = new Map<StringifiedSeqNum, (typeof SyncState.PayloadUpstream.Type)[]>()
870
789
 
871
790
  yield* Effect.addFinalizer(() =>
872
791
  Effect.gen(function* () {
@@ -878,21 +797,81 @@ const makePullQueueSet = Effect.gen(function* () {
878
797
  }),
879
798
  )
880
799
 
881
- const makeQueue: PullQueueSet['makeQueue'] = Effect.gen(function* () {
882
- const queue = yield* Queue.unbounded<{
883
- payload: typeof SyncState.PayloadUpstream.Type
884
- mergeCounter: number
885
- }>().pipe(Effect.acquireRelease(Queue.shutdown))
800
+ const makeQueue: PullQueueSet['makeQueue'] = (cursor) =>
801
+ Effect.gen(function* () {
802
+ const queue = yield* Queue.unbounded<{
803
+ payload: typeof SyncState.PayloadUpstream.Type
804
+ }>().pipe(Effect.acquireRelease(Queue.shutdown))
886
805
 
887
- yield* Effect.addFinalizer(() => Effect.sync(() => set.delete(queue)))
806
+ yield* Effect.addFinalizer(() => Effect.sync(() => set.delete(queue)))
888
807
 
889
- set.add(queue)
808
+ const payloadsSinceCursor = Array.from(cachedPayloads.entries())
809
+ .flatMap(([seqNumStr, payloads]) =>
810
+ payloads.map((payload) => ({ payload, seqNum: EventSequenceNumber.fromString(seqNumStr) })),
811
+ )
812
+ .filter(({ seqNum }) => EventSequenceNumber.isGreaterThan(seqNum, cursor))
813
+ .toSorted((a, b) => EventSequenceNumber.compare(a.seqNum, b.seqNum))
814
+ .map(({ payload }) => {
815
+ if (payload._tag === 'upstream-advance') {
816
+ return {
817
+ payload: {
818
+ _tag: 'upstream-advance' as const,
819
+ newEvents: ReadonlyArray.dropWhile(payload.newEvents, (eventEncoded) =>
820
+ EventSequenceNumber.isGreaterThanOrEqual(cursor, eventEncoded.seqNum),
821
+ ),
822
+ },
823
+ }
824
+ } else {
825
+ return { payload }
826
+ }
827
+ })
890
828
 
891
- return queue
892
- })
829
+ // console.debug(
830
+ // 'seeding new queue',
831
+ // {
832
+ // cursor,
833
+ // },
834
+ // '\n mergePayloads',
835
+ // ...Array.from(cachedPayloads.entries())
836
+ // .flatMap(([seqNumStr, payloads]) =>
837
+ // payloads.map((payload) => ({ payload, seqNum: EventSequenceNumber.fromString(seqNumStr) })),
838
+ // )
839
+ // .map(({ payload, seqNum }) => [
840
+ // seqNum,
841
+ // payload._tag,
842
+ // 'newEvents',
843
+ // ...payload.newEvents.map((_) => _.toJSON()),
844
+ // 'rollbackEvents',
845
+ // ...(payload._tag === 'upstream-rebase' ? payload.rollbackEvents.map((_) => _.toJSON()) : []),
846
+ // ]),
847
+ // '\n payloadsSinceCursor',
848
+ // ...payloadsSinceCursor.map(({ payload }) => [
849
+ // payload._tag,
850
+ // 'newEvents',
851
+ // ...payload.newEvents.map((_) => _.toJSON()),
852
+ // 'rollbackEvents',
853
+ // ...(payload._tag === 'upstream-rebase' ? payload.rollbackEvents.map((_) => _.toJSON()) : []),
854
+ // ]),
855
+ // )
856
+
857
+ yield* queue.offerAll(payloadsSinceCursor)
858
+
859
+ set.add(queue)
860
+
861
+ return queue
862
+ })
893
863
 
894
864
  const offer: PullQueueSet['offer'] = (item) =>
895
865
  Effect.gen(function* () {
866
+ const seqNumStr = EventSequenceNumber.toString(item.leaderHead)
867
+ if (cachedPayloads.has(seqNumStr)) {
868
+ cachedPayloads.get(seqNumStr)!.push(item.payload)
869
+ } else {
870
+ cachedPayloads.set(seqNumStr, [item.payload])
871
+ }
872
+
873
+ // console.debug(`offering to ${set.size} queues`, item.leaderHead, JSON.stringify(item.payload, null, 2))
874
+
896
875
  // Short-circuit if the payload is an empty upstream advance
897
876
  if (item.payload._tag === 'upstream-advance' && item.payload.newEvents.length === 0) {
898
877
  return
@@ -909,24 +888,6 @@ const makePullQueueSet = Effect.gen(function* () {
909
888
  }
910
889
  })
911
890
 
912
- const incrementMergeCounter = (mergeCounterRef: { current: number }) =>
913
- Effect.gen(function* () {
914
- const { dbState } = yield* LeaderThreadCtx
915
- mergeCounterRef.current++
916
- dbState.execute(
917
- sql`INSERT OR REPLACE INTO ${SystemTables.LEADER_MERGE_COUNTER_TABLE} (id, mergeCounter) VALUES (0, ${mergeCounterRef.current})`,
918
- )
919
- return mergeCounterRef.current
920
- })
921
-
922
- const getMergeCounterFromDb = (dbState: SqliteDb) =>
923
- Effect.gen(function* () {
924
- const result = dbState.select<{ mergeCounter: number }>(
925
- sql`SELECT mergeCounter FROM ${SystemTables.LEADER_MERGE_COUNTER_TABLE} WHERE id = 0`,
926
- )
927
- return result[0]?.mergeCounter ?? 0
928
- })
929
-
930
891
  const validatePushBatch = (
931
892
  batch: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
932
893
  pushHead: EventSequenceNumber.EventSequenceNumber,