@powersync/service-module-mongodb-storage 0.0.0-dev-20250903124544 → 0.0.0-dev-20251015143910
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 +60 -7
- package/dist/storage/implementation/MongoBucketBatch.js +1 -2
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.js +4 -2
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.js +61 -18
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +0 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +0 -2
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/package.json +7 -7
- package/src/storage/implementation/MongoBucketBatch.ts +1 -2
- package/src/storage/implementation/MongoChecksums.ts +4 -2
- package/src/storage/implementation/MongoSyncBucketStorage.ts +61 -18
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +0 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -247,7 +247,10 @@ export class MongoChecksums {
|
|
|
247
247
|
},
|
|
248
248
|
last_op: { $max: '$_id.o' }
|
|
249
249
|
}
|
|
250
|
-
}
|
|
250
|
+
},
|
|
251
|
+
// Sort the aggregated results (100 max, so should be fast).
|
|
252
|
+
// This is important to identify which buckets we have partial data for.
|
|
253
|
+
{ $sort: { _id: 1 } }
|
|
251
254
|
],
|
|
252
255
|
{ session: undefined, readConcern: 'snapshot', maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS }
|
|
253
256
|
)
|
|
@@ -284,7 +287,6 @@ export class MongoChecksums {
|
|
|
284
287
|
// All done for this bucket
|
|
285
288
|
requests.delete(bucket);
|
|
286
289
|
}
|
|
287
|
-
batchCount++;
|
|
288
290
|
}
|
|
289
291
|
if (!limitReached) {
|
|
290
292
|
break;
|
|
@@ -42,6 +42,17 @@ export interface MongoSyncBucketStorageOptions {
|
|
|
42
42
|
checksumOptions?: MongoChecksumOptions;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Only keep checkpoints around for a minute, before fetching a fresh one.
|
|
47
|
+
*
|
|
48
|
+
* The reason is that we keep a MongoDB snapshot reference (clusterTime) with the checkpoint,
|
|
49
|
+
* and they expire after 5 minutes by default. This is an issue if the checkpoint stream is idle,
|
|
50
|
+
* but new clients connect and use an outdated checkpoint snapshot for parameter queries.
|
|
51
|
+
*
|
|
52
|
+
* These will be filtered out for existing clients, so should not create significant overhead.
|
|
53
|
+
*/
|
|
54
|
+
const CHECKPOINT_TIMEOUT_MS = 60_000;
|
|
55
|
+
|
|
45
56
|
export class MongoSyncBucketStorage
|
|
46
57
|
extends BaseObserver<storage.SyncRulesBucketStorageListener>
|
|
47
58
|
implements storage.SyncRulesBucketStorage
|
|
@@ -681,25 +692,45 @@ export class MongoSyncBucketStorage
|
|
|
681
692
|
|
|
682
693
|
// We only watch changes to the active sync rules.
|
|
683
694
|
// If it changes to inactive, we abort and restart with the new sync rules.
|
|
684
|
-
|
|
695
|
+
try {
|
|
696
|
+
while (true) {
|
|
697
|
+
// If the stream is idle, we wait a max of a minute (CHECKPOINT_TIMEOUT_MS)
|
|
698
|
+
// before we get another checkpoint, to avoid stale checkpoint snapshots.
|
|
699
|
+
const timeout = timers
|
|
700
|
+
.setTimeout(CHECKPOINT_TIMEOUT_MS, { done: false }, { signal })
|
|
701
|
+
.catch(() => ({ done: true }));
|
|
702
|
+
try {
|
|
703
|
+
const result = await Promise.race([stream.next(), timeout]);
|
|
704
|
+
if (result.done) {
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
} catch (e) {
|
|
708
|
+
if (e.name == 'AbortError') {
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
throw e;
|
|
712
|
+
}
|
|
685
713
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
714
|
+
if (signal.aborted) {
|
|
715
|
+
// Would likely have been caught by the signal on the timeout or the upstream stream, but we check here anyway
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
690
718
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
719
|
+
const op = await this.getCheckpointInternal();
|
|
720
|
+
if (op == null) {
|
|
721
|
+
// Sync rules have changed - abort and restart.
|
|
722
|
+
// We do a soft close of the stream here - no error
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
697
725
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
726
|
+
// Previously, we only yielded when the checkpoint or lsn changed.
|
|
727
|
+
// However, we always want to use the latest snapshotTime, so we skip that filtering here.
|
|
728
|
+
// That filtering could be added in the per-user streams if needed, but in general the capped collection
|
|
729
|
+
// should already only contain useful changes in most cases.
|
|
701
730
|
yield op;
|
|
702
731
|
}
|
|
732
|
+
} finally {
|
|
733
|
+
await stream.return(null);
|
|
703
734
|
}
|
|
704
735
|
}
|
|
705
736
|
|
|
@@ -715,7 +746,10 @@ export class MongoSyncBucketStorage
|
|
|
715
746
|
let lastCheckpoint: ReplicationCheckpoint | null = null;
|
|
716
747
|
|
|
717
748
|
const iter = this.sharedIter[Symbol.asyncIterator](options.signal);
|
|
749
|
+
|
|
718
750
|
let writeCheckpoint: bigint | null = null;
|
|
751
|
+
// true if we queried the initial write checkpoint, even if it doesn't exist
|
|
752
|
+
let queriedInitialWriteCheckpoint = false;
|
|
719
753
|
|
|
720
754
|
for await (const nextCheckpoint of iter) {
|
|
721
755
|
// lsn changes are not important by itself.
|
|
@@ -723,14 +757,17 @@ export class MongoSyncBucketStorage
|
|
|
723
757
|
// 1. checkpoint (op_id) changes.
|
|
724
758
|
// 2. write checkpoint changes for the specific user
|
|
725
759
|
|
|
726
|
-
if (nextCheckpoint.lsn != null) {
|
|
727
|
-
|
|
760
|
+
if (nextCheckpoint.lsn != null && !queriedInitialWriteCheckpoint) {
|
|
761
|
+
// Lookup the first write checkpoint for the user when we can.
|
|
762
|
+
// There will not actually be one in all cases.
|
|
763
|
+
writeCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
|
|
728
764
|
sync_rules_id: this.group_id,
|
|
729
765
|
user_id: options.user_id,
|
|
730
766
|
heads: {
|
|
731
767
|
'1': nextCheckpoint.lsn
|
|
732
768
|
}
|
|
733
769
|
});
|
|
770
|
+
queriedInitialWriteCheckpoint = true;
|
|
734
771
|
}
|
|
735
772
|
|
|
736
773
|
if (
|
|
@@ -740,12 +777,13 @@ export class MongoSyncBucketStorage
|
|
|
740
777
|
) {
|
|
741
778
|
// No change - wait for next one
|
|
742
779
|
// In some cases, many LSNs may be produced in a short time.
|
|
743
|
-
// Add a delay to throttle the
|
|
780
|
+
// Add a delay to throttle the loop a bit.
|
|
744
781
|
await timers.setTimeout(20 + 10 * Math.random());
|
|
745
782
|
continue;
|
|
746
783
|
}
|
|
747
784
|
|
|
748
785
|
if (lastCheckpoint == null) {
|
|
786
|
+
// First message for this stream - "INVALIDATE_ALL" means it will lookup all data
|
|
749
787
|
yield {
|
|
750
788
|
base: nextCheckpoint,
|
|
751
789
|
writeCheckpoint,
|
|
@@ -759,7 +797,9 @@ export class MongoSyncBucketStorage
|
|
|
759
797
|
|
|
760
798
|
let updatedWriteCheckpoint = updates.updatedWriteCheckpoints.get(options.user_id) ?? null;
|
|
761
799
|
if (updates.invalidateWriteCheckpoints) {
|
|
762
|
-
|
|
800
|
+
// Invalidated means there were too many updates to track the individual ones,
|
|
801
|
+
// so we switch to "polling" (querying directly in each stream).
|
|
802
|
+
updatedWriteCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
|
|
763
803
|
sync_rules_id: this.group_id,
|
|
764
804
|
user_id: options.user_id,
|
|
765
805
|
heads: {
|
|
@@ -769,6 +809,9 @@ export class MongoSyncBucketStorage
|
|
|
769
809
|
}
|
|
770
810
|
if (updatedWriteCheckpoint != null && (writeCheckpoint == null || updatedWriteCheckpoint > writeCheckpoint)) {
|
|
771
811
|
writeCheckpoint = updatedWriteCheckpoint;
|
|
812
|
+
// If it happened that we haven't queried a write checkpoint at this point,
|
|
813
|
+
// then we don't need to anymore, since we got an updated one.
|
|
814
|
+
queriedInitialWriteCheckpoint = true;
|
|
772
815
|
}
|
|
773
816
|
|
|
774
817
|
yield {
|
|
@@ -12,12 +12,10 @@ export type MongoCheckpointAPIOptions = {
|
|
|
12
12
|
export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
13
13
|
readonly db: PowerSyncMongo;
|
|
14
14
|
private _mode: storage.WriteCheckpointMode;
|
|
15
|
-
private sync_rules_id: number;
|
|
16
15
|
|
|
17
16
|
constructor(options: MongoCheckpointAPIOptions) {
|
|
18
17
|
this.db = options.db;
|
|
19
18
|
this._mode = options.mode;
|
|
20
|
-
this.sync_rules_id = options.sync_rules_id;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
get writeCheckpointMode() {
|