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

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 (67) hide show
  1. package/CHANGELOG.md +20 -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 +29 -4
  9. package/dist/storage/implementation/MongoChecksums.js +32 -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 +7 -4
  18. package/dist/storage/implementation/MongoSyncBucketStorage.js +11 -4
  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/db.js +8 -1
  24. package/dist/storage/implementation/db.js.map +1 -1
  25. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +17 -2
  26. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  27. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +4 -4
  28. package/dist/storage/implementation/v1/MongoChecksumsV1.js +7 -6
  29. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  30. package/dist/storage/implementation/v1/MongoCompactorV1.js +1 -1
  31. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  32. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +4 -3
  33. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +17 -4
  34. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  35. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +20 -2
  36. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  37. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +3 -3
  38. package/dist/storage/implementation/v3/MongoChecksumsV3.js +8 -7
  39. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  40. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +4 -3
  41. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +16 -3
  42. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  43. package/dist/types/types.d.ts +12 -1
  44. package/dist/types/types.js +14 -1
  45. package/dist/types/types.js.map +1 -1
  46. package/package.json +7 -7
  47. package/src/storage/MongoBucketStorage.ts +4 -0
  48. package/src/storage/implementation/MongoBucketBatch.ts +23 -13
  49. package/src/storage/implementation/MongoChecksums.ts +84 -15
  50. package/src/storage/implementation/MongoCompactor.ts +1 -1
  51. package/src/storage/implementation/MongoParameterCompactor.ts +1 -1
  52. package/src/storage/implementation/MongoStorageProvider.ts +10 -0
  53. package/src/storage/implementation/MongoSyncBucketStorage.ts +18 -8
  54. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +62 -15
  55. package/src/storage/implementation/db.ts +8 -1
  56. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +18 -2
  57. package/src/storage/implementation/v1/MongoChecksumsV1.ts +20 -11
  58. package/src/storage/implementation/v1/MongoCompactorV1.ts +2 -1
  59. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +23 -6
  60. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +20 -2
  61. package/src/storage/implementation/v3/MongoChecksumsV3.ts +23 -11
  62. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +22 -5
  63. package/src/types/types.ts +17 -1
  64. package/test/src/storage_compacting.test.ts +4 -4
  65. package/test/src/storage_sync.test.ts +13 -5
  66. package/test/src/storeCurrentData.test.ts +9 -3
  67. package/tsconfig.tsbuildinfo +1 -1
@@ -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,32 @@ 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
+ }
393
+
379
394
  let filters: mongo.Filter<BucketDataDocumentV1>[] = [];
380
395
  const bucketMap = new Map(dataBuckets.map((request) => [request.bucket, request.start]));
381
396
 
382
- if (checkpoint == null) {
397
+ if (checkpoint.checkpoint == null) {
383
398
  throw new Error('checkpoint is null');
384
399
  }
385
- const end = checkpoint;
400
+ const end = checkpoint.checkpoint;
386
401
  for (let { bucket: name, start } of dataBuckets) {
387
402
  filters.push({
388
403
  _id: {
@@ -410,7 +425,9 @@ export async function* getBucketDataBatchV1(
410
425
  $or: filters
411
426
  },
412
427
  {
413
- session: undefined,
428
+ session,
429
+ readPreference,
430
+ readConcern,
414
431
  sort: { _id: 1 },
415
432
  limit: batchLimit,
416
433
  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,25 @@ 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
+ }
557
+
543
558
  const batchLimit = options?.limit ?? storage.DEFAULT_DOCUMENT_BATCH_LIMIT;
544
559
  const chunkSizeLimitBytes = options?.chunkLimitBytes ?? storage.DEFAULT_DOCUMENT_CHUNK_LIMIT_BYTES;
545
- const end = checkpoint;
560
+ const end = checkpoint.checkpoint;
546
561
  let remainingLimit = batchLimit;
547
562
 
548
563
  const requestsByDefinition = new Map<string, storage.BucketDataRequest[]>();
@@ -580,7 +595,9 @@ export async function* getBucketDataBatchV3(
580
595
  // raw: true returns Buffers, but the driver typing doesn't reflect that
581
596
  // without an explicit cast to FindCursor<Buffer>.
582
597
  const cursor = collection.find(filter, {
583
- session: undefined,
598
+ session,
599
+ readPreference,
600
+ readConcern,
584
601
  sort: { _id: 1 },
585
602
  raw: true,
586
603
  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);