@powersync/service-module-mongodb-storage 0.18.1 → 0.18.3

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 (69) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +3 -0
  3. package/dist/storage/MongoBucketStorage.js +2 -0
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +2 -2
  6. package/dist/storage/implementation/MongoBucketBatch.js +19 -10
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoChecksums.d.ts +31 -4
  9. package/dist/storage/implementation/MongoChecksums.js +33 -10
  10. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  11. package/dist/storage/implementation/MongoCompactor.d.ts +1 -1
  12. package/dist/storage/implementation/MongoCompactor.js +1 -1
  13. package/dist/storage/implementation/MongoParameterCompactor.d.ts +1 -1
  14. package/dist/storage/implementation/MongoParameterCompactor.js +1 -1
  15. package/dist/storage/implementation/MongoStorageProvider.js +9 -0
  16. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  17. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +9 -5
  18. package/dist/storage/implementation/MongoSyncBucketStorage.js +23 -6
  19. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  20. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +1 -1
  21. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +47 -12
  22. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  23. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +2 -0
  24. package/dist/storage/implementation/db.js +8 -1
  25. package/dist/storage/implementation/db.js.map +1 -1
  26. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +17 -2
  27. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  28. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +4 -4
  29. package/dist/storage/implementation/v1/MongoChecksumsV1.js +7 -6
  30. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  31. package/dist/storage/implementation/v1/MongoCompactorV1.js +1 -1
  32. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  33. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +4 -3
  34. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +18 -4
  35. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  36. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +20 -2
  37. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  38. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +3 -3
  39. package/dist/storage/implementation/v3/MongoChecksumsV3.js +8 -7
  40. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  41. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +4 -3
  42. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +17 -3
  43. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  44. package/dist/types/types.d.ts +12 -1
  45. package/dist/types/types.js +14 -1
  46. package/dist/types/types.js.map +1 -1
  47. package/package.json +7 -7
  48. package/src/storage/MongoBucketStorage.ts +4 -0
  49. package/src/storage/implementation/MongoBucketBatch.ts +23 -13
  50. package/src/storage/implementation/MongoChecksums.ts +87 -15
  51. package/src/storage/implementation/MongoCompactor.ts +1 -1
  52. package/src/storage/implementation/MongoParameterCompactor.ts +1 -1
  53. package/src/storage/implementation/MongoStorageProvider.ts +10 -0
  54. package/src/storage/implementation/MongoSyncBucketStorage.ts +29 -10
  55. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +62 -15
  56. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +2 -0
  57. package/src/storage/implementation/db.ts +8 -1
  58. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +18 -2
  59. package/src/storage/implementation/v1/MongoChecksumsV1.ts +20 -11
  60. package/src/storage/implementation/v1/MongoCompactorV1.ts +2 -1
  61. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +24 -6
  62. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +20 -2
  63. package/src/storage/implementation/v3/MongoChecksumsV3.ts +23 -11
  64. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +23 -5
  65. package/src/types/types.ts +17 -1
  66. package/test/src/storage_compacting.test.ts +4 -4
  67. package/test/src/storage_sync.test.ts +13 -5
  68. package/test/src/storeCurrentData.test.ts +9 -3
  69. package/tsconfig.tsbuildinfo +1 -1
@@ -203,6 +203,8 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
203
203
 
204
204
  await this.flush(options);
205
205
 
206
+ using _ = this.tracer.span('storage', 'commit');
207
+
206
208
  const now = new Date();
207
209
 
208
210
  await this.db.write_checkpoints.updateMany(
@@ -358,6 +360,7 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
358
360
  }
359
361
 
360
362
  async setResumeLsn(lsn: string): Promise<void> {
363
+ using _ = this.tracer.span('storage', 'set_resume_lsn');
361
364
  await this.db.sync_rules.updateOne(
362
365
  {
363
366
  _id: this.replicationStreamId
@@ -367,7 +370,13 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
367
370
  snapshot_lsn: lsn
368
371
  }
369
372
  },
370
- { session: this.session }
373
+ {
374
+ session: this.session,
375
+ // Losing occasional resume LSN is fine. That may mean reprocessing
376
+ // some source changes in some edge cases, which is not an issue since
377
+ // changes are processed in an idempotent way.
378
+ writeConcern: { w: 1 }
379
+ }
371
380
  );
372
381
  }
373
382
 
@@ -486,7 +495,12 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
486
495
 
487
496
  const session = this.session;
488
497
  let activated = false;
498
+ let needsFutureActivationCheck = true;
489
499
  await session.withTransaction(async () => {
500
+ // Reset on transaction retries.
501
+ activated = false;
502
+ needsFutureActivationCheck = true;
503
+
490
504
  const doc = (await this.db.sync_rules.findOne(
491
505
  { _id: this.replicationStreamId },
492
506
  { session }
@@ -518,13 +532,15 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
518
532
  );
519
533
  activated = true;
520
534
  } else if (doc?.state != storage.SyncRuleState.PROCESSING) {
521
- this.needsActivation = false;
535
+ needsFutureActivationCheck = false;
522
536
  }
523
537
  });
524
538
  if (activated) {
525
539
  this.logger.info(`Activated new replication stream at ${lsn}`);
526
540
  await this.db.notifyCheckpoint();
527
541
  this.needsActivation = false;
542
+ } else if (!needsFutureActivationCheck) {
543
+ this.needsActivation = false;
528
544
  }
529
545
  }
530
546
  }
@@ -13,15 +13,16 @@ import {
13
13
  checksumFromAggregate,
14
14
  DEFAULT_OPERATION_BATCH_LIMIT,
15
15
  FetchPartialBucketChecksumByBucket,
16
- MongoChecksums
16
+ MongoChecksums,
17
+ MongoChecksumSessionContext
17
18
  } from '../MongoChecksums.js';
18
19
  import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
19
-
20
20
  export class MongoChecksumsV1 extends MongoChecksums {
21
21
  declare protected readonly db: VersionedPowerSyncMongoV1;
22
22
 
23
23
  async computePartialChecksumsDirectByBucket(
24
- batch: FetchPartialBucketChecksumByBucket[]
24
+ batch: FetchPartialBucketChecksumByBucket[],
25
+ context: MongoChecksumSessionContext
25
26
  ): Promise<PartialChecksumMap> {
26
27
  const collection = this.db.bucketDataV1;
27
28
  const createFilter = (request: FetchPartialBucketChecksumByBucket) => ({
@@ -95,8 +96,7 @@ export class MongoChecksumsV1 extends MongoChecksums {
95
96
  { $sort: { _id: 1 } }
96
97
  ],
97
98
  {
98
- session: undefined,
99
- readConcern: 'snapshot',
99
+ ...context.readOptions,
100
100
  maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS
101
101
  }
102
102
  )
@@ -164,7 +164,8 @@ export class MongoChecksumsV1 extends MongoChecksums {
164
164
  }
165
165
 
166
166
  protected async fetchPreStates(
167
- batch: FetchPartialBucketChecksum[]
167
+ batch: FetchPartialBucketChecksum[],
168
+ context: MongoChecksumSessionContext
168
169
  ): Promise<Map<string, { opId: InternalOpId; checksum: BucketChecksum }>> {
169
170
  const preFilters = batch
170
171
  .filter((request) => request.start == null)
@@ -182,9 +183,14 @@ export class MongoChecksumsV1 extends MongoChecksums {
182
183
  }
183
184
 
184
185
  const states = await this.db.bucketStateV1
185
- .find({
186
- $or: preFilters
187
- })
186
+ .find(
187
+ {
188
+ $or: preFilters
189
+ },
190
+ {
191
+ ...context.readOptions
192
+ }
193
+ )
188
194
  .toArray();
189
195
 
190
196
  for (const state of states) {
@@ -202,7 +208,10 @@ export class MongoChecksumsV1 extends MongoChecksums {
202
208
  return preStates;
203
209
  }
204
210
 
205
- protected async computePartialChecksumsInternal(batch: FetchPartialBucketChecksum[]): Promise<PartialChecksumMap> {
206
- return this.computePartialChecksumsDirectByBucket(batch);
211
+ protected async computePartialChecksumsInternal(
212
+ batch: FetchPartialBucketChecksum[],
213
+ context: MongoChecksumSessionContext
214
+ ): Promise<PartialChecksumMap> {
215
+ return this.computePartialChecksumsDirectByBucket(batch, context);
207
216
  }
208
217
  }
@@ -64,7 +64,8 @@ export class MongoCompactorV1 extends MongoCompactor {
64
64
  buckets.map(({ bucket }) => ({
65
65
  bucket,
66
66
  end: this.maxOpId
67
- }))
67
+ })),
68
+ { readOptions: { readConcern: 'snapshot' } }
68
69
  );
69
70
  }
70
71
 
@@ -41,6 +41,7 @@ import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
41
41
  export interface MongoSyncBucketStorageContextV1 {
42
42
  db: VersionedPowerSyncMongoV1;
43
43
  replicationStreamId: number;
44
+ readPreference?: mongo.ReadPreference;
44
45
  }
45
46
 
46
47
  export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
@@ -172,6 +173,7 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
172
173
  protected createMongoChecksums(options: MongoSyncBucketStorageOptions): MongoChecksums {
173
174
  return new MongoChecksumsV1(this.db, this.replicationStreamId, {
174
175
  ...options.checksumOptions,
176
+ checksumCacheTtlMs: options.checksumCacheTtlMs,
175
177
  storageConfig: options?.storageConfig
176
178
  });
177
179
  }
@@ -190,7 +192,8 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
190
192
  protected get versionContext(): MongoSyncBucketStorageContextV1 {
191
193
  return {
192
194
  db: this.db,
193
- replicationStreamId: this.replicationStreamId
195
+ replicationStreamId: this.replicationStreamId,
196
+ readPreference: this.readPreference
194
197
  };
195
198
  }
196
199
 
@@ -203,7 +206,7 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
203
206
  }
204
207
 
205
208
  protected getBucketDataBatchImpl(
206
- checkpoint: utils.InternalOpId,
209
+ checkpoint: MongoSyncBucketStorageCheckpoint,
207
210
  dataBuckets: storage.BucketDataRequest[],
208
211
  options?: storage.BucketDataBatchOptions
209
212
  ): AsyncIterable<storage.SyncBucketDataChunk> {
@@ -369,20 +372,33 @@ export async function getParameterSetsV1(
369
372
 
370
373
  export async function* getBucketDataBatchV1(
371
374
  ctx: MongoSyncBucketStorageContextV1,
372
- checkpoint: utils.InternalOpId,
375
+ checkpoint: MongoSyncBucketStorageCheckpoint,
373
376
  dataBuckets: storage.BucketDataRequest[],
374
377
  options?: storage.BucketDataBatchOptions
375
378
  ): AsyncIterable<storage.SyncBucketDataChunk> {
376
379
  if (dataBuckets.length == 0) {
377
380
  return;
378
381
  }
382
+ const readPreference = options?.requestHint == 'bulk' ? ctx.readPreference : undefined;
383
+ const readConcern = ctx.readPreference == null ? undefined : 'majority';
384
+ const session =
385
+ readPreference == null || checkpoint.snapshotTime == null
386
+ ? undefined
387
+ : ctx.db.client.startSession({ causalConsistency: true });
388
+ await using _ = { [Symbol.asyncDispose]: async () => session?.endSession() };
389
+
390
+ if (session != null) {
391
+ session.advanceOperationTime(checkpoint.snapshotTime);
392
+ session.advanceClusterTime(checkpoint.clusterTime);
393
+ }
394
+
379
395
  let filters: mongo.Filter<BucketDataDocumentV1>[] = [];
380
396
  const bucketMap = new Map(dataBuckets.map((request) => [request.bucket, request.start]));
381
397
 
382
- if (checkpoint == null) {
398
+ if (checkpoint.checkpoint == null) {
383
399
  throw new Error('checkpoint is null');
384
400
  }
385
- const end = checkpoint;
401
+ const end = checkpoint.checkpoint;
386
402
  for (let { bucket: name, start } of dataBuckets) {
387
403
  filters.push({
388
404
  _id: {
@@ -410,7 +426,9 @@ export async function* getBucketDataBatchV1(
410
426
  $or: filters
411
427
  },
412
428
  {
413
- session: undefined,
429
+ session,
430
+ readPreference,
431
+ readConcern,
414
432
  sort: { _id: 1 },
415
433
  limit: batchLimit,
416
434
  batchSize: batchLimit + 1,
@@ -243,6 +243,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
243
243
 
244
244
  await this.flush(options);
245
245
 
246
+ using _ = this.tracer.span('storage', 'commit');
247
+
246
248
  const now = new Date();
247
249
 
248
250
  await this.db.write_checkpoints.updateMany(
@@ -418,6 +420,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
418
420
  }
419
421
 
420
422
  async setResumeLsn(lsn: string): Promise<void> {
423
+ using _ = this.tracer.span('storage', 'set_resume_lsn');
421
424
  await this.db.sync_rules.updateOne(
422
425
  {
423
426
  _id: this.replicationStreamId
@@ -427,7 +430,13 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
427
430
  resume_lsn: lsn
428
431
  }
429
432
  },
430
- { session: this.session }
433
+ {
434
+ session: this.session,
435
+ // Losing occasional resume LSN is fine. That may mean reprocessing
436
+ // some source changes in some edge cases, which is not an issue since
437
+ // changes are processed in an idempotent way.
438
+ writeConcern: { w: 1 }
439
+ }
431
440
  );
432
441
  }
433
442
 
@@ -438,7 +447,12 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
438
447
 
439
448
  const session = this.session;
440
449
  let activated = false;
450
+ let needsFutureActivationCheck = true;
441
451
  await session.withTransaction(async () => {
452
+ // Reset on transaction retries.
453
+ needsFutureActivationCheck = true;
454
+ activated = false;
455
+
442
456
  const doc = await this.db.sync_rules.findOne(
443
457
  {
444
458
  _id: this.replicationStreamId,
@@ -518,13 +532,17 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
518
532
  );
519
533
  activated = true;
520
534
  } else if (doc.state != storage.SyncRuleState.PROCESSING && doc.state != storage.SyncRuleState.ACTIVE) {
521
- this.needsActivationV3 = false;
535
+ needsFutureActivationCheck = false;
536
+ } else if (doc.state == storage.SyncRuleState.ACTIVE && processingStates.length == 0) {
537
+ needsFutureActivationCheck = false;
522
538
  }
523
539
  });
524
540
  if (activated) {
525
541
  this.logger.info(`Activated new replication stream at ${lsn}`);
526
542
  await this.db.notifyCheckpoint();
527
543
  this.needsActivationV3 = false;
544
+ } else if (!needsFutureActivationCheck) {
545
+ this.needsActivationV3 = false;
528
546
  }
529
547
  }
530
548
 
@@ -15,7 +15,8 @@ import {
15
15
  FetchPartialBucketChecksumByBucket,
16
16
  FetchPartialBucketChecksumByDefinition,
17
17
  MongoChecksumOptions,
18
- MongoChecksums
18
+ MongoChecksums,
19
+ MongoChecksumSessionContext
19
20
  } from '../MongoChecksums.js';
20
21
  import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
21
22
  import { BucketDataDocumentV3 } from './models.js';
@@ -66,7 +67,8 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
66
67
  for (const [definitionId, requests] of requestsByDefinition.entries()) {
67
68
  const groupResults = await this.computeChecksumsByDefinition(
68
69
  requests,
69
- this.db.bucketData(this.group_id, definitionId)
70
+ this.db.bucketData(this.group_id, definitionId),
71
+ { readOptions: { readConcern: 'snapshot' } }
70
72
  );
71
73
  for (const checksum of groupResults.values()) {
72
74
  results.set(checksum.bucket, checksum);
@@ -79,7 +81,8 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
79
81
  }
80
82
 
81
83
  protected async fetchPreStates(
82
- batch: FetchPartialBucketChecksum[]
84
+ batch: FetchPartialBucketChecksum[],
85
+ context: MongoChecksumSessionContext
83
86
  ): Promise<Map<string, { opId: InternalOpId; checksum: BucketChecksum }>> {
84
87
  const normalizedBatch = batch.map((request) => ({
85
88
  bucket: request.bucket,
@@ -105,9 +108,14 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
105
108
 
106
109
  const states = await this.db
107
110
  .bucketState(this.group_id)
108
- .find({
109
- $or: preFilters
110
- })
111
+ .find(
112
+ {
113
+ $or: preFilters
114
+ },
115
+ {
116
+ ...context.readOptions
117
+ }
118
+ )
111
119
  .toArray();
112
120
 
113
121
  for (const state of states) {
@@ -125,7 +133,10 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
125
133
  return preStates;
126
134
  }
127
135
 
128
- protected async computePartialChecksumsInternal(batch: FetchPartialBucketChecksum[]): Promise<PartialChecksumMap> {
136
+ protected async computePartialChecksumsInternal(
137
+ batch: FetchPartialBucketChecksum[],
138
+ context: MongoChecksumSessionContext
139
+ ): Promise<PartialChecksumMap> {
129
140
  const normalized = batch.map((request) => ({
130
141
  bucket: request.bucket,
131
142
  definitionId: this.syncConfigMapping().bucketSourceId(request.source),
@@ -145,7 +156,8 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
145
156
  for (const [definitionId, requests] of requestsByDefinition.entries()) {
146
157
  const groupResults = await this.computeChecksumsByDefinition(
147
158
  requests,
148
- this.db.bucketData(this.group_id, definitionId)
159
+ this.db.bucketData(this.group_id, definitionId),
160
+ context
149
161
  );
150
162
  for (const checksum of groupResults.values()) {
151
163
  results.set(checksum.bucket, checksum);
@@ -159,7 +171,8 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
159
171
 
160
172
  private async computeChecksumsByDefinition(
161
173
  batch: FetchPartialBucketChecksumByBucket[],
162
- collection: lib_mongo.mongo.Collection<BucketDataDocumentV3>
174
+ collection: lib_mongo.mongo.Collection<BucketDataDocumentV3>,
175
+ context: MongoChecksumSessionContext
163
176
  ): Promise<PartialChecksumMap> {
164
177
  const requests = new Map<string, FetchPartialBucketChecksumByBucket>();
165
178
  for (let request of batch) {
@@ -169,8 +182,7 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
169
182
  const pipeline = this.buildPartialChecksumPipeline(requests);
170
183
  const aggregate = await collection
171
184
  .aggregate(pipeline, {
172
- session: undefined,
173
- readConcern: 'snapshot',
185
+ ...context.readOptions,
174
186
  maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS
175
187
  })
176
188
  .toArray()
@@ -42,6 +42,7 @@ import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
42
42
  export interface MongoSyncBucketStorageContextV3 {
43
43
  db: VersionedPowerSyncMongoV3;
44
44
  replicationStreamId: number;
45
+ readPreference?: mongo.ReadPreference;
45
46
  /**
46
47
  * Persisted mapping of the single sync config that read operations are served from.
47
48
  *
@@ -171,6 +172,7 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
171
172
  protected createMongoChecksums(options: MongoSyncBucketStorageOptions): MongoChecksums {
172
173
  return new MongoChecksumsV3(this.db, this.replicationStreamId, {
173
174
  ...options.checksumOptions,
175
+ checksumCacheTtlMs: options.checksumCacheTtlMs,
174
176
  storageConfig: options?.storageConfig,
175
177
  syncConfigMapping: () => this.singleSyncConfigMapping()
176
178
  });
@@ -336,6 +338,7 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
336
338
  return {
337
339
  db: this.db,
338
340
  replicationStreamId: this.replicationStreamId,
341
+ readPreference: this.readPreference,
339
342
  get mapping() {
340
343
  return self.singleSyncConfigMapping();
341
344
  }
@@ -351,7 +354,7 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
351
354
  }
352
355
 
353
356
  protected getBucketDataBatchImpl(
354
- checkpoint: utils.InternalOpId,
357
+ checkpoint: MongoSyncBucketStorageCheckpoint,
355
358
  dataBuckets: storage.BucketDataRequest[],
356
359
  options?: storage.BucketDataBatchOptions
357
360
  ): AsyncIterable<storage.SyncBucketDataChunk> {
@@ -528,7 +531,7 @@ export async function getParameterSetsV3(
528
531
 
529
532
  export async function* getBucketDataBatchV3(
530
533
  ctx: MongoSyncBucketStorageContextV3,
531
- checkpoint: utils.InternalOpId,
534
+ checkpoint: MongoSyncBucketStorageCheckpoint,
532
535
  dataBuckets: storage.BucketDataRequest[],
533
536
  options?: storage.BucketDataBatchOptions
534
537
  ): AsyncIterable<storage.SyncBucketDataChunk> {
@@ -536,13 +539,26 @@ export async function* getBucketDataBatchV3(
536
539
  return;
537
540
  }
538
541
 
539
- if (checkpoint == null) {
542
+ if (checkpoint.checkpoint == null) {
540
543
  throw new Error('checkpoint is null');
541
544
  }
542
545
 
546
+ const readPreference = options?.requestHint == 'bulk' ? ctx.readPreference : undefined;
547
+ const readConcern = ctx.readPreference == null ? undefined : 'majority';
548
+ const session =
549
+ readPreference == null || checkpoint.snapshotTime == null
550
+ ? undefined
551
+ : ctx.db.client.startSession({ causalConsistency: true });
552
+ await using _ = { [Symbol.asyncDispose]: async () => session?.endSession() };
553
+
554
+ if (session != null) {
555
+ session.advanceOperationTime(checkpoint.snapshotTime);
556
+ session.advanceClusterTime(checkpoint.clusterTime);
557
+ }
558
+
543
559
  const batchLimit = options?.limit ?? storage.DEFAULT_DOCUMENT_BATCH_LIMIT;
544
560
  const chunkSizeLimitBytes = options?.chunkLimitBytes ?? storage.DEFAULT_DOCUMENT_CHUNK_LIMIT_BYTES;
545
- const end = checkpoint;
561
+ const end = checkpoint.checkpoint;
546
562
  let remainingLimit = batchLimit;
547
563
 
548
564
  const requestsByDefinition = new Map<string, storage.BucketDataRequest[]>();
@@ -580,7 +596,9 @@ export async function* getBucketDataBatchV3(
580
596
  // raw: true returns Buffers, but the driver typing doesn't reflect that
581
597
  // without an explicit cast to FindCursor<Buffer>.
582
598
  const cursor = collection.find(filter, {
583
- session: undefined,
599
+ session,
600
+ readPreference,
601
+ readConcern,
584
602
  sort: { _id: 1 },
585
603
  raw: true,
586
604
  maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS,
@@ -2,9 +2,25 @@ import * as lib_mongo from '@powersync/lib-service-mongodb';
2
2
  import * as service_types from '@powersync/service-types';
3
3
  import * as t from 'ts-codec';
4
4
 
5
+ export const MongoStorageReadPreference = t
6
+ .literal('primary')
7
+ .or(t.literal('primaryPreferred'))
8
+ .or(t.literal('secondary'))
9
+ .or(t.literal('secondaryPreferred'))
10
+ .or(t.literal('nearest'));
11
+
12
+ export type MongoStorageReadPreference = t.Encoded<typeof MongoStorageReadPreference>;
13
+
5
14
  export const MongoStorageConfig = lib_mongo.BaseMongoConfig.and(
6
15
  t.object({
7
- // Add any mongo specific storage settings here in future
16
+ /**
17
+ * Read preference for bulk checksum and bucket data reads.
18
+ *
19
+ * If unset, MongoDB driver defaults are used for backwards compatibility.
20
+ *
21
+ * This is an experimental option, and may be removed in a future release.
22
+ */
23
+ bulk_read_preference: MongoStorageReadPreference.optional()
8
24
  })
9
25
  );
10
26
 
@@ -95,7 +95,7 @@ bucket_definitions:
95
95
  const users = ['u1', 'u2'];
96
96
  const userRequests = users.map((user) => bucketRequest(syncRules, `by_user["${user}"]`));
97
97
  const [u1Request, u2Request] = userRequests;
98
- const checksumAfter = await bucketStorage.getChecksums(checkpoint, userRequests);
98
+ const checksumAfter = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint), userRequests);
99
99
  expect(checksumAfter.get(u1Request.bucket)).toEqual({
100
100
  bucket: u1Request.bucket,
101
101
  checksum: -659469718,
@@ -151,7 +151,7 @@ bucket_definitions:
151
151
  const users = ['u1', 'u2'];
152
152
  const userRequests = users.map((user) => bucketRequest(syncRulesContent, `by_user2["${user}"]`));
153
153
  const [u1Request, u2Request] = userRequests;
154
- const checksumAfter = await bucketStorage.getChecksums(checkpoint, userRequests);
154
+ const checksumAfter = await bucketStorage.getChecksums(test_utils.testCheckpoint(checkpoint), userRequests);
155
155
  expect(checksumAfter.get(u1Request.bucket)).toEqual({
156
156
  bucket: u1Request.bucket,
157
157
  checksum: -659469718,
@@ -869,7 +869,7 @@ bucket_definitions:
869
869
  } as any
870
870
  };
871
871
 
872
- const result = await bucketStorage.getChecksums(60n, [request]);
872
+ const result = await bucketStorage.getChecksums(test_utils.testCheckpoint(60n), [request]);
873
873
  const checksumResult = result.get(BUCKET)!;
874
874
 
875
875
  // The total checksum should be: compacted (ops 10,20,30) + partial (ops 40,50,60)
@@ -922,7 +922,7 @@ bucket_definitions:
922
922
  } as any
923
923
  };
924
924
 
925
- const result = await bucketStorage.getChecksums(45n, [request]);
925
+ const result = await bucketStorage.getChecksums(test_utils.testCheckpoint(45n), [request]);
926
926
  const checksumResult = result.get(BUCKET)!;
927
927
 
928
928
  // If createBucketFilter's _id.o <= 45 excludes this document,
@@ -192,7 +192,11 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
192
192
 
193
193
  const options: storage.BucketDataBatchOptions = {};
194
194
  const batch1 = await test_utils.fromAsync(
195
- bucketStorage.getBucketDataBatch(checkpoint, [bucketRequest(syncRulesContent, 'global[]', 0n)], options)
195
+ bucketStorage.getBucketDataBatch(
196
+ test_utils.testCheckpoint(checkpoint),
197
+ [bucketRequest(syncRulesContent, 'global[]', 0n)],
198
+ options
199
+ )
196
200
  );
197
201
  expect(test_utils.getBatchData(batch1)).toEqual([
198
202
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 },
@@ -206,7 +210,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
206
210
 
207
211
  const batch2 = await test_utils.fromAsync(
208
212
  bucketStorage.getBucketDataBatch(
209
- checkpoint,
213
+ test_utils.testCheckpoint(checkpoint),
210
214
  [bucketRequest(syncRulesContent, 'global[]', batch1[0].chunkData.next_after)],
211
215
  options
212
216
  )
@@ -222,7 +226,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
222
226
 
223
227
  const batch3 = await test_utils.fromAsync(
224
228
  bucketStorage.getBucketDataBatch(
225
- checkpoint,
229
+ test_utils.testCheckpoint(checkpoint),
226
230
  [bucketRequest(syncRulesContent, 'global[]', batch2[0].chunkData.next_after)],
227
231
  options
228
232
  )
@@ -1743,7 +1747,9 @@ describe('sync - mongodb', () => {
1743
1747
  async function getFilteredOps(start: number, checkpoint: number): Promise<bigint[]> {
1744
1748
  const { syncRules, bucketStorage } = await setupFilteringTest();
1745
1749
  const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', BigInt(start));
1746
- const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(BigInt(checkpoint), [request]));
1750
+ const batch = await test_utils.fromAsync(
1751
+ bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(BigInt(checkpoint)), [request])
1752
+ );
1747
1753
  const ops = batch.flatMap((b) => b.chunkData.data.map((d) => BigInt(d.op_id)));
1748
1754
  return ops;
1749
1755
  }
@@ -1888,7 +1894,9 @@ describe('sync - mongodb', () => {
1888
1894
  const roundRequests = requests
1889
1895
  .filter((request) => pending.has(request.bucket))
1890
1896
  .map((request) => ({ ...request, start: positions.get(request.bucket)! }));
1891
- const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(end, roundRequests));
1897
+ const batch = await test_utils.fromAsync(
1898
+ bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(end), roundRequests)
1899
+ );
1892
1900
  let anyHasMore = false;
1893
1901
  for (const { chunkData } of batch) {
1894
1902
  positions.set(chunkData.bucket, BigInt(chunkData.next_after));
@@ -131,7 +131,9 @@ function registerStoreCurrentDataTests(storageVersion: number) {
131
131
  const checkpoint = flushResult!.flushed_op;
132
132
 
133
133
  const batch = await test_utils.fromAsync(
134
- bucketStorage.getBucketDataBatch(checkpoint, [bucketRequest(syncRulesContent, 'global[]', 0n)])
134
+ bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [
135
+ bucketRequest(syncRulesContent, 'global[]', 0n)
136
+ ])
135
137
  );
136
138
  expect(test_utils.getBatchData(batch)).toMatchObject([{ op: 'PUT', object_id: 'test1' }]);
137
139
 
@@ -161,7 +163,9 @@ function registerStoreCurrentDataTests(storageVersion: number) {
161
163
  const checkpoint = flushResult!.flushed_op;
162
164
 
163
165
  const batch = await test_utils.fromAsync(
164
- bucketStorage.getBucketDataBatch(checkpoint, [bucketRequest(syncRulesContent, 'global[]', 0n)])
166
+ bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [
167
+ bucketRequest(syncRulesContent, 'global[]', 0n)
168
+ ])
165
169
  );
166
170
  expect(test_utils.getBatchData(batch)).toMatchObject([{ op: 'PUT', object_id: 'test1' }]);
167
171
 
@@ -200,7 +204,9 @@ function registerStoreCurrentDataTests(storageVersion: number) {
200
204
  const checkpoint = flushResult!.flushed_op;
201
205
 
202
206
  const batch = await test_utils.fromAsync(
203
- bucketStorage.getBucketDataBatch(checkpoint, [bucketRequest(syncRulesContent, 'global[]', 0n)])
207
+ bucketStorage.getBucketDataBatch(test_utils.testCheckpoint(checkpoint), [
208
+ bucketRequest(syncRulesContent, 'global[]', 0n)
209
+ ])
204
210
  );
205
211
  const data = test_utils.getBatchData(batch);
206
212
  expect(data.length).toBeGreaterThan(0);