@powersync/service-module-mongodb-storage 0.14.0 → 0.15.1

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 (65) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/storage/MongoBucketStorage.js +16 -3
  3. package/dist/storage/MongoBucketStorage.js.map +1 -1
  4. package/dist/storage/implementation/MongoBucketBatch.d.ts +13 -11
  5. package/dist/storage/implementation/MongoBucketBatch.js +208 -127
  6. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  7. package/dist/storage/implementation/MongoChecksums.d.ts +4 -4
  8. package/dist/storage/implementation/MongoChecksums.js +1 -0
  9. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  10. package/dist/storage/implementation/MongoCompactor.d.ts +8 -2
  11. package/dist/storage/implementation/MongoCompactor.js +50 -21
  12. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  13. package/dist/storage/implementation/MongoParameterCompactor.d.ts +2 -2
  14. package/dist/storage/implementation/MongoParameterCompactor.js +13 -1
  15. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  16. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +2 -7
  17. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  18. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +9 -4
  19. package/dist/storage/implementation/MongoSyncBucketStorage.js +35 -33
  20. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  21. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +3 -3
  22. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  23. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +4 -4
  24. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  25. package/dist/storage/implementation/OperationBatch.js +3 -2
  26. package/dist/storage/implementation/OperationBatch.js.map +1 -1
  27. package/dist/storage/implementation/PersistedBatch.d.ts +11 -4
  28. package/dist/storage/implementation/PersistedBatch.js +42 -11
  29. package/dist/storage/implementation/PersistedBatch.js.map +1 -1
  30. package/dist/storage/implementation/db.d.ts +35 -1
  31. package/dist/storage/implementation/db.js +99 -0
  32. package/dist/storage/implementation/db.js.map +1 -1
  33. package/dist/storage/implementation/models.d.ts +15 -3
  34. package/dist/storage/implementation/models.js +2 -1
  35. package/dist/storage/implementation/models.js.map +1 -1
  36. package/dist/utils/test-utils.d.ts +4 -1
  37. package/dist/utils/test-utils.js +15 -12
  38. package/dist/utils/test-utils.js.map +1 -1
  39. package/dist/utils/util.d.ts +2 -1
  40. package/dist/utils/util.js +15 -1
  41. package/dist/utils/util.js.map +1 -1
  42. package/package.json +6 -6
  43. package/src/storage/MongoBucketStorage.ts +29 -8
  44. package/src/storage/implementation/MongoBucketBatch.ts +263 -177
  45. package/src/storage/implementation/MongoChecksums.ts +5 -3
  46. package/src/storage/implementation/MongoCompactor.ts +53 -24
  47. package/src/storage/implementation/MongoParameterCompactor.ts +17 -4
  48. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +3 -11
  49. package/src/storage/implementation/MongoSyncBucketStorage.ts +33 -26
  50. package/src/storage/implementation/MongoSyncRulesLock.ts +3 -3
  51. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -4
  52. package/src/storage/implementation/OperationBatch.ts +3 -2
  53. package/src/storage/implementation/PersistedBatch.ts +42 -11
  54. package/src/storage/implementation/db.ts +129 -1
  55. package/src/storage/implementation/models.ts +18 -4
  56. package/src/utils/test-utils.ts +15 -12
  57. package/src/utils/util.ts +17 -2
  58. package/test/src/__snapshots__/storage.test.ts.snap +201 -0
  59. package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
  60. package/test/src/__snapshots__/storage_sync.test.ts.snap +1111 -16
  61. package/test/src/storage.test.ts +9 -7
  62. package/test/src/storage_compacting.test.ts +117 -45
  63. package/test/src/storage_sync.test.ts +53 -51
  64. package/test/src/util.ts +3 -3
  65. package/tsconfig.tsbuildinfo +1 -1
@@ -2,6 +2,7 @@ import * as lib_mongo from '@powersync/lib-service-mongodb';
2
2
  import {
3
3
  addPartialChecksums,
4
4
  bson,
5
+ BucketChecksumRequest,
5
6
  BucketChecksum,
6
7
  ChecksumCache,
7
8
  ChecksumMap,
@@ -12,7 +13,7 @@ import {
12
13
  PartialChecksumMap,
13
14
  PartialOrFullChecksum
14
15
  } from '@powersync/service-core';
15
- import { PowerSyncMongo } from './db.js';
16
+ import { VersionedPowerSyncMongo } from './db.js';
16
17
  import { StorageConfig } from './models.js';
17
18
 
18
19
  /**
@@ -49,7 +50,7 @@ export class MongoChecksums {
49
50
  private readonly storageConfig: StorageConfig;
50
51
 
51
52
  constructor(
52
- private db: PowerSyncMongo,
53
+ private db: VersionedPowerSyncMongo,
53
54
  private group_id: number,
54
55
  private options: MongoChecksumOptions
55
56
  ) {
@@ -74,7 +75,7 @@ export class MongoChecksums {
74
75
  * Calculate checksums, utilizing the cache for partial checkums, and querying the remainder from
75
76
  * the database (bucket_state + bucket_data).
76
77
  */
77
- async getChecksums(checkpoint: InternalOpId, buckets: string[]): Promise<ChecksumMap> {
78
+ async getChecksums(checkpoint: InternalOpId, buckets: BucketChecksumRequest[]): Promise<ChecksumMap> {
78
79
  return this.cache.getChecksumMap(checkpoint, buckets);
79
80
  }
80
81
 
@@ -298,6 +299,7 @@ export class MongoChecksums {
298
299
  const req = requests.get(bucket);
299
300
  requests.set(bucket, {
300
301
  bucket,
302
+ source: req!.source,
301
303
  start: doc.last_op,
302
304
  end: req!.end
303
305
  });
@@ -1,4 +1,4 @@
1
- import { mongo, MONGO_OPERATION_TIMEOUT_MS } from '@powersync/lib-service-mongodb';
1
+ import { isMongoServerError, mongo, MONGO_OPERATION_TIMEOUT_MS } from '@powersync/lib-service-mongodb';
2
2
  import { logger, ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
3
3
  import {
4
4
  addChecksums,
@@ -9,7 +9,7 @@ import {
9
9
  utils
10
10
  } from '@powersync/service-core';
11
11
 
12
- import { PowerSyncMongo } from './db.js';
12
+ import { VersionedPowerSyncMongo } from './db.js';
13
13
  import { BucketDataDocument, BucketDataKey, BucketStateDocument } from './models.js';
14
14
  import { MongoSyncBucketStorage } from './MongoSyncBucketStorage.js';
15
15
  import { cacheKey } from './OperationBatch.js';
@@ -85,19 +85,19 @@ export class MongoCompactor {
85
85
 
86
86
  constructor(
87
87
  private storage: MongoSyncBucketStorage,
88
- private db: PowerSyncMongo,
89
- options?: MongoCompactOptions
88
+ private db: VersionedPowerSyncMongo,
89
+ options: MongoCompactOptions
90
90
  ) {
91
91
  this.group_id = storage.group_id;
92
- this.idLimitBytes = (options?.memoryLimitMB ?? DEFAULT_MEMORY_LIMIT_MB) * 1024 * 1024;
93
- this.moveBatchLimit = options?.moveBatchLimit ?? DEFAULT_MOVE_BATCH_LIMIT;
94
- this.moveBatchQueryLimit = options?.moveBatchQueryLimit ?? DEFAULT_MOVE_BATCH_QUERY_LIMIT;
95
- this.clearBatchLimit = options?.clearBatchLimit ?? DEFAULT_CLEAR_BATCH_LIMIT;
96
- this.minBucketChanges = options?.minBucketChanges ?? DEFAULT_MIN_BUCKET_CHANGES;
97
- this.minChangeRatio = options?.minChangeRatio ?? DEFAULT_MIN_CHANGE_RATIO;
98
- this.maxOpId = options?.maxOpId ?? 0n;
99
- this.buckets = options?.compactBuckets;
100
- this.signal = options?.signal;
92
+ this.idLimitBytes = (options.memoryLimitMB ?? DEFAULT_MEMORY_LIMIT_MB) * 1024 * 1024;
93
+ this.moveBatchLimit = options.moveBatchLimit ?? DEFAULT_MOVE_BATCH_LIMIT;
94
+ this.moveBatchQueryLimit = options.moveBatchQueryLimit ?? DEFAULT_MOVE_BATCH_QUERY_LIMIT;
95
+ this.clearBatchLimit = options.clearBatchLimit ?? DEFAULT_CLEAR_BATCH_LIMIT;
96
+ this.minBucketChanges = options.minBucketChanges ?? DEFAULT_MIN_BUCKET_CHANGES;
97
+ this.minChangeRatio = options.minChangeRatio ?? DEFAULT_MIN_CHANGE_RATIO;
98
+ this.maxOpId = options.maxOpId ?? 0n;
99
+ this.buckets = options.compactBuckets;
100
+ this.signal = options.signal;
101
101
  }
102
102
 
103
103
  /**
@@ -111,7 +111,7 @@ export class MongoCompactor {
111
111
  // We can make this more efficient later on by iterating
112
112
  // through the buckets in a single query.
113
113
  // That makes batching more tricky, so we leave for later.
114
- await this.compactSingleBucket(bucket);
114
+ await this.compactSingleBucketRetried(bucket);
115
115
  }
116
116
  } else {
117
117
  await this.compactDirtyBuckets();
@@ -123,15 +123,36 @@ export class MongoCompactor {
123
123
  minBucketChanges: this.minBucketChanges,
124
124
  minChangeRatio: this.minChangeRatio
125
125
  })) {
126
- if (this.signal?.aborted) {
127
- break;
128
- }
126
+ this.signal?.throwIfAborted();
129
127
  if (buckets.length == 0) {
130
128
  continue;
131
129
  }
132
130
 
133
131
  for (let { bucket } of buckets) {
132
+ await this.compactSingleBucketRetried(bucket);
133
+ }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Compaction for a single bucket, with retries on failure.
139
+ *
140
+ * This covers against occasional network or other database errors during a long compact job.
141
+ */
142
+ private async compactSingleBucketRetried(bucket: string) {
143
+ let retryCount = 0;
144
+ while (true) {
145
+ try {
134
146
  await this.compactSingleBucket(bucket);
147
+ break;
148
+ } catch (e) {
149
+ if (retryCount < 3 && isMongoServerError(e)) {
150
+ logger.warn(`Error compacting bucket ${bucket}, retrying...`, e);
151
+ retryCount++;
152
+ await new Promise((resolve) => setTimeout(resolve, 1000 * retryCount));
153
+ } else {
154
+ throw e;
155
+ }
135
156
  }
136
157
  }
137
158
  }
@@ -165,7 +186,9 @@ export class MongoCompactor {
165
186
  o: new mongo.MaxKey() as any
166
187
  };
167
188
 
168
- while (!this.signal?.aborted) {
189
+ while (true) {
190
+ this.signal?.throwIfAborted();
191
+
169
192
  // Query one batch at a time, to avoid cursor timeouts
170
193
  const cursor = this.db.bucket_data.aggregate<BucketDataDocument & { size: number | bigint }>(
171
194
  [
@@ -393,7 +416,8 @@ export class MongoCompactor {
393
416
  const session = this.db.client.startSession();
394
417
  try {
395
418
  let done = false;
396
- while (!done && !this.signal?.aborted) {
419
+ while (!done) {
420
+ this.signal?.throwIfAborted();
397
421
  let opCountDiff = 0;
398
422
  // Do the CLEAR operation in batches, with each batch a separate transaction.
399
423
  // The state after each batch is fully consistent.
@@ -486,12 +510,14 @@ export class MongoCompactor {
486
510
  */
487
511
  async populateChecksums(options: { minBucketChanges: number }): Promise<PopulateChecksumCacheResults> {
488
512
  let count = 0;
489
- while (!this.signal?.aborted) {
513
+ while (true) {
514
+ this.signal?.throwIfAborted();
490
515
  const buckets = await this.dirtyBucketBatchForChecksums(options);
491
- if (buckets.length == 0 || this.signal?.aborted) {
516
+ if (buckets.length == 0) {
492
517
  // All done
493
518
  break;
494
519
  }
520
+ this.signal?.throwIfAborted();
495
521
 
496
522
  const start = Date.now();
497
523
 
@@ -593,10 +619,12 @@ export class MongoCompactor {
593
619
  lastId = cursor._id;
594
620
 
595
621
  const mapped = (result?.buckets ?? []).map((b) => {
622
+ // The numbers, specifically the bytes, could be a bigint. We convert to Number to allow calculating the ratios.
623
+ // BigInt precision is not needed here since it's just an estimate.
596
624
  const updatedCount = b.estimate_since_compact?.count ?? 0;
597
625
  const totalCount = (b.compacted_state?.count ?? 0) + updatedCount;
598
- const updatedBytes = b.estimate_since_compact?.bytes ?? 0;
599
- const totalBytes = (b.compacted_state?.bytes ?? 0) + updatedBytes;
626
+ const updatedBytes = Number(b.estimate_since_compact?.bytes ?? 0);
627
+ const totalBytes = Number(b.compacted_state?.bytes ?? 0) + updatedBytes;
600
628
  const dirtyChangeNumber = totalCount > 0 ? updatedCount / totalCount : 0;
601
629
  const dirtyChangeBytes = totalBytes > 0 ? updatedBytes / totalBytes : 0;
602
630
  return {
@@ -653,7 +681,7 @@ export class MongoCompactor {
653
681
 
654
682
  return dirtyBuckets.map((bucket) => ({
655
683
  bucket: bucket._id.b,
656
- estimatedCount: bucket.estimate_since_compact!.count + (bucket.compacted_state?.count ?? 0)
684
+ estimatedCount: Number(bucket.estimate_since_compact!.count) + Number(bucket.compacted_state?.count ?? 0)
657
685
  }));
658
686
  }
659
687
 
@@ -662,6 +690,7 @@ export class MongoCompactor {
662
690
  buckets.map((bucket) => {
663
691
  return {
664
692
  bucket,
693
+ source: {} as any,
665
694
  end: this.maxOpId
666
695
  };
667
696
  })
@@ -1,8 +1,8 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
1
2
  import { logger } from '@powersync/lib-services-framework';
2
3
  import { bson, CompactOptions, InternalOpId } from '@powersync/service-core';
3
4
  import { LRUCache } from 'lru-cache';
4
- import { PowerSyncMongo } from './db.js';
5
- import { mongo } from '@powersync/lib-service-mongodb';
5
+ import { VersionedPowerSyncMongo } from './db.js';
6
6
  import { BucketParameterDocument } from './models.js';
7
7
 
8
8
  /**
@@ -14,14 +14,14 @@ import { BucketParameterDocument } from './models.js';
14
14
  */
15
15
  export class MongoParameterCompactor {
16
16
  constructor(
17
- private db: PowerSyncMongo,
17
+ private db: VersionedPowerSyncMongo,
18
18
  private group_id: number,
19
19
  private checkpoint: InternalOpId,
20
20
  private options: CompactOptions
21
21
  ) {}
22
22
 
23
23
  async compact() {
24
- logger.info(`Compacting parameters for group ${this.group_id} up to checkpoint ${this.checkpoint}`);
24
+ logger.info(`Compacting parameters for sync config ${this.group_id} up to checkpoint ${this.checkpoint}`);
25
25
  // This is the currently-active checkpoint.
26
26
  // We do not remove any data that may be used by this checkpoint.
27
27
  // snapshot queries ensure that if any clients are still using older checkpoints, they would
@@ -49,6 +49,9 @@ export class MongoParameterCompactor {
49
49
  });
50
50
  let removeIds: InternalOpId[] = [];
51
51
  let removeDeleted: mongo.AnyBulkWriteOperation<BucketParameterDocument>[] = [];
52
+ let checkedEntries = 0;
53
+ let checkedEntriesAtLastLog = 0;
54
+ let lastProgressLogTime = Date.now();
52
55
 
53
56
  const flush = async (force: boolean) => {
54
57
  if (removeIds.length >= 1000 || (force && removeIds.length > 0)) {
@@ -66,6 +69,16 @@ export class MongoParameterCompactor {
66
69
 
67
70
  while (await cursor.hasNext()) {
68
71
  const batch = cursor.readBufferedDocuments();
72
+ checkedEntries += batch.length;
73
+ const now = Date.now();
74
+ if (now - lastProgressLogTime >= 60_000) {
75
+ const elapsedSeconds = (now - lastProgressLogTime) / 1000;
76
+ const rate = (checkedEntries - checkedEntriesAtLastLog) / elapsedSeconds;
77
+ logger.info(`Checked ${checkedEntries} parameter index entries for compaction (${rate.toFixed(1)} entries/s)`);
78
+ lastProgressLogTime = now;
79
+ checkedEntriesAtLastLog = checkedEntries;
80
+ }
81
+
69
82
  for (let doc of batch) {
70
83
  if (doc._id >= checkpoint) {
71
84
  continue;
@@ -1,9 +1,8 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import { storage } from '@powersync/service-core';
3
3
  import { MongoSyncRulesLock } from './MongoSyncRulesLock.js';
4
- import { PowerSyncMongo } from './db.js';
4
+ import { PowerSyncMongo, VersionedPowerSyncMongo } from './db.js';
5
5
  import { getMongoStorageConfig, SyncRuleDocument } from './models.js';
6
- import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
7
6
 
8
7
  export class MongoPersistedSyncRulesContent extends storage.PersistedSyncRulesContent {
9
8
  public current_lock: MongoSyncRulesLock | null = null;
@@ -29,18 +28,11 @@ export class MongoPersistedSyncRulesContent extends storage.PersistedSyncRulesCo
29
28
  }
30
29
 
31
30
  getStorageConfig() {
32
- const storageConfig = getMongoStorageConfig(this.storageVersion);
33
- if (storageConfig == null) {
34
- throw new ServiceError(
35
- ErrorCode.PSYNC_S1005,
36
- `Unsupported storage version ${this.storageVersion} for sync rules ${this.id}`
37
- );
38
- }
39
- return storageConfig;
31
+ return getMongoStorageConfig(this.storageVersion);
40
32
  }
41
33
 
42
34
  async lock() {
43
- const lock = await MongoSyncRulesLock.createLock(this.db, this);
35
+ const lock = await MongoSyncRulesLock.createLock(this.db.versioned(this.getStorageConfig()), this);
44
36
  this.current_lock = lock;
45
37
  return lock;
46
38
  }
@@ -31,7 +31,7 @@ import { LRUCache } from 'lru-cache';
31
31
  import * as timers from 'timers/promises';
32
32
  import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '../../utils/util.js';
33
33
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
34
- import { PowerSyncMongo } from './db.js';
34
+ import { VersionedPowerSyncMongo } from './db.js';
35
35
  import {
36
36
  BucketDataDocument,
37
37
  BucketDataKey,
@@ -44,6 +44,7 @@ import { MongoBucketBatch } from './MongoBucketBatch.js';
44
44
  import { MongoChecksumOptions, MongoChecksums } from './MongoChecksums.js';
45
45
  import { MongoCompactor } from './MongoCompactor.js';
46
46
  import { MongoParameterCompactor } from './MongoParameterCompactor.js';
47
+ import { MongoPersistedSyncRulesContent } from './MongoPersistedSyncRulesContent.js';
47
48
  import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
48
49
 
49
50
  export interface MongoSyncBucketStorageOptions {
@@ -66,7 +67,7 @@ export class MongoSyncBucketStorage
66
67
  extends BaseObserver<storage.SyncRulesBucketStorageListener>
67
68
  implements storage.SyncRulesBucketStorage
68
69
  {
69
- private readonly db: PowerSyncMongo;
70
+ private readonly db: VersionedPowerSyncMongo;
70
71
  readonly checksums: MongoChecksums;
71
72
 
72
73
  private parsedSyncRulesCache: { parsed: HydratedSyncRules; options: storage.ParseSyncRulesOptions } | undefined;
@@ -75,13 +76,13 @@ export class MongoSyncBucketStorage
75
76
  constructor(
76
77
  public readonly factory: MongoBucketStorage,
77
78
  public readonly group_id: number,
78
- private readonly sync_rules: storage.PersistedSyncRulesContent,
79
+ private readonly sync_rules: MongoPersistedSyncRulesContent,
79
80
  public readonly slot_name: string,
80
81
  writeCheckpointMode: storage.WriteCheckpointMode | undefined,
81
82
  options: MongoSyncBucketStorageOptions
82
83
  ) {
83
84
  super();
84
- this.db = factory.db;
85
+ this.db = factory.db.versioned(sync_rules.getStorageConfig());
85
86
  this.checksums = new MongoChecksums(this.db, this.group_id, {
86
87
  ...options.checksumOptions,
87
88
  storageConfig: options?.storageConfig
@@ -166,10 +167,7 @@ export class MongoSyncBucketStorage
166
167
  });
167
168
  }
168
169
 
169
- async startBatch(
170
- options: storage.StartBatchOptions,
171
- callback: (batch: storage.BucketStorageBatch) => Promise<void>
172
- ): Promise<storage.FlushedResult | null> {
170
+ async createWriter(options: storage.CreateWriterOptions): Promise<storage.BucketStorageBatch> {
173
171
  const doc = await this.db.sync_rules.findOne(
174
172
  {
175
173
  _id: this.group_id
@@ -178,7 +176,7 @@ export class MongoSyncBucketStorage
178
176
  );
179
177
  const checkpoint_lsn = doc?.last_checkpoint_lsn ?? null;
180
178
 
181
- await using batch = new MongoBucketBatch({
179
+ const writer = new MongoBucketBatch({
182
180
  logger: options.logger,
183
181
  db: this.db,
184
182
  syncRules: this.sync_rules.parsed(options).hydratedSyncRules(),
@@ -186,21 +184,26 @@ export class MongoSyncBucketStorage
186
184
  slotName: this.slot_name,
187
185
  lastCheckpointLsn: checkpoint_lsn,
188
186
  resumeFromLsn: maxLsn(checkpoint_lsn, doc?.snapshot_lsn),
189
- noCheckpointBeforeLsn: doc?.no_checkpoint_before ?? options.zeroLSN,
190
187
  keepaliveOp: doc?.keepalive_op ? BigInt(doc.keepalive_op) : null,
191
188
  storeCurrentData: options.storeCurrentData,
192
189
  skipExistingRows: options.skipExistingRows ?? false,
193
190
  markRecordUnavailable: options.markRecordUnavailable
194
191
  });
195
- this.iterateListeners((cb) => cb.batchStarted?.(batch));
196
-
197
- await callback(batch);
198
- await batch.flush();
199
- if (batch.last_flushed_op != null) {
200
- return { flushed_op: batch.last_flushed_op };
201
- } else {
202
- return null;
203
- }
192
+ this.iterateListeners((cb) => cb.batchStarted?.(writer));
193
+ return writer;
194
+ }
195
+
196
+ /**
197
+ * @deprecated Use `createWriter()` with `await using` instead.
198
+ */
199
+ async startBatch(
200
+ options: storage.CreateWriterOptions,
201
+ callback: (batch: storage.BucketStorageBatch) => Promise<void>
202
+ ): Promise<storage.FlushedResult | null> {
203
+ await using writer = await this.createWriter(options);
204
+ await callback(writer);
205
+ await writer.flush();
206
+ return writer.last_flushed_op != null ? { flushed_op: writer.last_flushed_op } : null;
204
207
  }
205
208
 
206
209
  async resolveTable(options: storage.ResolveTableOptions): Promise<storage.ResolveTableResult> {
@@ -372,19 +375,20 @@ export class MongoSyncBucketStorage
372
375
 
373
376
  async *getBucketDataBatch(
374
377
  checkpoint: utils.InternalOpId,
375
- dataBuckets: Map<string, InternalOpId>,
378
+ dataBuckets: storage.BucketDataRequest[],
376
379
  options?: storage.BucketDataBatchOptions
377
380
  ): AsyncIterable<storage.SyncBucketDataChunk> {
378
- if (dataBuckets.size == 0) {
381
+ if (dataBuckets.length == 0) {
379
382
  return;
380
383
  }
381
384
  let filters: mongo.Filter<BucketDataDocument>[] = [];
385
+ const bucketMap = new Map(dataBuckets.map((request) => [request.bucket, request.start]));
382
386
 
383
387
  if (checkpoint == null) {
384
388
  throw new ServiceAssertionError('checkpoint is null');
385
389
  }
386
390
  const end = checkpoint;
387
- for (let [name, start] of dataBuckets.entries()) {
391
+ for (let { bucket: name, start } of dataBuckets) {
388
392
  filters.push({
389
393
  _id: {
390
394
  $gt: {
@@ -477,7 +481,7 @@ export class MongoSyncBucketStorage
477
481
  }
478
482
 
479
483
  if (start == null) {
480
- const startOpId = dataBuckets.get(bucket);
484
+ const startOpId = bucketMap.get(bucket);
481
485
  if (startOpId == null) {
482
486
  throw new ServiceAssertionError(`data for unexpected bucket: ${bucket}`);
483
487
  }
@@ -519,7 +523,10 @@ export class MongoSyncBucketStorage
519
523
  }
520
524
  }
521
525
 
522
- async getChecksums(checkpoint: utils.InternalOpId, buckets: string[]): Promise<utils.ChecksumMap> {
526
+ async getChecksums(
527
+ checkpoint: utils.InternalOpId,
528
+ buckets: storage.BucketChecksumRequest[]
529
+ ): Promise<utils.ChecksumMap> {
523
530
  return this.checksums.getChecksums(checkpoint, buckets);
524
531
  }
525
532
 
@@ -576,7 +583,7 @@ export class MongoSyncBucketStorage
576
583
  async clear(options?: storage.ClearStorageOptions): Promise<void> {
577
584
  while (true) {
578
585
  if (options?.signal?.aborted) {
579
- throw new ReplicationAbortedError('Aborted clearing data');
586
+ throw new ReplicationAbortedError('Aborted clearing data', options.signal.reason);
580
587
  }
581
588
  try {
582
589
  await this.clearIteration();
@@ -631,7 +638,7 @@ export class MongoSyncBucketStorage
631
638
  { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
632
639
  );
633
640
 
634
- await this.db.current_data.deleteMany(
641
+ await this.db.common_current_data.deleteMany(
635
642
  {
636
643
  _id: idPrefixFilter<SourceKey>({ g: this.group_id }, ['t', 'k'])
637
644
  },
@@ -2,7 +2,7 @@ import crypto from 'crypto';
2
2
 
3
3
  import { ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
4
4
  import { storage } from '@powersync/service-core';
5
- import { PowerSyncMongo } from './db.js';
5
+ import { PowerSyncMongo, VersionedPowerSyncMongo } from './db.js';
6
6
 
7
7
  /**
8
8
  * Manages a lock on a sync rules document, so that only one process
@@ -12,7 +12,7 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
12
12
  private readonly refreshInterval: NodeJS.Timeout;
13
13
 
14
14
  static async createLock(
15
- db: PowerSyncMongo,
15
+ db: VersionedPowerSyncMongo,
16
16
  sync_rules: storage.PersistedSyncRulesContent
17
17
  ): Promise<MongoSyncRulesLock> {
18
18
  const lockId = crypto.randomBytes(8).toString('hex');
@@ -52,7 +52,7 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
52
52
  }
53
53
 
54
54
  constructor(
55
- private db: PowerSyncMongo,
55
+ private db: VersionedPowerSyncMongo,
56
56
  public sync_rules_id: number,
57
57
  private lock_id: string
58
58
  ) {
@@ -1,16 +1,16 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import * as framework from '@powersync/lib-services-framework';
3
3
  import { GetCheckpointChangesOptions, InternalOpId, storage } from '@powersync/service-core';
4
- import { PowerSyncMongo } from './db.js';
4
+ import { PowerSyncMongo, VersionedPowerSyncMongo } from './db.js';
5
5
 
6
6
  export type MongoCheckpointAPIOptions = {
7
- db: PowerSyncMongo;
7
+ db: VersionedPowerSyncMongo;
8
8
  mode: storage.WriteCheckpointMode;
9
9
  sync_rules_id: number;
10
10
  };
11
11
 
12
12
  export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
13
- readonly db: PowerSyncMongo;
13
+ readonly db: VersionedPowerSyncMongo;
14
14
  private _mode: storage.WriteCheckpointMode;
15
15
 
16
16
  constructor(options: MongoCheckpointAPIOptions) {
@@ -166,7 +166,7 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
166
166
  }
167
167
 
168
168
  export async function batchCreateCustomWriteCheckpoints(
169
- db: PowerSyncMongo,
169
+ db: VersionedPowerSyncMongo,
170
170
  session: mongo.ClientSession,
171
171
  checkpoints: storage.CustomWriteCheckpointOptions[],
172
172
  opId: InternalOpId
@@ -2,6 +2,7 @@ import { ToastableSqliteRow } from '@powersync/service-sync-rules';
2
2
  import * as bson from 'bson';
3
3
 
4
4
  import { storage } from '@powersync/service-core';
5
+ import { mongoTableId } from '../storage-index.js';
5
6
 
6
7
  /**
7
8
  * Maximum number of operations in a batch.
@@ -86,8 +87,8 @@ export class RecordOperation {
86
87
  const beforeId = record.beforeReplicaId ?? record.afterReplicaId;
87
88
  this.afterId = afterId;
88
89
  this.beforeId = beforeId;
89
- this.internalBeforeKey = cacheKey(record.sourceTable.id, beforeId);
90
- this.internalAfterKey = afterId ? cacheKey(record.sourceTable.id, afterId) : null;
90
+ this.internalBeforeKey = cacheKey(mongoTableId(record.sourceTable.id), beforeId);
91
+ this.internalAfterKey = afterId ? cacheKey(mongoTableId(record.sourceTable.id), afterId) : null;
91
92
 
92
93
  this.estimatedSize = estimateRowSize(record.before) + estimateRowSize(record.after);
93
94
  }
@@ -5,9 +5,9 @@ import * as bson from 'bson';
5
5
 
6
6
  import { Logger, logger as defaultLogger } from '@powersync/lib-services-framework';
7
7
  import { InternalOpId, storage, utils } from '@powersync/service-core';
8
- import { currentBucketKey, MAX_ROW_SIZE } from './MongoBucketBatch.js';
8
+ import { currentBucketKey, EMPTY_DATA, MAX_ROW_SIZE } from './MongoBucketBatch.js';
9
9
  import { MongoIdSequence } from './MongoIdSequence.js';
10
- import { PowerSyncMongo } from './db.js';
10
+ import { PowerSyncMongo, VersionedPowerSyncMongo } from './db.js';
11
11
  import {
12
12
  BucketDataDocument,
13
13
  BucketParameterDocument,
@@ -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 { mongoTableId, replicaIdToSubkey } from '../../utils/util.js';
20
20
 
21
21
  /**
22
22
  * Maximum size of operations we write in a single transaction.
@@ -63,6 +63,7 @@ export class PersistedBatch {
63
63
  currentSize = 0;
64
64
 
65
65
  constructor(
66
+ private db: VersionedPowerSyncMongo,
66
67
  private group_id: number,
67
68
  writtenSize: number,
68
69
  options?: { logger?: Logger }
@@ -132,7 +133,7 @@ export class PersistedBatch {
132
133
  o: op_id
133
134
  },
134
135
  op: 'PUT',
135
- source_table: options.table.id,
136
+ source_table: mongoTableId(options.table.id),
136
137
  source_key: options.sourceKey,
137
138
  table: k.table,
138
139
  row_id: k.id,
@@ -159,7 +160,7 @@ export class PersistedBatch {
159
160
  o: op_id
160
161
  },
161
162
  op: 'REMOVE',
162
- source_table: options.table.id,
163
+ source_table: mongoTableId(options.table.id),
163
164
  source_key: options.sourceKey,
164
165
  table: bd.table,
165
166
  row_id: bd.id,
@@ -208,7 +209,7 @@ export class PersistedBatch {
208
209
  _id: op_id,
209
210
  key: {
210
211
  g: this.group_id,
211
- t: sourceTable.id,
212
+ t: mongoTableId(sourceTable.id),
212
213
  k: sourceKey
213
214
  },
214
215
  lookup: binLookup,
@@ -230,7 +231,7 @@ export class PersistedBatch {
230
231
  _id: op_id,
231
232
  key: {
232
233
  g: this.group_id,
233
- t: sourceTable.id,
234
+ t: mongoTableId(sourceTable.id),
234
235
  k: sourceKey
235
236
  },
236
237
  lookup: lookup,
@@ -243,7 +244,7 @@ export class PersistedBatch {
243
244
  }
244
245
  }
245
246
 
246
- deleteCurrentData(id: SourceKey) {
247
+ hardDeleteCurrentData(id: SourceKey) {
247
248
  const op: mongo.AnyBulkWriteOperation<CurrentDataDocument> = {
248
249
  deleteOne: {
249
250
  filter: { _id: id }
@@ -253,12 +254,41 @@ export class PersistedBatch {
253
254
  this.currentSize += 50;
254
255
  }
255
256
 
257
+ /**
258
+ * Mark a current_data document as soft deleted, to delete on the next commit.
259
+ *
260
+ * If softDeleteCurrentData is not enabled, this falls back to a hard delete.
261
+ */
262
+ softDeleteCurrentData(id: SourceKey, checkpointGreaterThan: bigint) {
263
+ if (!this.db.storageConfig.softDeleteCurrentData) {
264
+ this.hardDeleteCurrentData(id);
265
+ return;
266
+ }
267
+ const op: mongo.AnyBulkWriteOperation<CurrentDataDocument> = {
268
+ updateOne: {
269
+ filter: { _id: id },
270
+ update: {
271
+ $set: {
272
+ data: EMPTY_DATA,
273
+ buckets: [],
274
+ lookups: [],
275
+ pending_delete: checkpointGreaterThan
276
+ }
277
+ },
278
+ upsert: true
279
+ }
280
+ };
281
+ this.currentData.push(op);
282
+ this.currentSize += 50;
283
+ }
284
+
256
285
  upsertCurrentData(id: SourceKey, values: Partial<CurrentDataDocument>) {
257
286
  const op: mongo.AnyBulkWriteOperation<CurrentDataDocument> = {
258
287
  updateOne: {
259
288
  filter: { _id: id },
260
289
  update: {
261
- $set: values
290
+ $set: values,
291
+ $unset: { pending_delete: 1 }
262
292
  },
263
293
  upsert: true
264
294
  }
@@ -276,7 +306,8 @@ export class PersistedBatch {
276
306
  );
277
307
  }
278
308
 
279
- async flush(db: PowerSyncMongo, session: mongo.ClientSession, options?: storage.BucketBatchCommitOptions) {
309
+ async flush(session: mongo.ClientSession, options?: storage.BucketBatchCommitOptions) {
310
+ const db = this.db;
280
311
  const startAt = performance.now();
281
312
  let flushedSomething = false;
282
313
  if (this.bucketData.length > 0) {
@@ -297,7 +328,7 @@ export class PersistedBatch {
297
328
  }
298
329
  if (this.currentData.length > 0) {
299
330
  flushedSomething = true;
300
- await db.current_data.bulkWrite(this.currentData, {
331
+ await db.common_current_data.bulkWrite(this.currentData, {
301
332
  session,
302
333
  // may update and delete data within the same batch - order matters
303
334
  ordered: true