@powersync/service-module-mongodb-storage 0.18.2 → 0.19.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 (143) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
  3. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
  4. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
  5. package/dist/storage/MongoBucketStorage.d.ts +5 -1
  6. package/dist/storage/MongoBucketStorage.js +5 -1
  7. package/dist/storage/MongoBucketStorage.js.map +1 -1
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +3 -0
  9. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  10. package/dist/storage/implementation/MongoChecksums.d.ts +2 -0
  11. package/dist/storage/implementation/MongoChecksums.js +3 -2
  12. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  13. package/dist/storage/implementation/MongoCompactor.d.ts +15 -19
  14. package/dist/storage/implementation/MongoCompactor.js +68 -260
  15. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  16. package/dist/storage/implementation/MongoStorageProvider.js +26 -3
  17. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  18. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +12 -5
  19. package/dist/storage/implementation/MongoSyncBucketStorage.js +25 -14
  20. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  21. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
  22. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +146 -34
  23. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  24. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +2 -0
  25. package/dist/storage/implementation/common/PersistedBatch.d.ts +6 -0
  26. package/dist/storage/implementation/common/PersistedBatch.js +15 -8
  27. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  28. package/dist/storage/implementation/models.d.ts +16 -0
  29. package/dist/storage/implementation/models.js.map +1 -1
  30. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
  31. package/dist/storage/implementation/v1/MongoCompactorV1.js +250 -1
  32. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  33. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +20 -11
  34. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  35. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
  36. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
  37. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
  38. package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
  39. package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
  40. package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
  41. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +3 -1
  42. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  43. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +1 -0
  44. package/dist/storage/implementation/v3/MongoChecksumsV3.js +59 -103
  45. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  46. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -2
  47. package/dist/storage/implementation/v3/MongoCompactorV3.js +354 -219
  48. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  49. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +5 -0
  50. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +12 -11
  51. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  52. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +5 -3
  53. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +109 -106
  54. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  55. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +5 -2
  56. package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -15
  57. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  58. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +2 -1
  59. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
  60. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  61. package/dist/storage/implementation/v3/bucket-format.d.ts +10 -1
  62. package/dist/storage/implementation/v3/bucket-format.js +28 -9
  63. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  64. package/dist/storage/implementation/v3/chunking.js +3 -1
  65. package/dist/storage/implementation/v3/chunking.js.map +1 -1
  66. package/dist/storage/implementation/v3/models.d.ts +38 -1
  67. package/dist/storage/implementation/v3/models.js.map +1 -1
  68. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
  69. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
  70. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
  71. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
  72. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
  73. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
  74. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
  75. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
  76. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
  77. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
  78. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
  79. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
  80. package/dist/types/types.d.ts +35 -0
  81. package/dist/types/types.js +35 -1
  82. package/dist/types/types.js.map +1 -1
  83. package/dist/utils/test-utils.js +3 -1
  84. package/dist/utils/test-utils.js.map +1 -1
  85. package/dist/utils/util.d.ts +8 -1
  86. package/dist/utils/util.js +125 -2
  87. package/dist/utils/util.js.map +1 -1
  88. package/package.json +10 -7
  89. package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
  90. package/src/storage/MongoBucketStorage.ts +10 -2
  91. package/src/storage/implementation/MongoBucketBatch.ts +4 -0
  92. package/src/storage/implementation/MongoChecksums.ts +5 -2
  93. package/src/storage/implementation/MongoCompactor.ts +90 -314
  94. package/src/storage/implementation/MongoStorageProvider.ts +32 -3
  95. package/src/storage/implementation/MongoSyncBucketStorage.ts +34 -24
  96. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +164 -37
  97. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +2 -0
  98. package/src/storage/implementation/common/PersistedBatch.ts +22 -10
  99. package/src/storage/implementation/models.ts +16 -0
  100. package/src/storage/implementation/v1/MongoCompactorV1.ts +292 -8
  101. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +54 -40
  102. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
  103. package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
  104. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +3 -1
  105. package/src/storage/implementation/v3/MongoChecksumsV3.ts +75 -105
  106. package/src/storage/implementation/v3/MongoCompactorV3.ts +456 -258
  107. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +21 -19
  108. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +123 -126
  109. package/src/storage/implementation/v3/PersistedBatchV3.ts +69 -16
  110. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +5 -0
  111. package/src/storage/implementation/v3/bucket-format.ts +36 -9
  112. package/src/storage/implementation/v3/chunking.ts +3 -1
  113. package/src/storage/implementation/v3/models.ts +40 -1
  114. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
  115. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
  116. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
  117. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
  118. package/src/types/types.ts +42 -1
  119. package/src/utils/test-utils.ts +3 -1
  120. package/src/utils/util.ts +182 -4
  121. package/test/src/__snapshots__/storage_sync.test.ts.snap +1 -583
  122. package/test/src/cleanup-stopped-sync-configs.test.ts +24 -1
  123. package/test/src/helpers/MemoryObjectStorage.ts +53 -0
  124. package/test/src/helpers/s3TestFactory.ts +69 -0
  125. package/test/src/object_storage_lifecycle.test.ts +59 -0
  126. package/test/src/setup.ts +6 -1
  127. package/test/src/storage.test.ts +216 -2
  128. package/test/src/storage_compacting.test.ts +176 -187
  129. package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
  130. package/test/src/storage_s3_checksums.test.ts +191 -0
  131. package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
  132. package/test/src/storage_s3_reading.test.ts +526 -0
  133. package/test/src/storage_s3_writing.test.ts +135 -0
  134. package/test/src/storage_sync.test.ts +95 -5
  135. package/tsconfig.tsbuildinfo +1 -1
  136. package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
  137. package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
  138. package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
  139. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
  140. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -84
  141. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
  142. package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
  143. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -108
@@ -1,20 +1,25 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import { ReplicationAssertionError } from '@powersync/lib-services-framework';
3
- import { storage } from '@powersync/service-core';
3
+ import { addChecksums, storage, utils } from '@powersync/service-core';
4
4
  import { BucketDefinitionId } from '@powersync/service-sync-rules';
5
- import { SingleBucketStore } from '../common/SingleBucketStore.js';
5
+ import { BucketDataDoc } from '../common/BucketDataDoc.js';
6
6
  import { BucketStateDocumentBase, LEGACY_BUCKET_DATA_DEFINITION_ID } from '../models.js';
7
- import { DirtyBucket, MongoCompactor } from '../MongoCompactor.js';
8
- import { BucketStateDocumentV1 } from './models.js';
7
+ import { CurrentBucketState, DirtyBucket, MongoCompactor } from '../MongoCompactor.js';
8
+ import { cacheKey } from '../OperationBatch.js';
9
+ import { BucketDataDocumentV1, BucketStateDocumentV1 } from './models.js';
9
10
  import type { MongoSyncBucketStorageV1 } from './MongoSyncBucketStorageV1.js';
10
11
  import { SingleBucketStoreV1 } from './SingleBucketStoreV1.js';
11
12
  import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
12
13
 
14
+ type CompactClearProperties = 'op' | 'checksum' | 'target_op';
15
+
13
16
  export class MongoCompactorV1 extends MongoCompactor {
14
17
  // Override types to the more specific ones
15
18
  declare protected readonly db: VersionedPowerSyncMongoV1;
16
19
  declare protected readonly storage: MongoSyncBucketStorageV1;
17
20
 
21
+ private updates: mongo.AnyBulkWriteOperation<BucketDataDocumentV1>[] = [];
22
+
18
23
  public async *dirtyBucketBatches(options: {
19
24
  minBucketChanges: number;
20
25
  minChangeRatio: number;
@@ -81,14 +86,293 @@ export class MongoCompactorV1 extends MongoCompactor {
81
86
  };
82
87
  }
83
88
 
84
- protected async getBucketDataContext(
85
- bucket: string,
86
- _definitionId: BucketDefinitionId | null
87
- ): Promise<SingleBucketStore | null> {
89
+ private getBucketDataContext(bucket: string): SingleBucketStoreV1 {
88
90
  return new SingleBucketStoreV1(this.db, {
89
91
  replicationStreamId: this.group_id,
90
92
  definitionId: LEGACY_BUCKET_DATA_DEFINITION_ID,
91
93
  bucket
92
94
  });
93
95
  }
96
+
97
+ protected async compactSingleBucket(bucket: string) {
98
+ // Do not carry queued writes from a failed attempt into the rescan
99
+ this.updates = [];
100
+
101
+ const idLimitBytes = this.idLimitBytes;
102
+ const bucketContext = this.getBucketDataContext(bucket);
103
+ const currentState: CurrentBucketState = {
104
+ bucket,
105
+ definitionId: bucketContext.key.definitionId,
106
+ seen: new Map(),
107
+ trackingSize: 0,
108
+ lastNotPut: null,
109
+ opsSincePut: 0,
110
+ checksum: 0,
111
+ opCount: 0,
112
+ opBytes: 0
113
+ };
114
+
115
+ // Constant lower bound.
116
+ const lowerBound = bucketContext.minId;
117
+ // Upper bound is adjusted for each batch.
118
+ let upperBound = bucketContext.maxId;
119
+
120
+ // Paginate through bucket data in batches to avoid cursor timeouts.
121
+ while (true) {
122
+ this.signal?.throwIfAborted();
123
+
124
+ // Query one batch at a time, to avoid cursor timeouts.
125
+ const pipeline = [
126
+ {
127
+ $match: {
128
+ _id: {
129
+ $gte: lowerBound,
130
+ $lt: upperBound
131
+ }
132
+ }
133
+ },
134
+ { $sort: { _id: -1 } },
135
+ { $limit: this.moveBatchQueryLimit },
136
+ {
137
+ $project: {
138
+ _id: 1,
139
+ op: 1,
140
+ table: 1,
141
+ row_id: 1,
142
+ source_table: 1,
143
+ source_key: 1,
144
+ checksum: 1,
145
+ size: { $bsonSize: '$$ROOT' }
146
+ }
147
+ }
148
+ ];
149
+
150
+ const cursor = bucketContext.collection.aggregate<BucketDataDocumentV1 & { size: number | bigint }>(pipeline, {
151
+ // batchSize is 1 more than limit to auto-close the cursor.
152
+ // See https://github.com/mongodb/node-mongodb-native/pull/4580
153
+ batchSize: this.moveBatchQueryLimit + 1
154
+ });
155
+ // We don't limit to a single batch here, since that often causes MongoDB to scan through more than it returns.
156
+ // Instead, we load up to the limit.
157
+ const rawBatch = await cursor.toArray();
158
+ const batch = rawBatch.map((document) => {
159
+ const { size, ...rest } = document;
160
+ return {
161
+ doc: bucketContext.fromPersistedDocument(rest),
162
+ size
163
+ };
164
+ });
165
+
166
+ if (batch.length == 0) {
167
+ // We've reached the end.
168
+ break;
169
+ }
170
+
171
+ // Reuse the exact collection _id value from Mongo for the next bound.
172
+ upperBound = rawBatch[rawBatch.length - 1]._id;
173
+
174
+ for (const { doc, size } of batch) {
175
+ if (doc.o > this.maxOpId) {
176
+ continue;
177
+ }
178
+
179
+ currentState.checksum = addChecksums(currentState.checksum, Number(doc.checksum));
180
+ currentState.opCount += 1;
181
+
182
+ let isPersistentPut = doc.op == 'PUT';
183
+
184
+ currentState.opBytes += Number(size);
185
+ if (doc.op == 'REMOVE' || doc.op == 'PUT') {
186
+ const key = `${doc.table}/${doc.row_id}/${cacheKey(doc.source_table!, doc.source_key!)}`;
187
+ const targetOp = currentState.seen.get(key);
188
+ if (targetOp) {
189
+ // Will convert to MOVE, so don't count as PUT.
190
+ isPersistentPut = false;
191
+
192
+ this.updates.push({
193
+ updateOne: {
194
+ filter: { _id: bucketContext.docId(doc.o) },
195
+ update: {
196
+ $set: {
197
+ op: 'MOVE',
198
+ target_op: targetOp
199
+ },
200
+ $unset: {
201
+ source_table: 1,
202
+ source_key: 1,
203
+ table: 1,
204
+ row_id: 1,
205
+ data: 1
206
+ }
207
+ } satisfies mongo.UpdateFilter<BucketDataDocumentV1>
208
+ }
209
+ });
210
+
211
+ // TODO: better estimate for this.
212
+ currentState.opBytes += 200 - Number(size);
213
+ } else if (currentState.trackingSize < idLimitBytes) {
214
+ // flatstr reduces the memory usage by flattening the string.
215
+ currentState.seen.set(utils.flatstr(key), doc.o);
216
+ // length + 16 for the string
217
+ // 24 for the bigint
218
+ // 50 for map overhead
219
+ // 50 for additional overhead
220
+ currentState.trackingSize += key.length + 140;
221
+ }
222
+ }
223
+
224
+ if (isPersistentPut) {
225
+ currentState.lastNotPut = null;
226
+ currentState.opsSincePut = 0;
227
+ } else if (doc.op != 'CLEAR') {
228
+ if (currentState.lastNotPut == null) {
229
+ currentState.lastNotPut = doc.o;
230
+ }
231
+ currentState.opsSincePut += 1;
232
+ }
233
+
234
+ if (this.updates.length + this.bucketStateUpdates.length >= this.moveBatchLimit) {
235
+ await this.flush(bucketContext);
236
+ }
237
+ }
238
+
239
+ this.logger.info(`Processed batch of length ${batch.length} current bucket: ${bucket}`);
240
+ }
241
+
242
+ // Free memory before clearing the bucket.
243
+ currentState.seen.clear();
244
+ if (currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
245
+ this.logger.info(
246
+ `Inserting CLEAR at ${this.group_id}:${bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
247
+ );
248
+ // Need flush() before clear().
249
+ await this.flush(bucketContext);
250
+ await this.clearBucket(currentState, bucketContext);
251
+ }
252
+
253
+ // Do this after clearBucket so we have accurate counts.
254
+ this.updateBucketChecksums(currentState, this.maxOpId);
255
+ // Need another flush after updateBucketChecksums().
256
+ await this.flush(bucketContext);
257
+ }
258
+
259
+ private async flush(bucketContext: SingleBucketStoreV1) {
260
+ if (this.updates.length > 0) {
261
+ this.logger.info(`Compacting ${this.updates.length} ops`);
262
+ await bucketContext.collection.bulkWrite(this.updates, {
263
+ // Order is not important. Since checksums are not affected, these operations can happen in any order,
264
+ // and it's fine if the operations are partially applied. Each individual operation is atomic.
265
+ ordered: false
266
+ });
267
+ this.updates = [];
268
+ }
269
+
270
+ await this.flushBucketStateUpdates();
271
+ }
272
+
273
+ /**
274
+ * Perform a CLEAR compact for a bucket.
275
+ *
276
+ * @param currentState tracks the last non-PUT op, which will be converted to CLEAR.
277
+ */
278
+ private async clearBucket(currentState: CurrentBucketState, bucketContext: SingleBucketStoreV1) {
279
+ const clearOp = currentState.lastNotPut!;
280
+
281
+ const opFilter = {
282
+ _id: {
283
+ $gte: bucketContext.minId,
284
+ $lte: bucketContext.docId(clearOp)
285
+ }
286
+ };
287
+
288
+ const session = this.db.client.startSession();
289
+ try {
290
+ let done = false;
291
+ while (!done) {
292
+ this.signal?.throwIfAborted();
293
+ let opCountDiff = 0;
294
+ // Do the CLEAR operation in batches, with each batch a separate transaction.
295
+ // The state after each batch is fully consistent.
296
+ // We need a transaction per batch to make sure checksums stay consistent.
297
+ await session.withTransaction(
298
+ async () => {
299
+ const query = bucketContext.collection.find<Pick<BucketDataDocumentV1, '_id' | CompactClearProperties>>(
300
+ opFilter,
301
+ {
302
+ session,
303
+ sort: { _id: 1 },
304
+ projection: {
305
+ _id: 1,
306
+ op: 1,
307
+ checksum: 1,
308
+ target_op: 1
309
+ },
310
+ limit: this.clearBatchLimit
311
+ }
312
+ );
313
+ let checksum = 0;
314
+ let lastOp: Pick<BucketDataDoc, 'o' | CompactClearProperties> | null = null;
315
+ let targetOp: bigint | null = null;
316
+ let gotAnOp = false;
317
+ let numberOfOpsToClear = 0;
318
+ for await (const rawOp of query.stream()) {
319
+ const op = bucketContext.fromPartialPersistedDocument(rawOp);
320
+
321
+ if (op.op == 'MOVE' || op.op == 'REMOVE' || op.op == 'CLEAR') {
322
+ checksum = utils.addChecksums(checksum, Number(op.checksum));
323
+ lastOp = op;
324
+ numberOfOpsToClear += 1;
325
+ if (op.op != 'CLEAR') {
326
+ gotAnOp = true;
327
+ }
328
+ if (op.target_op != null && (targetOp == null || op.target_op > targetOp)) {
329
+ targetOp = op.target_op;
330
+ }
331
+ } else {
332
+ throw new ReplicationAssertionError(`Unexpected ${op.op} operation at ${this.formatBucketDataKey(op)}`);
333
+ }
334
+ }
335
+ if (!gotAnOp) {
336
+ done = true;
337
+ return;
338
+ }
339
+
340
+ this.logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${lastOp?.o}`);
341
+ await bucketContext.collection.deleteMany(
342
+ {
343
+ _id: {
344
+ $gte: bucketContext.minId,
345
+ $lte: bucketContext.docId(lastOp!.o)
346
+ }
347
+ },
348
+ { session }
349
+ );
350
+
351
+ const op = bucketContext.toPersistedDocument({
352
+ o: lastOp!.o,
353
+ op: 'CLEAR',
354
+ checksum: BigInt(checksum),
355
+ data: null,
356
+ target_op: targetOp
357
+ });
358
+ await bucketContext.collection.insertOne(op, { session });
359
+
360
+ opCountDiff = -numberOfOpsToClear + 1;
361
+ },
362
+ {
363
+ writeConcern: { w: 'majority' },
364
+ readConcern: { level: 'snapshot' }
365
+ }
366
+ );
367
+ // Update outside the transaction, since the transaction can be retried multiple times.
368
+ currentState.opCount += opCountDiff;
369
+ }
370
+ } finally {
371
+ await session.endSession();
372
+ }
373
+ }
374
+
375
+ private formatBucketDataKey(doc: Pick<BucketDataDoc, 'bucketKey' | 'o'>) {
376
+ return `${doc.bucketKey.replicationStreamId}:${doc.bucketKey.bucket}:${doc.o}`;
377
+ }
94
378
  }
@@ -16,7 +16,15 @@ import {
16
16
  import { JSONBig } from '@powersync/service-jsonbig';
17
17
  import { ParameterLookupRows, ScopedParameterLookup, SqliteJsonRow } from '@powersync/service-sync-rules';
18
18
  import * as bson from 'bson';
19
- import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '../../../utils/util.js';
19
+ import {
20
+ clearCollectionInIdBatches,
21
+ clearCollectionInIdRanges,
22
+ idPrefixFilter,
23
+ mapOpEntry,
24
+ readSingleBatch,
25
+ retryOnMongoMaxTimeMSExpired,
26
+ setSessionSnapshotTime
27
+ } from '../../../utils/util.js';
20
28
  import { MongoBucketStorage } from '../../MongoBucketStorage.js';
21
29
  import { MongoSyncBucketStorageCheckpoint } from '../common/MongoSyncBucketStorageCheckpoint.js';
22
30
  import { SourceKey } from '../models.js';
@@ -214,64 +222,59 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
214
222
  }
215
223
 
216
224
  protected async clearBucketData(signal?: AbortSignal): Promise<void> {
217
- await this.clearDeleteMany(
225
+ await clearCollectionInIdRanges(
226
+ this.logger,
218
227
  'bucket data',
219
- () =>
220
- this.db.bucket_data.deleteMany(
221
- {
222
- _id: idPrefixFilter<BucketDataKeyV1>({ g: this.replicationStreamId }, ['b', 'o'])
223
- },
224
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
225
- ),
226
- signal
228
+ this.db.bucket_data,
229
+ {
230
+ _id: idPrefixFilter<BucketDataKeyV1>({ g: this.replicationStreamId }, ['b', 'o'])
231
+ },
232
+ signal,
233
+ this.clearBatchThrottleRate
227
234
  );
228
235
  }
229
236
 
230
237
  protected async clearParameterIndexes(signal?: AbortSignal): Promise<void> {
231
- await this.clearDeleteMany(
238
+ await clearCollectionInIdBatches(
239
+ this.logger,
232
240
  'parameter index',
233
- () =>
234
- this.db.parameterIndexV1.deleteMany(
235
- {
236
- 'key.g': this.replicationStreamId
237
- },
238
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
239
- ),
240
- signal
241
+ this.db.parameterIndexV1,
242
+ {
243
+ 'key.g': this.replicationStreamId
244
+ },
245
+ signal,
246
+ this.clearBatchThrottleRate
241
247
  );
242
248
  }
243
249
 
244
250
  protected async clearSourceRecords(signal?: AbortSignal): Promise<void> {
245
- await this.clearDeleteMany(
251
+ await clearCollectionInIdRanges(
252
+ this.logger,
246
253
  'source records',
247
- () =>
248
- this.db.sourceRecordsV1.deleteMany(
249
- {
250
- _id: idPrefixFilter<SourceKey>({ g: this.replicationStreamId }, ['t', 'k'])
251
- },
252
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
253
- ),
254
- signal
254
+ this.db.sourceRecordsV1,
255
+ {
256
+ _id: idPrefixFilter<SourceKey>({ g: this.replicationStreamId }, ['t', 'k'])
257
+ },
258
+ signal,
259
+ this.clearBatchThrottleRate
255
260
  );
256
261
  }
257
262
 
258
263
  protected async clearBucketState(signal?: AbortSignal): Promise<void> {
259
- await this.clearDeleteMany(
264
+ await clearCollectionInIdRanges(
265
+ this.logger,
260
266
  'bucket state',
261
- () =>
262
- this.db.bucketStateV1.deleteMany(
263
- {
264
- _id: idPrefixFilter<BucketStateDocument['_id']>({ g: this.replicationStreamId }, ['b'])
265
- },
266
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
267
- ),
268
- signal
267
+ this.db.bucketStateV1,
268
+ {
269
+ _id: idPrefixFilter<BucketStateDocument['_id']>({ g: this.replicationStreamId }, ['b'])
270
+ },
271
+ signal,
272
+ this.clearBatchThrottleRate
269
273
  );
270
274
  }
271
275
 
272
276
  protected async clearSourceTables(signal?: AbortSignal): Promise<void> {
273
- await this.clearDeleteMany(
274
- 'source tables',
277
+ await retryOnMongoMaxTimeMSExpired(
275
278
  () =>
276
279
  this.db.sourceTablesV1(this.replicationStreamId).deleteMany(
277
280
  {
@@ -279,7 +282,17 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
279
282
  },
280
283
  { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
281
284
  ),
282
- signal
285
+ {
286
+ signal,
287
+ abortMessage: 'Aborted clearing data',
288
+ // This is a fairly long delay - only expected to hit this when the storage database is under high load.
289
+ retryDelayMs: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS,
290
+ onRetry: () => {
291
+ this.logger.info(
292
+ `Clearing batch of source tables timed out after ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, retrying...`
293
+ );
294
+ }
295
+ }
283
296
  );
284
297
  }
285
298
 
@@ -389,6 +402,7 @@ export async function* getBucketDataBatchV1(
389
402
 
390
403
  if (session != null) {
391
404
  session.advanceOperationTime(checkpoint.snapshotTime);
405
+ session.advanceClusterTime(checkpoint.clusterTime);
392
406
  }
393
407
 
394
408
  let filters: mongo.Filter<BucketDataDocumentV1>[] = [];
@@ -1,58 +1,50 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import { InternalOpId } from '@powersync/service-core';
3
3
  import { BucketDataDoc, BucketKey } from '../common/BucketDataDoc.js';
4
- import {
5
- BucketDataDocumentGeneric,
6
- BucketDataDocumentGenericId,
7
- SingleBucketStore
8
- } from '../common/SingleBucketStore.js';
9
4
  import { BucketDataProperties } from '../models.js';
10
5
  import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
11
6
  import { BucketDataDocumentV1, BucketDataKeyV1, serializeBucketDataV1 } from './models.js';
12
7
 
13
- export class SingleBucketStoreV1 implements SingleBucketStore {
14
- public readonly collection: mongo.Collection<BucketDataDocumentGeneric>;
8
+ export class SingleBucketStoreV1 {
9
+ public readonly collection: mongo.Collection<BucketDataDocumentV1>;
15
10
 
16
11
  constructor(
17
- private db: VersionedPowerSyncMongoV1,
12
+ db: VersionedPowerSyncMongoV1,
18
13
  public readonly key: BucketKey
19
14
  ) {
20
- this.collection = db.bucketDataV1 as unknown as mongo.Collection<BucketDataDocumentGeneric>;
15
+ this.collection = db.bucketDataV1;
21
16
  }
22
17
 
23
- docId(o: InternalOpId): BucketDataDocumentGenericId {
24
- // `satisfies BucketDataKeyV1` checks that we use the correct type for V1 storage
25
- // `as anyt` is to allow casting to the interface virtual type
18
+ docId(o: InternalOpId): BucketDataKeyV1 {
26
19
  return {
27
20
  g: this.key.replicationStreamId,
28
21
  b: this.key.bucket,
29
22
  o
30
- } satisfies BucketDataKeyV1 as any;
23
+ };
31
24
  }
32
25
 
33
- get minId(): BucketDataDocumentGenericId {
26
+ get minId(): BucketDataKeyV1 {
34
27
  return {
35
28
  g: this.key.replicationStreamId,
36
29
  b: this.key.bucket,
37
- o: new mongo.MinKey()
38
- } as any;
30
+ o: new mongo.MinKey() as any
31
+ };
39
32
  }
40
33
 
41
- get maxId(): BucketDataDocumentGenericId {
34
+ get maxId(): BucketDataKeyV1 {
42
35
  return {
43
36
  g: this.key.replicationStreamId,
44
37
  b: this.key.bucket,
45
- o: new mongo.MaxKey()
46
- } as any;
38
+ o: new mongo.MaxKey() as any
39
+ };
47
40
  }
48
41
 
49
- toPersistedDocument(source: Omit<BucketDataDoc, 'bucketKey'>): BucketDataDocumentGeneric {
50
- return serializeBucketDataV1({ bucketKey: this.key, ...source }) as unknown as BucketDataDocumentGeneric;
42
+ toPersistedDocument(source: Omit<BucketDataDoc, 'bucketKey'>): BucketDataDocumentV1 {
43
+ return serializeBucketDataV1({ bucketKey: this.key, ...source });
51
44
  }
52
45
 
53
- fromPersistedDocument(doc: BucketDataDocumentGeneric): BucketDataDoc {
54
- const document = doc as unknown as BucketDataDocumentV1;
55
- const { _id, ...rest } = document;
46
+ fromPersistedDocument(doc: BucketDataDocumentV1): BucketDataDoc {
47
+ const { _id, ...rest } = doc;
56
48
  return {
57
49
  bucketKey: this.key,
58
50
  o: _id.o,
@@ -61,10 +53,9 @@ export class SingleBucketStoreV1 implements SingleBucketStore {
61
53
  }
62
54
 
63
55
  fromPartialPersistedDocument<T extends keyof BucketDataProperties>(
64
- doc: Pick<BucketDataDocumentGeneric, '_id' | T>
56
+ doc: Pick<BucketDataDocumentV1, '_id' | T>
65
57
  ): Pick<BucketDataDoc, 'bucketKey' | 'o' | T> {
66
- const document = doc as Pick<BucketDataDocumentV1, '_id' | T>;
67
- const { _id, ...rest } = document;
58
+ const { _id, ...rest } = doc;
68
59
  return {
69
60
  bucketKey: this.key,
70
61
  o: _id.o,
@@ -0,0 +1,32 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { InternalOpId } from '@powersync/service-core';
3
+ import { BucketKey } from '../common/BucketDataDoc.js';
4
+ import { BucketDataKey } from '../models.js';
5
+ import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
6
+ import { BucketDataDocumentV3 } from './models.js';
7
+
8
+ export class BucketDataContextV3 {
9
+ public readonly collection: mongo.Collection<BucketDataDocumentV3>;
10
+
11
+ constructor(
12
+ db: VersionedPowerSyncMongoV3,
13
+ public readonly key: BucketKey
14
+ ) {
15
+ this.collection = db.bucketData(key.replicationStreamId, key.definitionId);
16
+ }
17
+
18
+ docId(o: InternalOpId): BucketDataKey {
19
+ return {
20
+ b: this.key.bucket,
21
+ o
22
+ };
23
+ }
24
+
25
+ get minId(): BucketDataKey {
26
+ // MongoDB's MinKey sentinel does not match the bigint type for _id.o.
27
+ return {
28
+ b: this.key.bucket,
29
+ o: new mongo.MinKey()
30
+ } as unknown as BucketDataKey;
31
+ }
32
+ }
@@ -43,7 +43,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
43
43
 
44
44
  protected createPersistedBatch(writtenSize: number): PersistedBatch {
45
45
  return new PersistedBatchV3(this.db, this.replicationStreamId, this.mapping, writtenSize, {
46
- logger: this.logger
46
+ logger: this.logger,
47
+ objectStorage: this.options.objectStorage,
48
+ inlineThresholdBytes: this.options.inlineThresholdBytes
47
49
  });
48
50
  }
49
51