@@ -13,15 +13,18 @@ import { CustomWriteCheckpointDocument, WriteCheckpointDocument } from './models
13
13
export type MongoCheckpointAPIOptions = {
14
14
db : PowerSyncMongo ;
15
15
mode : storage . WriteCheckpointMode ;
16
+ sync_rules_id : number ;
16
17
} ;
17
18
18
19
export class MongoWriteCheckpointAPI implements storage . WriteCheckpointAPI {
19
20
readonly db : PowerSyncMongo ;
20
21
private _mode : storage . WriteCheckpointMode ;
22
+ private sync_rules_id : number ;
21
23
22
24
constructor ( options : MongoCheckpointAPIOptions ) {
23
25
this . db = options . db ;
24
26
this . _mode = options . mode ;
27
+ this . sync_rules_id = options . sync_rules_id ;
25
28
}
26
29
27
30
get writeCheckpointMode ( ) {
@@ -89,14 +92,8 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
89
92
}
90
93
}
91
94
92
- private sharedIter = new Demultiplexer < WriteCheckpointResult > ( ( signal ) => {
93
- const clusterTimePromise = ( async ( ) => {
94
- const hello = await this . db . db . command ( { hello : 1 } ) ;
95
- // Note: This is not valid on sharded clusters.
96
- const startClusterTime = hello . lastWrite ?. majorityOpTime ?. ts as mongo . Timestamp ;
97
- startClusterTime ;
98
- return startClusterTime ;
99
- } ) ( ) ;
95
+ private sharedManagedIter = new Demultiplexer < WriteCheckpointResult > ( ( signal ) => {
96
+ const clusterTimePromise = this . getClusterTime ( ) ;
100
97
101
98
return {
102
99
iterator : this . watchAllManagedWriteCheckpoints ( clusterTimePromise , signal ) ,
@@ -170,13 +167,6 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
170
167
}
171
168
) ;
172
169
173
- const hello = await this . db . db . command ( { hello : 1 } ) ;
174
- // Note: This is not valid on sharded clusters.
175
- const startClusterTime = hello . lastWrite ?. majorityOpTime ?. ts as mongo . Timestamp ;
176
- if ( startClusterTime == null ) {
177
- throw new framework . ServiceAssertionError ( 'Could not get clusterTime' ) ;
178
- }
179
-
180
170
signal . onabort = ( ) => {
181
171
stream . close ( ) ;
182
172
} ;
@@ -202,55 +192,75 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
202
192
}
203
193
}
204
194
205
- async * watchManagedWriteCheckpoint (
206
- options : WatchUserWriteCheckpointOptions
207
- ) : AsyncIterable < storage . WriteCheckpointResult > {
208
- const stream = this . sharedIter . subscribe ( options . user_id , options . signal ) ;
195
+ watchManagedWriteCheckpoint ( options : WatchUserWriteCheckpointOptions ) : AsyncIterable < storage . WriteCheckpointResult > {
196
+ const stream = this . sharedManagedIter . subscribe ( options . user_id , options . signal ) ;
197
+ return this . orderedStream ( stream ) ;
198
+ }
209
199
210
- let lastId = - 1n ;
200
+ private sharedCustomIter = new Demultiplexer < WriteCheckpointResult > ( ( signal ) => {
201
+ const clusterTimePromise = this . getClusterTime ( ) ;
211
202
212
- for await ( let doc of stream ) {
213
- // Guard against out-of-order events
214
- if ( lastId == - 1n || ( doc . id != null && doc . id > lastId ) ) {
215
- yield doc ;
216
- if ( doc . id != null ) {
217
- lastId = doc . id ;
218
- }
219
- }
220
- }
221
- }
203
+ return {
204
+ iterator : this . watchAllCustomWriteCheckpoints ( clusterTimePromise , signal ) ,
205
+ getFirstValue : async ( user_id : string ) => {
206
+ // We cater for the same potential race conditions as for managed write checkpoints.
222
207
223
- async * watchCustomWriteCheckpoint (
224
- options : WatchUserWriteCheckpointOptions
225
- ) : AsyncIterable < storage . WriteCheckpointResult > {
226
- const { user_id, sync_rules_id, signal } = options ;
208
+ const changeStreamStart = await clusterTimePromise ;
227
209
228
- let doc = null as CustomWriteCheckpointDocument | null ;
229
- let clusterTime = null as mongo . Timestamp | null ;
210
+ let doc = null as CustomWriteCheckpointDocument | null ;
211
+ let clusterTime = null as mongo . Timestamp | null ;
230
212
231
- await this . db . client . withSession ( async ( session ) => {
232
- doc = await this . db . custom_write_checkpoints . findOne (
233
- {
234
- user_id : user_id ,
235
- sync_rules_id : sync_rules_id
236
- } ,
237
- {
238
- session
213
+ await this . db . client . withSession ( async ( session ) => {
214
+ doc = await this . db . custom_write_checkpoints . findOne (
215
+ {
216
+ user_id : user_id ,
217
+ sync_rules_id : this . sync_rules_id
218
+ } ,
219
+ {
220
+ session
221
+ }
222
+ ) ;
223
+ const time = session . clusterTime ?. clusterTime ?? null ;
224
+ clusterTime = time ;
225
+ } ) ;
226
+ if ( clusterTime == null ) {
227
+ throw new framework . ServiceAssertionError ( 'Could not get clusterTime for write checkpoint' ) ;
228
+ }
229
+
230
+ if ( clusterTime . lessThan ( changeStreamStart ) ) {
231
+ throw new framework . ServiceAssertionError (
232
+ 'clusterTime for write checkpoint is older than changestream start'
233
+ ) ;
234
+ }
235
+
236
+ if ( doc == null ) {
237
+ // No write checkpoint, but we still need to return a result
238
+ return {
239
+ id : null ,
240
+ lsn : null
241
+ } ;
239
242
}
240
- ) ;
241
- const time = session . clusterTime ?. clusterTime ?? null ;
242
- clusterTime = time ;
243
- } ) ;
244
- if ( clusterTime == null ) {
245
- throw new framework . ServiceAssertionError ( 'Could not get clusterTime' ) ;
246
- }
243
+
244
+ return {
245
+ id : doc . checkpoint ,
246
+ // custom write checkpoints are not tied to a LSN
247
+ lsn : null
248
+ } ;
249
+ }
250
+ } ;
251
+ } ) ;
252
+
253
+ private async * watchAllCustomWriteCheckpoints (
254
+ clusterTimePromise : Promise < mongo . BSON . Timestamp > ,
255
+ signal : AbortSignal
256
+ ) : AsyncGenerator < DemultiplexerValue < WriteCheckpointResult > > {
257
+ const clusterTime = await clusterTimePromise ;
247
258
248
259
const stream = this . db . custom_write_checkpoints . watch (
249
260
[
250
261
{
251
262
$match : {
252
- 'fullDocument.user_id' : user_id ,
253
- 'fullDocument.sync_rules_id' : sync_rules_id ,
263
+ 'fullDocument.sync_rules_id' : this . sync_rules_id ,
254
264
operationType : { $in : [ 'insert' , 'update' , 'replace' ] }
255
265
}
256
266
}
@@ -270,34 +280,30 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
270
280
return ;
271
281
}
272
282
273
- let lastId = - 1n ;
274
-
275
- if ( doc != null ) {
276
- yield {
277
- id : doc . checkpoint ,
278
- lsn : null
279
- } ;
280
- lastId = doc . checkpoint ;
281
- } else {
282
- yield {
283
- id : null ,
284
- lsn : null
285
- } ;
286
- }
287
-
288
283
for await ( let event of stream ) {
289
284
if ( ! ( 'fullDocument' in event ) || event . fullDocument == null ) {
290
285
continue ;
291
286
}
292
- // Guard against out-of-order events
293
- if ( event . fullDocument . checkpoint > lastId ) {
294
- yield {
287
+
288
+ const user_id = event . fullDocument . user_id ;
289
+ yield {
290
+ key : user_id ,
291
+ value : {
295
292
id : event . fullDocument . checkpoint ,
293
+ // Custom write checkpoints are not tied to a specific LSN
296
294
lsn : null
297
- } ;
298
- lastId = event . fullDocument . checkpoint ;
299
- }
295
+ }
296
+ } ;
297
+ }
298
+ }
299
+
300
+ watchCustomWriteCheckpoint ( options : WatchUserWriteCheckpointOptions ) : AsyncIterable < storage . WriteCheckpointResult > {
301
+ if ( options . sync_rules_id != this . sync_rules_id ) {
302
+ throw new framework . ServiceAssertionError ( 'sync_rules_id does not match' ) ;
300
303
}
304
+
305
+ const stream = this . sharedCustomIter . subscribe ( options . user_id , options . signal ) ;
306
+ return this . orderedStream ( stream ) ;
301
307
}
302
308
303
309
protected async lastCustomWriteCheckpoint ( filters : storage . CustomWriteCheckpointFilters ) {
@@ -323,6 +329,30 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
323
329
} ) ;
324
330
return lastWriteCheckpoint ?. client_id ?? null ;
325
331
}
332
+
333
+ private async getClusterTime ( ) : Promise < mongo . Timestamp > {
334
+ const hello = await this . db . db . command ( { hello : 1 } ) ;
335
+ // Note: This is not valid on sharded clusters.
336
+ const startClusterTime = hello . lastWrite ?. majorityOpTime ?. ts as mongo . Timestamp ;
337
+ return startClusterTime ;
338
+ }
339
+
340
+ /**
341
+ * Makes a write checkpoint stream an orderered one - any out-of-order events are discarded.
342
+ */
343
+ private async * orderedStream ( stream : AsyncIterable < storage . WriteCheckpointResult > ) {
344
+ let lastId = - 1n ;
345
+
346
+ for await ( let event of stream ) {
347
+ // Guard against out-of-order events
348
+ if ( lastId == - 1n || ( event . id != null && event . id > lastId ) ) {
349
+ yield event ;
350
+ if ( event . id != null ) {
351
+ lastId = event . id ;
352
+ }
353
+ }
354
+ }
355
+ }
326
356
}
327
357
328
358
export async function batchCreateCustomWriteCheckpoints (
0 commit comments