@powersync/service-module-mongodb-storage 0.13.2 → 0.15.0

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 (78) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/migrations/db/migrations/1770213298299-storage-version.d.ts +3 -0
  3. package/dist/migrations/db/migrations/1770213298299-storage-version.js +29 -0
  4. package/dist/migrations/db/migrations/1770213298299-storage-version.js.map +1 -0
  5. package/dist/storage/MongoBucketStorage.d.ts +7 -15
  6. package/dist/storage/MongoBucketStorage.js +28 -53
  7. package/dist/storage/MongoBucketStorage.js.map +1 -1
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +12 -11
  9. package/dist/storage/implementation/MongoBucketBatch.js +199 -127
  10. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  11. package/dist/storage/implementation/MongoChecksums.d.ts +8 -5
  12. package/dist/storage/implementation/MongoChecksums.js +8 -4
  13. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  14. package/dist/storage/implementation/MongoCompactor.d.ts +2 -2
  15. package/dist/storage/implementation/MongoCompactor.js +52 -26
  16. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  17. package/dist/storage/implementation/MongoParameterCompactor.d.ts +2 -2
  18. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  19. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +2 -12
  20. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +20 -25
  21. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  22. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +7 -4
  23. package/dist/storage/implementation/MongoSyncBucketStorage.js +11 -8
  24. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  25. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +3 -3
  26. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  27. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +4 -4
  28. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  29. package/dist/storage/implementation/OperationBatch.js +3 -2
  30. package/dist/storage/implementation/OperationBatch.js.map +1 -1
  31. package/dist/storage/implementation/PersistedBatch.d.ts +11 -4
  32. package/dist/storage/implementation/PersistedBatch.js +42 -11
  33. package/dist/storage/implementation/PersistedBatch.js.map +1 -1
  34. package/dist/storage/implementation/db.d.ts +35 -1
  35. package/dist/storage/implementation/db.js +99 -0
  36. package/dist/storage/implementation/db.js.map +1 -1
  37. package/dist/storage/implementation/models.d.ts +25 -1
  38. package/dist/storage/implementation/models.js +10 -1
  39. package/dist/storage/implementation/models.js.map +1 -1
  40. package/dist/storage/storage-index.d.ts +0 -1
  41. package/dist/storage/storage-index.js +0 -1
  42. package/dist/storage/storage-index.js.map +1 -1
  43. package/dist/utils/test-utils.d.ts +7 -5
  44. package/dist/utils/test-utils.js +17 -14
  45. package/dist/utils/test-utils.js.map +1 -1
  46. package/dist/utils/util.d.ts +2 -1
  47. package/dist/utils/util.js +15 -1
  48. package/dist/utils/util.js.map +1 -1
  49. package/package.json +7 -7
  50. package/src/migrations/db/migrations/1770213298299-storage-version.ts +44 -0
  51. package/src/storage/MongoBucketStorage.ts +44 -61
  52. package/src/storage/implementation/MongoBucketBatch.ts +253 -177
  53. package/src/storage/implementation/MongoChecksums.ts +19 -9
  54. package/src/storage/implementation/MongoCompactor.ts +62 -31
  55. package/src/storage/implementation/MongoParameterCompactor.ts +3 -3
  56. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +20 -34
  57. package/src/storage/implementation/MongoSyncBucketStorage.ts +32 -17
  58. package/src/storage/implementation/MongoSyncRulesLock.ts +3 -3
  59. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -4
  60. package/src/storage/implementation/OperationBatch.ts +3 -2
  61. package/src/storage/implementation/PersistedBatch.ts +42 -11
  62. package/src/storage/implementation/db.ts +129 -1
  63. package/src/storage/implementation/models.ts +39 -1
  64. package/src/storage/storage-index.ts +0 -1
  65. package/src/utils/test-utils.ts +18 -16
  66. package/src/utils/util.ts +17 -2
  67. package/test/src/__snapshots__/storage.test.ts.snap +198 -22
  68. package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
  69. package/test/src/__snapshots__/storage_sync.test.ts.snap +2211 -21
  70. package/test/src/storage.test.ts +9 -7
  71. package/test/src/storage_compacting.test.ts +33 -24
  72. package/test/src/storage_sync.test.ts +31 -15
  73. package/test/src/util.ts +4 -1
  74. package/tsconfig.tsbuildinfo +1 -1
  75. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -10
  76. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -17
  77. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  78. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -20
@@ -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,8 @@ 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';
17
+ import { StorageConfig } from './models.js';
16
18
 
17
19
  /**
18
20
  * Checksum calculation options, primarily for tests.
@@ -27,6 +29,8 @@ export interface MongoChecksumOptions {
27
29
  * Limit on the number of documents to calculate a checksum on at a time.
28
30
  */
29
31
  operationBatchLimit?: number;
32
+
33
+ storageConfig: StorageConfig;
30
34
  }
31
35
 
32
36
  const DEFAULT_BUCKET_BATCH_LIMIT = 200;
@@ -43,12 +47,15 @@ const DEFAULT_OPERATION_BATCH_LIMIT = 50_000;
43
47
  */
44
48
  export class MongoChecksums {
45
49
  private _cache: ChecksumCache | undefined;
50
+ private readonly storageConfig: StorageConfig;
46
51
 
47
52
  constructor(
48
- private db: PowerSyncMongo,
53
+ private db: VersionedPowerSyncMongo,
49
54
  private group_id: number,
50
- private options?: MongoChecksumOptions
51
- ) {}
55
+ private options: MongoChecksumOptions
56
+ ) {
57
+ this.storageConfig = options.storageConfig;
58
+ }
52
59
 
53
60
  /**
54
61
  * Lazy-instantiated cache.
@@ -68,7 +75,7 @@ export class MongoChecksums {
68
75
  * Calculate checksums, utilizing the cache for partial checkums, and querying the remainder from
69
76
  * the database (bucket_state + bucket_data).
70
77
  */
71
- async getChecksums(checkpoint: InternalOpId, buckets: string[]): Promise<ChecksumMap> {
78
+ async getChecksums(checkpoint: InternalOpId, buckets: BucketChecksumRequest[]): Promise<ChecksumMap> {
72
79
  return this.cache.getChecksumMap(checkpoint, buckets);
73
80
  }
74
81
 
@@ -222,6 +229,11 @@ export class MongoChecksums {
222
229
  });
223
230
  }
224
231
 
232
+ // Historically, checksum may be stored as 'int' or 'double'.
233
+ // More recently, this should be a 'long'.
234
+ // $toLong ensures that we always sum it as a long, avoiding inaccuracies in the calculations.
235
+ const checksumLong = this.storageConfig.longChecksums ? '$checksum' : { $toLong: '$checksum' };
236
+
225
237
  // Aggregate over a max of `batchLimit` operations at a time.
226
238
  // Let's say we have 3 buckets (A, B, C), each with 10 operations, and our batch limit is 12.
227
239
  // Then we'll do three batches:
@@ -245,10 +257,7 @@ export class MongoChecksums {
245
257
  {
246
258
  $group: {
247
259
  _id: '$_id.b',
248
- // Historically, checksum may be stored as 'int' or 'double'.
249
- // More recently, this should be a 'long'.
250
- // $toLong ensures that we always sum it as a long, avoiding inaccuracies in the calculations.
251
- checksum_total: { $sum: { $toLong: '$checksum' } },
260
+ checksum_total: { $sum: checksumLong },
252
261
  count: { $sum: 1 },
253
262
  has_clear_op: {
254
263
  $max: {
@@ -290,6 +299,7 @@ export class MongoChecksums {
290
299
  const req = requests.get(bucket);
291
300
  requests.set(bucket, {
292
301
  bucket,
302
+ source: req!.source,
293
303
  start: doc.last_op,
294
304
  end: req!.end
295
305
  });
@@ -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';
@@ -63,6 +63,7 @@ const DEFAULT_MOVE_BATCH_LIMIT = 2000;
63
63
  const DEFAULT_MOVE_BATCH_QUERY_LIMIT = 10_000;
64
64
  const DEFAULT_MIN_BUCKET_CHANGES = 10;
65
65
  const DEFAULT_MIN_CHANGE_RATIO = 0.1;
66
+ const DIRTY_BUCKET_SCAN_BATCH_SIZE = 2_000;
66
67
 
67
68
  /** This default is primarily for tests. */
68
69
  const DEFAULT_MEMORY_LIMIT_MB = 64;
@@ -84,19 +85,19 @@ export class MongoCompactor {
84
85
 
85
86
  constructor(
86
87
  private storage: MongoSyncBucketStorage,
87
- private db: PowerSyncMongo,
88
- options?: MongoCompactOptions
88
+ private db: VersionedPowerSyncMongo,
89
+ options: MongoCompactOptions
89
90
  ) {
90
91
  this.group_id = storage.group_id;
91
- this.idLimitBytes = (options?.memoryLimitMB ?? DEFAULT_MEMORY_LIMIT_MB) * 1024 * 1024;
92
- this.moveBatchLimit = options?.moveBatchLimit ?? DEFAULT_MOVE_BATCH_LIMIT;
93
- this.moveBatchQueryLimit = options?.moveBatchQueryLimit ?? DEFAULT_MOVE_BATCH_QUERY_LIMIT;
94
- this.clearBatchLimit = options?.clearBatchLimit ?? DEFAULT_CLEAR_BATCH_LIMIT;
95
- this.minBucketChanges = options?.minBucketChanges ?? DEFAULT_MIN_BUCKET_CHANGES;
96
- this.minChangeRatio = options?.minChangeRatio ?? DEFAULT_MIN_CHANGE_RATIO;
97
- this.maxOpId = options?.maxOpId ?? 0n;
98
- this.buckets = options?.compactBuckets;
99
- 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;
100
101
  }
101
102
 
102
103
  /**
@@ -538,31 +539,60 @@ export class MongoCompactor {
538
539
  let lastId = { g: this.group_id, b: new mongo.MinKey() as any };
539
540
  const maxId = { g: this.group_id, b: new mongo.MaxKey() as any };
540
541
  while (true) {
541
- const batch = await this.db.bucket_state
542
- .find(
543
- {
544
- _id: { $gt: lastId, $lt: maxId },
545
- 'estimate_since_compact.count': { $gte: options.minBucketChanges }
546
- },
547
- {
548
- projection: {
549
- _id: 1,
550
- estimate_since_compact: 1,
551
- compacted_state: 1
542
+ // To avoid timeouts from too many buckets not meeting the minBucketChanges criteria, we use an aggregation pipeline
543
+ // to scan a fixed batch of buckets at a time, but only return buckets that meet the criteria, rather than limiting
544
+ // on the output number.
545
+ const [result] = await this.db.bucket_state
546
+ .aggregate<{
547
+ buckets: Pick<BucketStateDocument, '_id' | 'estimate_since_compact' | 'compacted_state'>[];
548
+ cursor: Pick<BucketStateDocument, '_id'>[];
549
+ }>(
550
+ [
551
+ {
552
+ $match: {
553
+ _id: { $gt: lastId, $lt: maxId }
554
+ }
552
555
  },
553
- sort: {
554
- _id: 1
556
+ {
557
+ $sort: { _id: 1 }
555
558
  },
556
- limit: 2000,
557
- maxTimeMS: MONGO_OPERATION_TIMEOUT_MS
558
- }
559
+ {
560
+ // Scan a fixed number of docs each query so sparse matches don't block progress.
561
+ $limit: DIRTY_BUCKET_SCAN_BATCH_SIZE
562
+ },
563
+ {
564
+ $facet: {
565
+ // This is the results for the batch
566
+ buckets: [
567
+ {
568
+ $match: {
569
+ 'estimate_since_compact.count': { $gte: options.minBucketChanges }
570
+ }
571
+ },
572
+ {
573
+ $project: {
574
+ _id: 1,
575
+ estimate_since_compact: 1,
576
+ compacted_state: 1
577
+ }
578
+ }
579
+ ],
580
+ // This is used for the next query.
581
+ cursor: [{ $sort: { _id: -1 } }, { $limit: 1 }, { $project: { _id: 1 } }]
582
+ }
583
+ }
584
+ ],
585
+ { maxTimeMS: MONGO_OPERATION_TIMEOUT_MS }
559
586
  )
560
587
  .toArray();
561
- if (batch.length == 0) {
588
+
589
+ const cursor = result?.cursor?.[0];
590
+ if (cursor == null) {
562
591
  break;
563
592
  }
564
- lastId = batch[batch.length - 1]._id;
565
- const mapped = batch.map((b) => {
593
+ lastId = cursor._id;
594
+
595
+ const mapped = (result?.buckets ?? []).map((b) => {
566
596
  const updatedCount = b.estimate_since_compact?.count ?? 0;
567
597
  const totalCount = (b.compacted_state?.count ?? 0) + updatedCount;
568
598
  const updatedBytes = b.estimate_since_compact?.bytes ?? 0;
@@ -632,6 +662,7 @@ export class MongoCompactor {
632
662
  buckets.map((bucket) => {
633
663
  return {
634
664
  bucket,
665
+ source: {} as any,
635
666
  end: this.maxOpId
636
667
  };
637
668
  })
@@ -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,7 +14,7 @@ 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
@@ -1,52 +1,38 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import { storage } from '@powersync/service-core';
3
- import { SqlSyncRules } from '@powersync/service-sync-rules';
4
- import { MongoPersistedSyncRules } from './MongoPersistedSyncRules.js';
5
3
  import { MongoSyncRulesLock } from './MongoSyncRulesLock.js';
6
- import { PowerSyncMongo } from './db.js';
7
- import { SyncRuleDocument } from './models.js';
8
-
9
- export class MongoPersistedSyncRulesContent implements storage.PersistedSyncRulesContent {
10
- public readonly slot_name: string;
11
-
12
- public readonly id: number;
13
- public readonly sync_rules_content: string;
14
- public readonly last_checkpoint_lsn: string | null;
15
- public readonly last_fatal_error: string | null;
16
- public readonly last_fatal_error_ts: Date | null;
17
- public readonly last_keepalive_ts: Date | null;
18
- public readonly last_checkpoint_ts: Date | null;
19
- public readonly active: boolean;
4
+ import { PowerSyncMongo, VersionedPowerSyncMongo } from './db.js';
5
+ import { getMongoStorageConfig, SyncRuleDocument } from './models.js';
20
6
 
7
+ export class MongoPersistedSyncRulesContent extends storage.PersistedSyncRulesContent {
21
8
  public current_lock: MongoSyncRulesLock | null = null;
22
9
 
23
10
  constructor(
24
11
  private db: PowerSyncMongo,
25
12
  doc: mongo.WithId<SyncRuleDocument>
26
13
  ) {
27
- this.id = doc._id;
28
- this.sync_rules_content = doc.content;
29
- this.last_checkpoint_lsn = doc.last_checkpoint_lsn;
30
- // Handle legacy values
31
- this.slot_name = doc.slot_name ?? `powersync_${this.id}`;
32
- this.last_fatal_error = doc.last_fatal_error;
33
- this.last_fatal_error_ts = doc.last_fatal_error_ts;
34
- this.last_checkpoint_ts = doc.last_checkpoint_ts;
35
- this.last_keepalive_ts = doc.last_keepalive_ts;
36
- this.active = doc.state == 'ACTIVE';
14
+ super({
15
+ id: doc._id,
16
+ sync_rules_content: doc.content,
17
+ compiled_plan: doc.serialized_plan ?? null,
18
+ last_checkpoint_lsn: doc.last_checkpoint_lsn,
19
+ // Handle legacy values
20
+ slot_name: doc.slot_name ?? `powersync_${doc._id}`,
21
+ last_fatal_error: doc.last_fatal_error,
22
+ last_fatal_error_ts: doc.last_fatal_error_ts,
23
+ last_checkpoint_ts: doc.last_checkpoint_ts,
24
+ last_keepalive_ts: doc.last_keepalive_ts,
25
+ active: doc.state == 'ACTIVE',
26
+ storageVersion: doc.storage_version ?? storage.LEGACY_STORAGE_VERSION
27
+ });
37
28
  }
38
29
 
39
- parsed(options: storage.ParseSyncRulesOptions) {
40
- return new MongoPersistedSyncRules(
41
- this.id,
42
- SqlSyncRules.fromYaml(this.sync_rules_content, options),
43
- this.last_checkpoint_lsn,
44
- this.slot_name
45
- );
30
+ getStorageConfig() {
31
+ return getMongoStorageConfig(this.storageVersion);
46
32
  }
47
33
 
48
34
  async lock() {
49
- const lock = await MongoSyncRulesLock.createLock(this.db, this);
35
+ const lock = await MongoSyncRulesLock.createLock(this.db.versioned(this.getStorageConfig()), this);
50
36
  this.current_lock = lock;
51
37
  return lock;
52
38
  }
@@ -31,16 +31,25 @@ 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';
35
- import { BucketDataDocument, BucketDataKey, BucketStateDocument, SourceKey, SourceTableDocument } from './models.js';
34
+ import { VersionedPowerSyncMongo } from './db.js';
35
+ import {
36
+ BucketDataDocument,
37
+ BucketDataKey,
38
+ BucketStateDocument,
39
+ SourceKey,
40
+ SourceTableDocument,
41
+ StorageConfig
42
+ } from './models.js';
36
43
  import { MongoBucketBatch } from './MongoBucketBatch.js';
37
44
  import { MongoChecksumOptions, MongoChecksums } from './MongoChecksums.js';
38
45
  import { MongoCompactor } from './MongoCompactor.js';
39
46
  import { MongoParameterCompactor } from './MongoParameterCompactor.js';
47
+ import { MongoPersistedSyncRulesContent } from './MongoPersistedSyncRulesContent.js';
40
48
  import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
41
49
 
42
50
  export interface MongoSyncBucketStorageOptions {
43
- checksumOptions?: MongoChecksumOptions;
51
+ checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig'>;
52
+ storageConfig: StorageConfig;
44
53
  }
45
54
 
46
55
  /**
@@ -58,7 +67,7 @@ export class MongoSyncBucketStorage
58
67
  extends BaseObserver<storage.SyncRulesBucketStorageListener>
59
68
  implements storage.SyncRulesBucketStorage
60
69
  {
61
- private readonly db: PowerSyncMongo;
70
+ private readonly db: VersionedPowerSyncMongo;
62
71
  readonly checksums: MongoChecksums;
63
72
 
64
73
  private parsedSyncRulesCache: { parsed: HydratedSyncRules; options: storage.ParseSyncRulesOptions } | undefined;
@@ -67,14 +76,17 @@ export class MongoSyncBucketStorage
67
76
  constructor(
68
77
  public readonly factory: MongoBucketStorage,
69
78
  public readonly group_id: number,
70
- private readonly sync_rules: storage.PersistedSyncRulesContent,
79
+ private readonly sync_rules: MongoPersistedSyncRulesContent,
71
80
  public readonly slot_name: string,
72
- writeCheckpointMode?: storage.WriteCheckpointMode,
73
- options?: MongoSyncBucketStorageOptions
81
+ writeCheckpointMode: storage.WriteCheckpointMode | undefined,
82
+ options: MongoSyncBucketStorageOptions
74
83
  ) {
75
84
  super();
76
- this.db = factory.db;
77
- this.checksums = new MongoChecksums(this.db, this.group_id, options?.checksumOptions);
85
+ this.db = factory.db.versioned(sync_rules.getStorageConfig());
86
+ this.checksums = new MongoChecksums(this.db, this.group_id, {
87
+ ...options.checksumOptions,
88
+ storageConfig: options?.storageConfig
89
+ });
78
90
  this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
79
91
  db: this.db,
80
92
  mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
@@ -175,7 +187,6 @@ export class MongoSyncBucketStorage
175
187
  slotName: this.slot_name,
176
188
  lastCheckpointLsn: checkpoint_lsn,
177
189
  resumeFromLsn: maxLsn(checkpoint_lsn, doc?.snapshot_lsn),
178
- noCheckpointBeforeLsn: doc?.no_checkpoint_before ?? options.zeroLSN,
179
190
  keepaliveOp: doc?.keepalive_op ? BigInt(doc.keepalive_op) : null,
180
191
  storeCurrentData: options.storeCurrentData,
181
192
  skipExistingRows: options.skipExistingRows ?? false,
@@ -361,19 +372,20 @@ export class MongoSyncBucketStorage
361
372
 
362
373
  async *getBucketDataBatch(
363
374
  checkpoint: utils.InternalOpId,
364
- dataBuckets: Map<string, InternalOpId>,
375
+ dataBuckets: storage.BucketDataRequest[],
365
376
  options?: storage.BucketDataBatchOptions
366
377
  ): AsyncIterable<storage.SyncBucketDataChunk> {
367
- if (dataBuckets.size == 0) {
378
+ if (dataBuckets.length == 0) {
368
379
  return;
369
380
  }
370
381
  let filters: mongo.Filter<BucketDataDocument>[] = [];
382
+ const bucketMap = new Map(dataBuckets.map((request) => [request.bucket, request.start]));
371
383
 
372
384
  if (checkpoint == null) {
373
385
  throw new ServiceAssertionError('checkpoint is null');
374
386
  }
375
387
  const end = checkpoint;
376
- for (let [name, start] of dataBuckets.entries()) {
388
+ for (let { bucket: name, start } of dataBuckets) {
377
389
  filters.push({
378
390
  _id: {
379
391
  $gt: {
@@ -466,7 +478,7 @@ export class MongoSyncBucketStorage
466
478
  }
467
479
 
468
480
  if (start == null) {
469
- const startOpId = dataBuckets.get(bucket);
481
+ const startOpId = bucketMap.get(bucket);
470
482
  if (startOpId == null) {
471
483
  throw new ServiceAssertionError(`data for unexpected bucket: ${bucket}`);
472
484
  }
@@ -508,7 +520,10 @@ export class MongoSyncBucketStorage
508
520
  }
509
521
  }
510
522
 
511
- async getChecksums(checkpoint: utils.InternalOpId, buckets: string[]): Promise<utils.ChecksumMap> {
523
+ async getChecksums(
524
+ checkpoint: utils.InternalOpId,
525
+ buckets: storage.BucketChecksumRequest[]
526
+ ): Promise<utils.ChecksumMap> {
512
527
  return this.checksums.getChecksums(checkpoint, buckets);
513
528
  }
514
529
 
@@ -565,7 +580,7 @@ export class MongoSyncBucketStorage
565
580
  async clear(options?: storage.ClearStorageOptions): Promise<void> {
566
581
  while (true) {
567
582
  if (options?.signal?.aborted) {
568
- throw new ReplicationAbortedError('Aborted clearing data');
583
+ throw new ReplicationAbortedError('Aborted clearing data', options.signal.reason);
569
584
  }
570
585
  try {
571
586
  await this.clearIteration();
@@ -620,7 +635,7 @@ export class MongoSyncBucketStorage
620
635
  { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
621
636
  );
622
637
 
623
- await this.db.current_data.deleteMany(
638
+ await this.db.common_current_data.deleteMany(
624
639
  {
625
640
  _id: idPrefixFilter<SourceKey>({ g: this.group_id }, ['t', 'k'])
626
641
  },
@@ -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