@@ -4,10 +4,11 @@ import { Option, type Runtime, type Scope } from '@livestore/utils/effect'
4
4
import { BucketQueue , Effect , FiberHandle , Queue , Schema , Stream , Subscribable } from '@livestore/utils/effect'
5
5
import * as otel from '@opentelemetry/api'
6
6
7
- import { type ClientSession , SyncError , type UnexpectedError } from '../adapter-types.js'
7
+ import type { ClientSession , UnexpectedError } from '../adapter-types.js'
8
8
import * as EventSequenceNumber from '../schema/EventSequenceNumber.js'
9
9
import * as LiveStoreEvent from '../schema/LiveStoreEvent.js'
10
- import { getEventDef , type LiveStoreSchema } from '../schema/mod.js'
10
+ import { getEventDef , type LiveStoreSchema , SystemTables } from '../schema/mod.js'
11
+ import { sql } from '../util.js'
11
12
import * as SyncState from './syncstate.js'
12
13
13
14
/**
@@ -20,10 +21,6 @@ import * as SyncState from './syncstate.js'
20
21
* - We might need to make the rebase behaviour configurable e.g. to let users manually trigger a rebase
21
22
*
22
23
* Longer term we should evalutate whether we can unify the ClientSessionSyncProcessor with the LeaderSyncProcessor.
23
- *
24
- * The session and leader sync processor are different in the following ways:
25
- * - The leader sync processor pulls regular LiveStore events, while the session sync processor pulls SyncState.PayloadUpstream items
26
- * - The session sync processor has no downstream nodes.
27
24
*/
28
25
export const makeClientSessionSyncProcessor = ( {
29
26
schema,
@@ -40,7 +37,7 @@ export const makeClientSessionSyncProcessor = ({
40
37
clientSession : ClientSession
41
38
runtime : Runtime . Runtime < Scope . Scope >
42
39
materializeEvent : (
43
- eventDecoded : LiveStoreEvent . AnyDecoded ,
40
+ eventDecoded : LiveStoreEvent . PartialAnyDecoded ,
44
41
options : { otelContext : otel . Context ; withChangeset : boolean ; materializerHashLeader : Option . Option < number > } ,
45
42
) => {
46
43
writeTables : Set < string >
@@ -79,16 +76,16 @@ export const makeClientSessionSyncProcessor = ({
79
76
/** We're queuing push requests to reduce the number of messages sent to the leader by batching them */
80
77
const leaderPushQueue = BucketQueue . make < LiveStoreEvent . EncodedWithMeta > ( ) . pipe ( Effect . runSync )
81
78
82
- const push : ClientSessionSyncProcessor [ 'push' ] = ( batch , { otelContext } ) => {
79
+ const push : ClientSessionSyncProcessor [ 'push' ] = Effect . fn ( 'client-session-sync-processor:push' ) ( function * (
80
+ batch ,
81
+ { otelContext } ,
82
+ ) {
83
83
// TODO validate batch
84
84
85
85
let baseEventSequenceNumber = syncStateRef . current . localHead
86
86
const encodedEventDefs = batch . map ( ( { name, args } ) => {
87
87
const eventDef = getEventDef ( schema , name )
88
- const nextNumPair = EventSequenceNumber . nextPair ( {
89
- seqNum : baseEventSequenceNumber ,
90
- isClient : eventDef . eventDef . options . clientOnly ,
91
- } )
88
+ const nextNumPair = EventSequenceNumber . nextPair ( baseEventSequenceNumber , eventDef . eventDef . options . clientOnly )
92
89
baseEventSequenceNumber = nextNumPair . seqNum
93
90
return new LiveStoreEvent . EncodedWithMeta (
94
91
Schema . encodeUnknownSync ( eventSchema ) ( {
@@ -100,29 +97,29 @@ export const makeClientSessionSyncProcessor = ({
100
97
} ) ,
101
98
)
102
99
} )
103
-
104
- const mergeResult = SyncState . merge ( {
105
- syncState : syncStateRef . current ,
106
- payload : { _tag : 'local-push' , newEvents : encodedEventDefs } ,
107
- isClientEvent,
108
- isEqualEvent : LiveStoreEvent . isEqualEncoded ,
109
- } )
100
+ yield * Effect . annotateCurrentSpan ( { batchSize : encodedEventDefs . length } )
101
+
102
+ const mergeResult = yield * Effect . sync ( ( ) =>
103
+ SyncState . merge ( {
104
+ syncState : syncStateRef . current ,
105
+ payload : { _tag : 'local-push' , newEvents : encodedEventDefs } ,
106
+ isClientEvent,
107
+ isEqualEvent : LiveStoreEvent . isEqualEncoded ,
108
+ } ) ,
109
+ )
110
110
111
111
if ( mergeResult . _tag === 'unexpected-error' ) {
112
- return shouldNeverHappen ( ' Unexpected error in client-session-sync-processor' , mergeResult . message )
112
+ return yield * Effect . die ( new Error ( ` Unexpected error in client-session-sync-processor: ${ mergeResult . cause } ` ) )
113
113
}
114
114
115
- span . addEvent ( 'local-push' , {
116
- batchSize : encodedEventDefs . length ,
117
- mergeResult : TRACE_VERBOSE ? JSON . stringify ( mergeResult ) : undefined ,
118
- } )
115
+ if ( TRACE_VERBOSE ) yield * Effect . annotateCurrentSpan ( { mergeResult : JSON . stringify ( mergeResult ) } )
119
116
120
117
if ( mergeResult . _tag !== 'advance' ) {
121
- return shouldNeverHappen ( `Expected advance, got ${ mergeResult . _tag } ` )
118
+ return yield * Effect . die ( new Error ( `Expected advance, got ${ mergeResult . _tag } ` ) )
122
119
}
123
120
124
121
syncStateRef . current = mergeResult . newSyncState
125
- syncStateUpdateQueue . offer ( mergeResult . newSyncState ) . pipe ( Effect . runSync )
122
+ yield * syncStateUpdateQueue . offer ( mergeResult . newSyncState )
126
123
127
124
// Materialize events to state
128
125
const writeTables = new Set < string > ( )
@@ -141,16 +138,18 @@ export const makeClientSessionSyncProcessor = ({
141
138
for ( const table of newWriteTables ) {
142
139
writeTables . add ( table )
143
140
}
144
- event . meta . sessionChangeset = sessionChangeset
145
- event . meta . materializerHashSession = materializerHash
141
+ yield * Effect . sync ( ( ) => {
142
+ event . meta . sessionChangeset = sessionChangeset
143
+ event . meta . materializerHashSession = materializerHash
144
+ } )
146
145
}
147
146
148
147
// Trigger push to leader
149
148
// console.debug('pushToLeader', encodedEventDefs.length, ...encodedEventDefs.map((_) => _.toJSON()))
150
- BucketQueue . offerAll ( leaderPushQueue , encodedEventDefs ) . pipe ( Effect . runSync )
149
+ yield * BucketQueue . offerAll ( leaderPushQueue , encodedEventDefs )
151
150
152
151
return { writeTables }
153
- }
152
+ } )
154
153
155
154
const debugInfo = {
156
155
rebaseCount : 0 ,
@@ -190,11 +189,18 @@ export const makeClientSessionSyncProcessor = ({
190
189
191
190
yield * FiberHandle . run ( leaderPushingFiberHandle , backgroundLeaderPushing )
192
191
192
+ const getMergeCounter = ( ) =>
193
+ clientSession . sqliteDb . select < { mergeCounter : number } > (
194
+ sql `SELECT mergeCounter FROM ${ SystemTables . LEADER_MERGE_COUNTER_TABLE } WHERE id = 0` ,
195
+ ) [ 0 ] ?. mergeCounter ?? 0
196
+
193
197
// NOTE We need to lazily call `.pull` as we want the cursor to be updated
194
198
yield * Stream . suspend ( ( ) =>
195
- clientSession . leaderThread . events . pull ( { cursor : syncStateRef . current . upstreamHead } ) ,
199
+ clientSession . leaderThread . events . pull ( {
200
+ cursor : { mergeCounter : getMergeCounter ( ) , eventNum : syncStateRef . current . localHead } ,
201
+ } ) ,
196
202
) . pipe (
197
- Stream . tap ( ( { payload } ) =>
203
+ Stream . tap ( ( { payload, mergeCounter : leaderMergeCounter } ) =>
198
204
Effect . gen ( function * ( ) {
199
205
// yield* Effect.logDebug('ClientSessionSyncProcessor:pull', payload)
200
206
@@ -210,13 +216,13 @@ export const makeClientSessionSyncProcessor = ({
210
216
} )
211
217
212
218
if ( mergeResult . _tag === 'unexpected-error' ) {
213
- return yield * new SyncError ( { cause : mergeResult . message } )
219
+ return yield * Effect . fail ( mergeResult . cause )
214
220
} else if ( mergeResult . _tag === 'reject' ) {
215
221
return shouldNeverHappen ( 'Unexpected reject in client-session-sync-processor' , mergeResult )
216
222
}
217
223
218
224
syncStateRef . current = mergeResult . newSyncState
219
- yield * syncStateUpdateQueue . offer ( mergeResult . newSyncState )
225
+ syncStateUpdateQueue . offer ( mergeResult . newSyncState ) . pipe ( Effect . runSync )
220
226
221
227
if ( mergeResult . _tag === 'rebase' ) {
222
228
span . addEvent ( 'merge:pull:rebase' , {
@@ -225,7 +231,7 @@ export const makeClientSessionSyncProcessor = ({
225
231
newEventsCount : mergeResult . newEvents . length ,
226
232
rollbackCount : mergeResult . rollbackEvents . length ,
227
233
res : TRACE_VERBOSE ? JSON . stringify ( mergeResult ) : undefined ,
228
- rebaseGeneration : mergeResult . newSyncState . localHead . rebaseGeneration ,
234
+ leaderMergeCounter ,
229
235
} )
230
236
231
237
debugInfo . rebaseCount ++
@@ -242,6 +248,7 @@ export const makeClientSessionSyncProcessor = ({
242
248
'merge:pull:rebase: rollback' ,
243
249
mergeResult . rollbackEvents . length ,
244
250
...mergeResult . rollbackEvents . slice ( 0 , 10 ) . map ( ( _ ) => _ . toJSON ( ) ) ,
251
+ { leaderMergeCounter } ,
245
252
) . pipe ( Effect . provide ( runtime ) , Effect . runSync )
246
253
}
247
254
@@ -261,6 +268,7 @@ export const makeClientSessionSyncProcessor = ({
261
268
payload : TRACE_VERBOSE ? JSON . stringify ( payload ) : undefined ,
262
269
newEventsCount : mergeResult . newEvents . length ,
263
270
res : TRACE_VERBOSE ? JSON . stringify ( mergeResult ) : undefined ,
271
+ leaderMergeCounter,
264
272
} )
265
273
266
274
debugInfo . advanceCount ++
@@ -337,9 +345,12 @@ export interface ClientSessionSyncProcessor {
337
345
push : (
338
346
batch : ReadonlyArray < LiveStoreEvent . PartialAnyDecoded > ,
339
347
options : { otelContext : otel . Context } ,
340
- ) => {
341
- writeTables : Set < string >
342
- }
348
+ ) => Effect . Effect <
349
+ {
350
+ writeTables : Set < string >
351
+ } ,
352
+ never
353
+ >
343
354
boot : Effect . Effect < void , UnexpectedError , Scope . Scope >
344
355
/**
345
356
* Only used for debugging / observability.
0 commit comments