@powersync/service-module-mongodb-storage 0.0.0-dev-20250827091123 → 0.0.0-dev-20250828134335

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 +28 -13
  2. package/dist/index.d.ts +0 -1
  3. package/dist/index.js +0 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/storage/MongoBucketStorage.js +1 -1
  6. package/dist/storage/MongoBucketStorage.js.map +1 -1
  7. package/dist/storage/implementation/MongoBucketBatch.d.ts +1 -1
  8. package/dist/storage/implementation/MongoBucketBatch.js +7 -4
  9. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  10. package/dist/storage/implementation/MongoCompactor.d.ts +16 -2
  11. package/dist/storage/implementation/MongoCompactor.js +204 -48
  12. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  13. package/dist/storage/implementation/MongoStorageProvider.d.ts +1 -1
  14. package/dist/storage/implementation/MongoStorageProvider.js +3 -7
  15. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  16. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +12 -1
  17. package/dist/storage/implementation/MongoSyncBucketStorage.js +196 -37
  18. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  19. package/dist/storage/implementation/MongoTestStorageFactoryGenerator.d.ts +7 -0
  20. package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js +18 -0
  21. package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js.map +1 -0
  22. package/dist/storage/implementation/PersistedBatch.d.ts +1 -0
  23. package/dist/storage/implementation/PersistedBatch.js +13 -6
  24. package/dist/storage/implementation/PersistedBatch.js.map +1 -1
  25. package/dist/storage/implementation/db.d.ts +1 -6
  26. package/dist/storage/implementation/db.js +0 -16
  27. package/dist/storage/implementation/db.js.map +1 -1
  28. package/dist/storage/implementation/models.d.ts +14 -3
  29. package/dist/{utils → storage/implementation}/util.d.ts +35 -3
  30. package/dist/{utils → storage/implementation}/util.js +54 -0
  31. package/dist/storage/implementation/util.js.map +1 -0
  32. package/dist/storage/storage-index.d.ts +2 -3
  33. package/dist/storage/storage-index.js +2 -3
  34. package/dist/storage/storage-index.js.map +1 -1
  35. package/package.json +8 -8
  36. package/src/index.ts +0 -1
  37. package/src/storage/MongoBucketStorage.ts +1 -1
  38. package/src/storage/implementation/MongoBucketBatch.ts +8 -6
  39. package/src/storage/implementation/MongoCompactor.ts +239 -49
  40. package/src/storage/implementation/MongoStorageProvider.ts +4 -9
  41. package/src/storage/implementation/MongoSyncBucketStorage.ts +242 -38
  42. package/src/storage/implementation/MongoTestStorageFactoryGenerator.ts +28 -0
  43. package/src/storage/implementation/PersistedBatch.ts +14 -6
  44. package/src/storage/implementation/db.ts +0 -18
  45. package/src/storage/implementation/models.ts +15 -3
  46. package/src/{utils → storage/implementation}/util.ts +61 -3
  47. package/src/storage/storage-index.ts +2 -3
  48. package/test/src/__snapshots__/storage_sync.test.ts.snap +110 -0
  49. package/test/src/util.ts +2 -6
  50. package/tsconfig.tsbuildinfo +1 -1
  51. package/dist/migrations/db/migrations/1752661449910-connection-reporting.d.ts +0 -3
  52. package/dist/migrations/db/migrations/1752661449910-connection-reporting.js +0 -36
  53. package/dist/migrations/db/migrations/1752661449910-connection-reporting.js.map +0 -1
  54. package/dist/storage/MongoReportStorage.d.ts +0 -18
  55. package/dist/storage/MongoReportStorage.js +0 -154
  56. package/dist/storage/MongoReportStorage.js.map +0 -1
  57. package/dist/utils/test-utils.d.ts +0 -11
  58. package/dist/utils/test-utils.js +0 -40
  59. package/dist/utils/test-utils.js.map +0 -1
  60. package/dist/utils/util.js.map +0 -1
  61. package/dist/utils/utils-index.d.ts +0 -2
  62. package/dist/utils/utils-index.js +0 -3
  63. package/dist/utils/utils-index.js.map +0 -1
  64. package/src/migrations/db/migrations/1752661449910-connection-reporting.ts +0 -58
  65. package/src/storage/MongoReportStorage.ts +0 -177
  66. package/src/utils/test-utils.ts +0 -55
  67. package/src/utils/utils-index.ts +0 -2
  68. package/test/src/__snapshots__/connection-report-storage.test.ts.snap +0 -215
  69. package/test/src/connection-report-storage.test.ts +0 -133
@@ -2,19 +2,28 @@ import * as lib_mongo from '@powersync/lib-service-mongodb';
2
2
  import { mongo } from '@powersync/lib-service-mongodb';
3
3
  import {
4
4
  BaseObserver,
5
+ DatabaseQueryError,
6
+ ErrorCode,
5
7
  logger,
6
8
  ReplicationAbortedError,
7
9
  ServiceAssertionError
8
10
  } from '@powersync/lib-services-framework';
9
11
  import {
12
+ addBucketChecksums,
13
+ addPartialChecksums,
10
14
  BroadcastIterable,
15
+ BucketChecksum,
11
16
  CHECKPOINT_INVALIDATE_ALL,
12
17
  CheckpointChanges,
18
+ CompactOptions,
13
19
  deserializeParameterLookup,
14
20
  GetCheckpointChangesOptions,
15
21
  InternalOpId,
16
22
  internalToExternalOpId,
23
+ isPartialChecksum,
17
24
  maxLsn,
25
+ PartialChecksum,
26
+ PartialOrFullChecksum,
18
27
  ProtocolOpId,
19
28
  ReplicationCheckpoint,
20
29
  storage,
@@ -31,9 +40,16 @@ import { PowerSyncMongo } from './db.js';
31
40
  import { BucketDataDocument, BucketDataKey, BucketStateDocument, SourceKey, SourceTableDocument } from './models.js';
32
41
  import { MongoBucketBatch } from './MongoBucketBatch.js';
33
42
  import { MongoCompactor } from './MongoCompactor.js';
34
- import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
35
- import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '../../utils/util.js';
36
43
  import { MongoParameterCompactor } from './MongoParameterCompactor.js';
44
+ import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
45
+ import {
46
+ CHECKSUM_QUERY_GROUP_STAGE,
47
+ checksumFromAggregate,
48
+ idPrefixFilter,
49
+ mapOpEntry,
50
+ readSingleBatch,
51
+ setSessionSnapshotTime
52
+ } from './util.js';
37
53
 
38
54
  export class MongoSyncBucketStorage
39
55
  extends BaseObserver<storage.SyncRulesBucketStorageListener>
@@ -490,11 +506,96 @@ export class MongoSyncBucketStorage
490
506
  return this.checksumCache.getChecksumMap(checkpoint, buckets);
491
507
  }
492
508
 
509
+ clearChecksumCache() {
510
+ this.checksumCache.clear();
511
+ }
512
+
493
513
  private async getChecksumsInternal(batch: storage.FetchPartialBucketChecksum[]): Promise<storage.PartialChecksumMap> {
494
514
  if (batch.length == 0) {
495
515
  return new Map();
496
516
  }
497
517
 
518
+ const preFilters: any[] = [];
519
+ for (let request of batch) {
520
+ if (request.start == null) {
521
+ preFilters.push({
522
+ _id: {
523
+ g: this.group_id,
524
+ b: request.bucket
525
+ },
526
+ 'compacted_state.op_id': { $exists: true, $lte: request.end }
527
+ });
528
+ }
529
+ }
530
+
531
+ const preStates = new Map<string, { opId: InternalOpId; checksum: BucketChecksum }>();
532
+
533
+ if (preFilters.length > 0) {
534
+ // For un-cached bucket checksums, attempt to use the compacted state first.
535
+ const states = await this.db.bucket_state
536
+ .find({
537
+ $or: preFilters
538
+ })
539
+ .toArray();
540
+ for (let state of states) {
541
+ const compactedState = state.compacted_state!;
542
+ preStates.set(state._id.b, {
543
+ opId: compactedState.op_id,
544
+ checksum: {
545
+ bucket: state._id.b,
546
+ checksum: Number(compactedState.checksum),
547
+ count: compactedState.count
548
+ }
549
+ });
550
+ }
551
+ }
552
+
553
+ const mappedRequests = batch.map((request) => {
554
+ let start = request.start;
555
+ if (start == null) {
556
+ const preState = preStates.get(request.bucket);
557
+ if (preState != null) {
558
+ start = preState.opId;
559
+ }
560
+ }
561
+ return {
562
+ ...request,
563
+ start
564
+ };
565
+ });
566
+
567
+ const queriedChecksums = await this.queryPartialChecksums(mappedRequests);
568
+
569
+ return new Map<string, storage.PartialOrFullChecksum>(
570
+ batch.map((request) => {
571
+ const bucket = request.bucket;
572
+ // Could be null if this is either (1) a partial request, or (2) no compacted checksum was available
573
+ const preState = preStates.get(bucket);
574
+ // Could be null if we got no data
575
+ const partialChecksum = queriedChecksums.get(bucket);
576
+ const merged = addPartialChecksums(bucket, preState?.checksum ?? null, partialChecksum ?? null);
577
+
578
+ return [bucket, merged];
579
+ })
580
+ );
581
+ }
582
+
583
+ async queryPartialChecksums(batch: storage.FetchPartialBucketChecksum[]): Promise<storage.PartialChecksumMap> {
584
+ try {
585
+ return await this.queryPartialChecksumsInternal(batch);
586
+ } catch (e) {
587
+ if (e.codeName == 'MaxTimeMSExpired') {
588
+ logger.warn(`Checksum query timed out; falling back to slower version`, e);
589
+ // Timeout - try the slower but more robust version
590
+ return await this.queryPartialChecksumsFallback(batch);
591
+ }
592
+ throw lib_mongo.mapQueryError(e, 'while reading checksums');
593
+ }
594
+ }
595
+
596
+ private async queryPartialChecksumsInternal(
597
+ batch: storage.FetchPartialBucketChecksum[]
598
+ ): Promise<storage.PartialChecksumMap> {
498
599
  const filters: any[] = [];
499
600
  for (let request of batch) {
500
601
  filters.push({
@@ -502,12 +603,12 @@ export class MongoSyncBucketStorage
502
603
  $gt: {
503
604
  g: this.group_id,
504
605
  b: request.bucket,
505
- o: request.start ? BigInt(request.start) : new bson.MinKey()
606
+ o: request.start ?? new bson.MinKey()
506
607
  },
507
608
  $lte: {
508
609
  g: this.group_id,
509
610
  b: request.bucket,
510
- o: BigInt(request.end)
611
+ o: request.end
511
612
  }
512
613
  }
513
614
  });
@@ -521,44 +622,126 @@ export class MongoSyncBucketStorage
521
622
  $or: filters
522
623
  }
523
624
  },
524
- {
525
- $group: {
526
- _id: '$_id.b',
527
- // Historically, checksum may be stored as 'int' or 'double'.
528
- // More recently, this should be a 'long'.
529
- // $toLong ensures that we always sum it as a long, avoiding inaccuracies in the calculations.
530
- checksum_total: { $sum: { $toLong: '$checksum' } },
531
- count: { $sum: 1 },
532
- has_clear_op: {
533
- $max: {
534
- $cond: [{ $eq: ['$op', 'CLEAR'] }, 1, 0]
535
- }
536
- }
537
- }
538
- }
625
+ CHECKSUM_QUERY_GROUP_STAGE
539
626
  ],
540
- { session: undefined, readConcern: 'snapshot', maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS }
627
+ { session: undefined, readConcern: 'snapshot', maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS }
541
628
  )
542
- .toArray()
543
- .catch((e) => {
544
- throw lib_mongo.mapQueryError(e, 'while reading checksums');
545
- });
629
+ // Don't map the error here - we want to keep timeout errors as-is
630
+ .toArray();
546
631
 
547
- return new Map<string, storage.PartialChecksum>(
632
+ const partialChecksums = new Map<string, storage.PartialOrFullChecksum>(
548
633
  aggregate.map((doc) => {
549
- return [
550
- doc._id,
551
- {
552
- bucket: doc._id,
553
- partialCount: doc.count,
554
- partialChecksum: Number(BigInt(doc.checksum_total) & 0xffffffffn) & 0xffffffff,
555
- isFullChecksum: doc.has_clear_op == 1
556
- } satisfies storage.PartialChecksum
557
- ];
634
+ const bucket = doc._id;
635
+ return [bucket, checksumFromAggregate(doc)];
636
+ })
637
+ );
638
+
639
+ return new Map<string, storage.PartialOrFullChecksum>(
640
+ batch.map((request) => {
641
+ const bucket = request.bucket;
642
+ // Could be null if we got no data
643
+ let partialChecksum = partialChecksums.get(bucket);
644
+ if (partialChecksum == null) {
645
+ partialChecksum = {
646
+ bucket,
647
+ partialCount: 0,
648
+ partialChecksum: 0
649
+ };
650
+ }
651
+ if (request.start == null && isPartialChecksum(partialChecksum)) {
652
+ partialChecksum = {
653
+ bucket,
654
+ count: partialChecksum.partialCount,
655
+ checksum: partialChecksum.partialChecksum
656
+ };
657
+ }
658
+
659
+ return [bucket, partialChecksum];
558
660
  })
559
661
  );
560
662
  }
561
663
 
664
+ /**
665
+ * Checksums for large buckets can run over the query timeout.
666
+ * To avoid this, we query in batches.
667
+ * This version can handle larger amounts of data, but is slower, especially for many buckets.
668
+ */
669
+ async queryPartialChecksumsFallback(
670
+ batch: storage.FetchPartialBucketChecksum[]
671
+ ): Promise<storage.PartialChecksumMap> {
672
+ const partialChecksums = new Map<string, storage.PartialOrFullChecksum>();
673
+ for (let request of batch) {
674
+ const checksum = await this.slowChecksum(request);
675
+ partialChecksums.set(request.bucket, checksum);
676
+ }
677
+
678
+ return partialChecksums;
679
+ }
680
+
681
+ private async slowChecksum(request: storage.FetchPartialBucketChecksum): Promise<PartialOrFullChecksum> {
682
+ const batchLimit = 50_000;
683
+
684
+ let lowerBound = 0n;
685
+ const bucket = request.bucket;
686
+
687
+ let runningChecksum: PartialOrFullChecksum = {
688
+ bucket,
689
+ partialCount: 0,
690
+ partialChecksum: 0
691
+ };
692
+ if (request.start == null) {
693
+ runningChecksum = {
694
+ bucket,
695
+ count: 0,
696
+ checksum: 0
697
+ };
698
+ }
699
+
700
+ while (true) {
701
+ const filter = {
702
+ _id: {
703
+ $gt: {
704
+ g: this.group_id,
705
+ b: bucket,
706
+ o: lowerBound
707
+ },
708
+ $lte: {
709
+ g: this.group_id,
710
+ b: bucket,
711
+ o: request.end
712
+ }
713
+ }
714
+ };
715
+ const docs = await this.db.bucket_data
716
+ .aggregate(
717
+ [
718
+ {
719
+ $match: filter
720
+ },
721
+ // sort and limit _before_ grouping
722
+ { $sort: { _id: 1 } },
723
+ { $limit: batchLimit },
724
+ CHECKSUM_QUERY_GROUP_STAGE
725
+ ],
726
+ { session: undefined, readConcern: 'snapshot', maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS }
727
+ )
728
+ .toArray();
729
+ const doc = docs[0];
730
+ if (doc == null) {
731
+ return runningChecksum;
732
+ }
733
+ const partial = checksumFromAggregate(doc);
734
+ runningChecksum = addPartialChecksums(bucket, runningChecksum, partial);
735
+ const isFinal = doc.count != batchLimit;
736
+ if (isFinal) {
737
+ break;
738
+ } else {
739
+ lowerBound = doc.last_op;
740
+ }
741
+ }
742
+ return runningChecksum;
743
+ }
744
+
562
745
  async terminate(options?: storage.TerminateOptions) {
563
746
  // Default is to clear the storage except when explicitly requested not to.
564
747
  if (!options || options?.clearStorage) {
@@ -701,11 +884,32 @@ export class MongoSyncBucketStorage
701
884
  }
702
885
 
703
886
  async compact(options?: storage.CompactOptions) {
704
- const checkpoint = await this.getCheckpointInternal();
705
- await new MongoCompactor(this.db, this.group_id, options).compact();
706
- if (checkpoint != null && options?.compactParameterData) {
707
- await new MongoParameterCompactor(this.db, this.group_id, checkpoint.checkpoint, options).compact();
887
+ let maxOpId = options?.maxOpId;
888
+ if (maxOpId == null) {
889
+ const checkpoint = await this.getCheckpointInternal();
890
+ maxOpId = checkpoint?.checkpoint ?? undefined;
708
891
  }
892
+ await new MongoCompactor(this, this.db, { ...options, maxOpId }).compact();
893
+
894
+ if (maxOpId != null && options?.compactParameterData) {
895
+ await new MongoParameterCompactor(this.db, this.group_id, maxOpId, options).compact();
896
+ }
897
+ }
898
+
899
+ async populatePersistentChecksumCache(options: Required<Pick<CompactOptions, 'signal' | 'maxOpId'>>): Promise<void> {
900
+ logger.info(`Populating persistent checksum cache...`);
901
+ const start = Date.now();
902
+ // We do a minimal compact here.
903
+ // We can optimize this in the future.
904
+ const compactor = new MongoCompactor(this, this.db, {
905
+ ...options,
906
+ // Don't track updates for MOVE compacting
907
+ memoryLimitMB: 0
908
+ });
909
+
910
+ await compactor.populateChecksums();
911
+ const duration = Date.now() - start;
912
+ logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
709
913
  }
710
914
 
711
915
  /**
@@ -0,0 +1,28 @@
1
+ import { TestStorageOptions } from '@powersync/service-core';
2
+ import { MongoBucketStorage } from '../MongoBucketStorage.js';
3
+ import { connectMongoForTests } from './util.js';
4
+
5
+ export type MongoTestStorageOptions = {
6
+ url: string;
7
+ isCI: boolean;
8
+ };
9
+
10
+ export const MongoTestStorageFactoryGenerator = (factoryOptions: MongoTestStorageOptions) => {
11
+ return async (options?: TestStorageOptions) => {
12
+ const db = connectMongoForTests(factoryOptions.url, factoryOptions.isCI);
13
+
14
+ // None of the tests insert data into this collection, so it was never created
15
+ if (!(await db.db.listCollections({ name: db.bucket_parameters.collectionName }).hasNext())) {
16
+ await db.db.createCollection('bucket_parameters');
17
+ }
18
+
19
+ // Full migrations are not currently run for tests, so we manually create this
20
+ await db.createCheckpointEventsCollection();
21
+
22
+ if (!options?.doNotClear) {
23
+ await db.clear();
24
+ }
25
+
26
+ return new MongoBucketStorage(db, { slot_name_prefix: 'test_' });
27
+ };
28
+ };
@@ -16,7 +16,7 @@ import {
16
16
  CurrentDataDocument,
17
17
  SourceKey
18
18
  } from './models.js';
19
- import { replicaIdToSubkey } from '../../utils/util.js';
19
+ import { replicaIdToSubkey } from './util.js';
20
20
 
21
21
  /**
22
22
  * Maximum size of operations we write in a single transaction.
@@ -71,15 +71,17 @@ export class PersistedBatch {
71
71
  this.logger = options?.logger ?? defaultLogger;
72
72
  }
73
73
 
74
- private incrementBucket(bucket: string, op_id: InternalOpId) {
74
+ private incrementBucket(bucket: string, op_id: InternalOpId, bytes: number) {
75
75
  let existingState = this.bucketStates.get(bucket);
76
76
  if (existingState) {
77
77
  existingState.lastOp = op_id;
78
78
  existingState.incrementCount += 1;
79
+ existingState.incrementBytes += bytes;
79
80
  } else {
80
81
  this.bucketStates.set(bucket, {
81
82
  lastOp: op_id,
82
- incrementCount: 1
83
+ incrementCount: 1,
84
+ incrementBytes: bytes
83
85
  });
84
86
  }
85
87
  }
@@ -115,7 +117,8 @@ export class PersistedBatch {
115
117
  }
116
118
 
117
119
  remaining_buckets.delete(key);
118
- this.currentSize += recordData.length + 200;
120
+ const byteEstimate = recordData.length + 200;
121
+ this.currentSize += byteEstimate;
119
122
 
120
123
  const op_id = options.op_seq.next();
121
124
  this.debugLastOpId = op_id;
@@ -138,7 +141,7 @@ export class PersistedBatch {
138
141
  }
139
142
  }
140
143
  });
141
- this.incrementBucket(k.bucket, op_id);
144
+ this.incrementBucket(k.bucket, op_id, byteEstimate);
142
145
  }
143
146
 
144
147
  for (let bd of remaining_buckets.values()) {
@@ -166,7 +169,7 @@ export class PersistedBatch {
166
169
  }
167
170
  });
168
171
  this.currentSize += 200;
169
- this.incrementBucket(bd.bucket, op_id);
172
+ this.incrementBucket(bd.bucket, op_id, 200);
170
173
  }
171
174
  }
172
175
 
@@ -369,6 +372,10 @@ export class PersistedBatch {
369
372
  update: {
370
373
  $set: {
371
374
  last_op: state.lastOp
375
+ },
376
+ $inc: {
377
+ 'estimate_since_compact.count': state.incrementCount,
378
+ 'estimate_since_compact.bytes': state.incrementBytes
372
379
  }
373
380
  },
374
381
  upsert: true
@@ -381,4 +388,5 @@ export class PersistedBatch {
381
388
  interface BucketStateUpdate {
382
389
  lastOp: InternalOpId;
383
390
  incrementCount: number;
391
+ incrementBytes: number;
384
392
  }
@@ -8,7 +8,6 @@ import {
8
8
  BucketParameterDocument,
9
9
  BucketStateDocument,
10
10
  CheckpointEventDocument,
11
- ClientConnectionDocument,
12
11
  CurrentDataDocument,
13
12
  CustomWriteCheckpointDocument,
14
13
  IdSequenceDocument,
@@ -38,7 +37,6 @@ export class PowerSyncMongo {
38
37
  readonly locks: mongo.Collection<lib_mongo.locks.Lock>;
39
38
  readonly bucket_state: mongo.Collection<BucketStateDocument>;
40
39
  readonly checkpoint_events: mongo.Collection<CheckpointEventDocument>;
41
- readonly connection_report_events: mongo.Collection<ClientConnectionDocument>;
42
40
 
43
41
  readonly client: mongo.MongoClient;
44
42
  readonly db: mongo.Db;
@@ -63,7 +61,6 @@ export class PowerSyncMongo {
63
61
  this.locks = this.db.collection('locks');
64
62
  this.bucket_state = this.db.collection('bucket_state');
65
63
  this.checkpoint_events = this.db.collection('checkpoint_events');
66
- this.connection_report_events = this.db.collection('connection_report_events');
67
64
  }
68
65
 
69
66
  /**
@@ -81,7 +78,6 @@ export class PowerSyncMongo {
81
78
  await this.locks.deleteMany({});
82
79
  await this.bucket_state.deleteMany({});
83
80
  await this.custom_write_checkpoints.deleteMany({});
84
- await this.connection_report_events.deleteMany({});
85
81
  }
86
82
 
87
83
  /**
@@ -131,20 +127,6 @@ export class PowerSyncMongo {
131
127
  max: 50 // max number of documents
132
128
  });
133
129
  }
134
-
135
- /**
136
- * Only use in migrations and tests.
137
- */
138
- async createConnectionReportingCollection() {
139
- const existingCollections = await this.db
140
- .listCollections({ name: 'connection_report_events' }, { nameOnly: false })
141
- .toArray();
142
- const collection = existingCollections[0];
143
- if (collection != null) {
144
- return;
145
- }
146
- await this.db.createCollection('connection_report_events');
147
- }
148
130
  }
149
131
 
150
132
  export function createPowerSyncMongo(config: MongoStorageConfig, options?: lib_mongo.MongoConnectionOptions) {
@@ -1,7 +1,6 @@
1
1
  import { InternalOpId, storage } from '@powersync/service-core';
2
2
  import { SqliteJsonValue } from '@powersync/service-sync-rules';
3
3
  import * as bson from 'bson';
4
- import { event_types } from '@powersync/service-types';
5
4
 
6
5
  /**
7
6
  * Replica id uniquely identifying a row on the source database.
@@ -99,6 +98,21 @@ export interface BucketStateDocument {
99
98
  b: string;
100
99
  };
101
100
  last_op: bigint;
101
+ /**
102
+ * If set, this can be treated as "cache" of a checksum at a specific point.
103
+ * Can be updated periodically, for example by the compact job.
104
+ */
105
+ compacted_state?: {
106
+ op_id: InternalOpId;
107
+ count: number;
108
+ checksum: bigint;
109
+ bytes: number;
110
+ };
111
+
112
+ estimate_since_compact?: {
113
+ count: number;
114
+ bytes: number;
115
+ };
102
116
  }
103
117
 
104
118
  export interface IdSequenceDocument {
@@ -220,5 +234,3 @@ export interface InstanceDocument {
220
234
  // The instance UUID
221
235
  _id: string;
222
236
  }
223
-
224
- export interface ClientConnectionDocument extends event_types.ClientConnection {}
@@ -1,9 +1,12 @@
1
1
  import * as bson from 'bson';
2
2
  import * as crypto from 'crypto';
3
3
  import * as uuid from 'uuid';
4
+
4
5
  import { mongo } from '@powersync/lib-service-mongodb';
5
- import { storage, utils } from '@powersync/service-core';
6
- import { BucketDataDocument } from '../storage/implementation/models.js';
6
+ import { BucketChecksum, PartialChecksum, PartialOrFullChecksum, storage, utils } from '@powersync/service-core';
7
+
8
+ import { PowerSyncMongo } from './db.js';
9
+ import { BucketDataDocument } from './models.js';
7
10
  import { ServiceAssertionError } from '@powersync/lib-services-framework';
8
11
 
9
12
  export function idPrefixFilter<T>(prefix: Partial<T>, rest: (keyof T)[]): mongo.Condition<T> {
@@ -40,7 +43,7 @@ export function generateSlotName(prefix: string, sync_rules_id: number) {
40
43
  *
41
44
  * For this to be effective, set batchSize = limit in the find command.
42
45
  */
43
- export async function readSingleBatch<T>(cursor: mongo.FindCursor<T>): Promise<{ data: T[]; hasMore: boolean }> {
46
+ export async function readSingleBatch<T>(cursor: mongo.AbstractCursor<T>): Promise<{ data: T[]; hasMore: boolean }> {
44
47
  try {
45
48
  let data: T[];
46
49
  let hasMore = true;
@@ -102,6 +105,20 @@ export function replicaIdToSubkey(table: bson.ObjectId, id: storage.ReplicaId):
102
105
  }
103
106
  }
104
107
 
108
+ /**
109
+ * Helper for unit tests
110
+ */
111
+ export const connectMongoForTests = (url: string, isCI: boolean) => {
112
+ // Short timeout for tests, to fail fast when the server is not available.
113
+ // Slightly longer timeouts for CI, to avoid arbitrary test failures
114
+ const client = new mongo.MongoClient(url, {
115
+ connectTimeoutMS: isCI ? 15_000 : 5_000,
116
+ socketTimeoutMS: isCI ? 15_000 : 5_000,
117
+ serverSelectionTimeoutMS: isCI ? 15_000 : 2_500
118
+ });
119
+ return new PowerSyncMongo(client);
120
+ };
121
+
105
122
  export function setSessionSnapshotTime(session: mongo.ClientSession, time: bson.Timestamp) {
106
123
  // This is a workaround for the lack of direct support for snapshot reads in the MongoDB driver.
107
124
  if (!session.snapshotEnabled) {
@@ -113,3 +130,44 @@ export function setSessionSnapshotTime(session: mongo.ClientSession, time: bson.
113
130
  throw new ServiceAssertionError(`Session snapshotTime is already set`);
114
131
  }
115
132
  }
133
+
134
+ export const CHECKSUM_QUERY_GROUP_STAGE = {
135
+ $group: {
136
+ _id: '$_id.b',
137
+ // Historically, checksum may be stored as 'int' or 'double'.
138
+ // More recently, this should be a 'long'.
139
+ // $toLong ensures that we always sum it as a long, avoiding inaccuracies in the calculations.
140
+ checksum_total: { $sum: { $toLong: '$checksum' } },
141
+ count: { $sum: 1 },
142
+ has_clear_op: {
143
+ $max: {
144
+ $cond: [{ $eq: ['$op', 'CLEAR'] }, 1, 0]
145
+ }
146
+ },
147
+ last_op: { $max: '$_id.o' }
148
+ }
149
+ };
150
+
151
+ /**
152
+ * Convert output of CHECKSUM_QUERY_GROUP_STAGE into a checksum.
153
+ */
154
+ export function checksumFromAggregate(doc: bson.Document): PartialOrFullChecksum {
155
+ const partialChecksum = Number(BigInt(doc.checksum_total) & 0xffffffffn) & 0xffffffff;
156
+ const bucket = doc._id;
157
+
158
+ if (doc.has_clear_op == 1) {
159
+ return {
160
+ // full checksum - replaces any previous one
161
+ bucket,
162
+ checksum: partialChecksum,
163
+ count: doc.count
164
+ } satisfies BucketChecksum;
165
+ } else {
166
+ return {
167
+ // partial checksum - is added to a previous one
168
+ bucket,
169
+ partialCount: doc.count,
170
+ partialChecksum
171
+ } satisfies PartialChecksum;
172
+ }
173
+ }
@@ -7,9 +7,8 @@ export * from './implementation/MongoPersistedSyncRulesContent.js';
7
7
  export * from './implementation/MongoStorageProvider.js';
8
8
  export * from './implementation/MongoSyncBucketStorage.js';
9
9
  export * from './implementation/MongoSyncRulesLock.js';
10
+ export * from './implementation/MongoTestStorageFactoryGenerator.js';
10
11
  export * from './implementation/OperationBatch.js';
11
12
  export * from './implementation/PersistedBatch.js';
12
- export * from '../utils/util.js';
13
+ export * from './implementation/util.js';
13
14
  export * from './MongoBucketStorage.js';
14
- export * from './MongoReportStorage.js';
15
- export * as test_utils from '../utils/test-utils.js';