@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.
- package/CHANGELOG.md +28 -13
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/storage/MongoBucketStorage.js +1 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +1 -1
- package/dist/storage/implementation/MongoBucketBatch.js +7 -4
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +16 -2
- package/dist/storage/implementation/MongoCompactor.js +204 -48
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.d.ts +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +3 -7
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +12 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.js +196 -37
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoTestStorageFactoryGenerator.d.ts +7 -0
- package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js +18 -0
- package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js.map +1 -0
- package/dist/storage/implementation/PersistedBatch.d.ts +1 -0
- package/dist/storage/implementation/PersistedBatch.js +13 -6
- package/dist/storage/implementation/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +1 -6
- package/dist/storage/implementation/db.js +0 -16
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +14 -3
- package/dist/{utils → storage/implementation}/util.d.ts +35 -3
- package/dist/{utils → storage/implementation}/util.js +54 -0
- package/dist/storage/implementation/util.js.map +1 -0
- package/dist/storage/storage-index.d.ts +2 -3
- package/dist/storage/storage-index.js +2 -3
- package/dist/storage/storage-index.js.map +1 -1
- package/package.json +8 -8
- package/src/index.ts +0 -1
- package/src/storage/MongoBucketStorage.ts +1 -1
- package/src/storage/implementation/MongoBucketBatch.ts +8 -6
- package/src/storage/implementation/MongoCompactor.ts +239 -49
- package/src/storage/implementation/MongoStorageProvider.ts +4 -9
- package/src/storage/implementation/MongoSyncBucketStorage.ts +242 -38
- package/src/storage/implementation/MongoTestStorageFactoryGenerator.ts +28 -0
- package/src/storage/implementation/PersistedBatch.ts +14 -6
- package/src/storage/implementation/db.ts +0 -18
- package/src/storage/implementation/models.ts +15 -3
- package/src/{utils → storage/implementation}/util.ts +61 -3
- package/src/storage/storage-index.ts +2 -3
- package/test/src/__snapshots__/storage_sync.test.ts.snap +110 -0
- package/test/src/util.ts +2 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/migrations/db/migrations/1752661449910-connection-reporting.d.ts +0 -3
- package/dist/migrations/db/migrations/1752661449910-connection-reporting.js +0 -36
- package/dist/migrations/db/migrations/1752661449910-connection-reporting.js.map +0 -1
- package/dist/storage/MongoReportStorage.d.ts +0 -18
- package/dist/storage/MongoReportStorage.js +0 -154
- package/dist/storage/MongoReportStorage.js.map +0 -1
- package/dist/utils/test-utils.d.ts +0 -11
- package/dist/utils/test-utils.js +0 -40
- package/dist/utils/test-utils.js.map +0 -1
- package/dist/utils/util.js.map +0 -1
- package/dist/utils/utils-index.d.ts +0 -2
- package/dist/utils/utils-index.js +0 -3
- package/dist/utils/utils-index.js.map +0 -1
- package/src/migrations/db/migrations/1752661449910-connection-reporting.ts +0 -58
- package/src/storage/MongoReportStorage.ts +0 -177
- package/src/utils/test-utils.ts +0 -55
- package/src/utils/utils-index.ts +0 -2
- package/test/src/__snapshots__/connection-report-storage.test.ts.snap +0 -215
- 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
|
|
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:
|
|
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.
|
|
627
|
+
{ session: undefined, readConcern: 'snapshot', maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS }
|
|
541
628
|
)
|
|
542
|
-
|
|
543
|
-
.
|
|
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
|
-
|
|
632
|
+
const partialChecksums = new Map<string, storage.PartialOrFullChecksum>(
|
|
548
633
|
aggregate.map((doc) => {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 '
|
|
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';
|