@powersync/service-module-mongodb-storage 0.20.0 → 0.22.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 (192) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +36 -3
  3. package/dist/storage/MongoBucketStorage.js +372 -207
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +17 -5
  6. package/dist/storage/implementation/MongoBucketBatch.js +44 -17
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
  9. package/dist/storage/implementation/MongoCompactor.js +27 -238
  10. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  11. package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
  12. package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
  13. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
  15. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
  16. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  17. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
  18. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  19. package/dist/storage/implementation/MongoStorageProvider.js +7 -3
  20. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  21. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
  22. package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
  23. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  24. package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
  25. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  26. package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
  27. package/dist/storage/implementation/MongoWriteBatch.js +149 -0
  28. package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
  29. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
  30. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +9 -30
  31. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  32. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  33. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  34. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  35. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
  36. package/dist/storage/implementation/common/PersistedBatch.d.ts +32 -4
  37. package/dist/storage/implementation/common/PersistedBatch.js +49 -6
  38. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  39. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
  40. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
  41. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  42. package/dist/storage/implementation/db.d.ts +9 -1
  43. package/dist/storage/implementation/db.js +27 -1
  44. package/dist/storage/implementation/db.js.map +1 -1
  45. package/dist/storage/implementation/models.d.ts +31 -0
  46. package/dist/storage/implementation/models.js.map +1 -1
  47. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +2 -0
  48. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +33 -4
  49. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  50. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +38 -6
  51. package/dist/storage/implementation/v1/MongoCompactorV1.js +247 -20
  52. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  53. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  54. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  55. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  56. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  57. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
  58. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  59. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
  60. package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
  61. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  62. package/dist/storage/implementation/v1/models.js +1 -0
  63. package/dist/storage/implementation/v1/models.js.map +1 -1
  64. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  65. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  66. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  67. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +10 -2
  68. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +100 -26
  69. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  70. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +82 -25
  71. package/dist/storage/implementation/v3/MongoCompactorV3.js +698 -159
  72. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  73. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  74. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  75. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  76. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  77. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  78. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  79. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  80. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
  81. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  82. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  83. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  84. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  85. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +14 -8
  86. package/dist/storage/implementation/v3/PersistedBatchV3.js +76 -42
  87. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  88. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  89. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  90. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  91. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  92. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  93. package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
  94. package/dist/storage/implementation/v3/compact-utils.js +216 -0
  95. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  96. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  97. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  98. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  99. package/dist/storage/implementation/v3/models.d.ts +84 -4
  100. package/dist/storage/implementation/v3/models.js.map +1 -1
  101. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  102. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +16 -5
  103. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  104. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  105. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +6 -8
  106. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +17 -11
  107. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  108. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +38 -0
  109. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +164 -0
  110. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  111. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  112. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +197 -25
  113. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  114. package/dist/storage/implementation/v3/source-table-utils.d.ts +6 -6
  115. package/dist/storage/implementation/v3/source-table-utils.js +30 -37
  116. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  117. package/dist/storage/storage-index.d.ts +2 -1
  118. package/dist/storage/storage-index.js +1 -0
  119. package/dist/storage/storage-index.js.map +1 -1
  120. package/dist/types/types.d.ts +28 -1
  121. package/dist/types/types.js +28 -7
  122. package/dist/types/types.js.map +1 -1
  123. package/dist/utils/test-utils.js +1 -0
  124. package/dist/utils/test-utils.js.map +1 -1
  125. package/dist/utils/util.js +3 -1
  126. package/dist/utils/util.js.map +1 -1
  127. package/package.json +10 -9
  128. package/src/storage/MongoBucketStorage.ts +507 -269
  129. package/src/storage/implementation/MongoBucketBatch.ts +67 -24
  130. package/src/storage/implementation/MongoCompactor.ts +35 -346
  131. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  132. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
  133. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
  134. package/src/storage/implementation/MongoStorageProvider.ts +15 -3
  135. package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
  136. package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
  137. package/src/storage/implementation/MongoWriteBatch.ts +172 -0
  138. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +16 -46
  139. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  140. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  141. package/src/storage/implementation/common/PersistedBatch.ts +66 -10
  142. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
  143. package/src/storage/implementation/db.ts +35 -2
  144. package/src/storage/implementation/models.ts +34 -0
  145. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +46 -6
  146. package/src/storage/implementation/v1/MongoCompactorV1.ts +327 -40
  147. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  148. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
  149. package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
  150. package/src/storage/implementation/v1/models.ts +1 -0
  151. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  152. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +138 -34
  153. package/src/storage/implementation/v3/MongoCompactorV3.ts +958 -225
  154. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  155. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  156. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
  157. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  158. package/src/storage/implementation/v3/PersistedBatchV3.ts +93 -48
  159. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  160. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  161. package/src/storage/implementation/v3/compact-utils.ts +320 -0
  162. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  163. package/src/storage/implementation/v3/models.ts +91 -3
  164. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +24 -9
  165. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  166. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +26 -14
  167. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +229 -0
  168. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +279 -29
  169. package/src/storage/implementation/v3/source-table-utils.ts +39 -41
  170. package/src/storage/storage-index.ts +2 -0
  171. package/src/types/types.ts +42 -7
  172. package/src/utils/test-utils.ts +1 -0
  173. package/src/utils/util.ts +3 -1
  174. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  175. package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
  176. package/test/src/chunk_compaction_config.test.ts +38 -0
  177. package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
  178. package/test/src/compact-utils.test.ts +226 -0
  179. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  180. package/test/src/object_storage_usage.test.ts +267 -0
  181. package/test/src/parameter_compacting_v1.test.ts +120 -0
  182. package/test/src/parameter_compacting_v3.test.ts +192 -0
  183. package/test/src/parameter_compaction_fence.test.ts +276 -0
  184. package/test/src/storage.test.ts +273 -22
  185. package/test/src/storage_compacting.test.ts +968 -246
  186. package/test/src/storage_s3_checksums.test.ts +3 -4
  187. package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
  188. package/test/src/storage_s3_reading.test.ts +290 -2
  189. package/test/src/storage_s3_writing.test.ts +1 -0
  190. package/test/src/storage_sync.test.ts +336 -41
  191. package/test/src/util.ts +1 -1
  192. package/tsconfig.tsbuildinfo +1 -1
@@ -13,8 +13,10 @@ import {
13
13
  UpsertCurrentDataOptions
14
14
  } from '../common/PersistedBatch.js';
15
15
  import { SourceRecordLookupState } from '../common/SourceRecordStore.js';
16
+ import { MongoWriteBatch } from '../MongoWriteBatch.js';
16
17
  import { serializeBucketData } from './bucket-format.js';
17
18
  import { chunkBucketData } from './chunking.js';
19
+ import { DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS } from './compaction-constants.js';
18
20
  import {
19
21
  BucketDataDocumentV3,
20
22
  BucketStateDocumentV3,
@@ -24,12 +26,21 @@ import {
24
26
  taggedBucketParameterDocumentToTagged
25
27
  } from './models.js';
26
28
  import { ObjectStorageLifecycle } from './object-storage/ObjectStorageLifecycle.js';
29
+ import { ObjectStorageUsage } from './object-storage/ObjectStorageUsage.js';
27
30
  import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
28
31
 
32
+ interface SourceRecordWrite {
33
+ sourceTableId: bson.ObjectId;
34
+ operation: mongo.AnyBulkWriteOperation<CurrentDataDocumentV3>;
35
+ }
36
+
29
37
  export class PersistedBatchV3 extends PersistedBatch {
30
- currentData: { sourceTableId: bson.ObjectId; operation: mongo.AnyBulkWriteOperation<CurrentDataDocumentV3> }[] = [];
38
+ // Upserts and soft deletes supply the complete source-record state, including
39
+ // pending_delete. Keep the final state per key, without removing any history.
40
+ currentData = new Map<string, SourceRecordWrite>();
31
41
  sourceTablePendingDeletes = new Map<string, InternalOpId>();
32
42
  protected readonly objectStorageLifecycle?: ObjectStorageLifecycle;
43
+ protected readonly objectStorageUsage?: ObjectStorageUsage;
33
44
 
34
45
  declare protected readonly db: VersionedPowerSyncMongoV3;
35
46
 
@@ -43,6 +54,7 @@ export class PersistedBatchV3 extends PersistedBatch {
43
54
  super(db, group_id, mapping, writtenSize, options);
44
55
  if (this.objectStorage) {
45
56
  this.objectStorageLifecycle = new ObjectStorageLifecycle(this.db, this.group_id, this.objectStorage);
57
+ this.objectStorageUsage = new ObjectStorageUsage(this.db, this.group_id, this.objectStorageUsageWriterId);
46
58
  }
47
59
  }
48
60
 
@@ -127,7 +139,7 @@ export class PersistedBatchV3 extends PersistedBatch {
127
139
  }
128
140
 
129
141
  hardDeleteCurrentData(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId) {
130
- this.currentData.push({
142
+ this.currentData.set(this.currentDataKey(sourceTableId, replicaId), {
131
143
  sourceTableId,
132
144
  operation: {
133
145
  deleteOne: {
@@ -143,7 +155,7 @@ export class PersistedBatchV3 extends PersistedBatch {
143
155
  replicaId: storage.ReplicaId,
144
156
  checkpointGreaterThan: InternalOpId
145
157
  ) {
146
- this.currentData.push({
158
+ this.currentData.set(this.currentDataKey(sourceTableId, replicaId), {
147
159
  sourceTableId,
148
160
  operation: {
149
161
  updateOne: {
@@ -191,7 +203,7 @@ export class PersistedBatchV3 extends PersistedBatch {
191
203
  };
192
204
  });
193
205
 
194
- this.currentData.push({
206
+ this.currentData.set(this.currentDataKey(values.sourceTableId, values.replicaId), {
195
207
  sourceTableId: values.sourceTableId,
196
208
  operation: {
197
209
  updateOne: {
@@ -212,12 +224,12 @@ export class PersistedBatchV3 extends PersistedBatch {
212
224
  }
213
225
 
214
226
  protected get currentDataCount() {
215
- return this.currentData.length;
227
+ return this.currentData.size;
216
228
  }
217
229
 
218
230
  // Flush methods
219
231
 
220
- protected async flushBucketData(session: mongo.ClientSession) {
232
+ protected async queueBucketData(writes: MongoWriteBatch) {
221
233
  const operationsByDefinition = new Map<BucketDefinitionId, BucketDataDoc[]>();
222
234
  for (const document of this.bucketData) {
223
235
  const existing = operationsByDefinition.get(document.bucketKey.definitionId) ?? [];
@@ -225,15 +237,17 @@ export class PersistedBatchV3 extends PersistedBatch {
225
237
  operationsByDefinition.set(document.bucketKey.definitionId, existing);
226
238
  }
227
239
 
228
- let uploadCount = 0;
229
240
  const plans = Array.from(operationsByDefinition, ([definitionId, documents]) => {
230
241
  const operationsByBucket = Map.groupBy(documents, (document) => document.bucketKey.bucket);
231
242
  const lifecycle = this.objectStorageLifecycle;
232
- const createInserts: (() => Promise<mongo.AnyBulkWriteOperation<BucketDataDocumentV3>>)[] = [];
243
+ type BucketDataInsert = { insertOne: { document: BucketDataDocumentV3 } };
244
+ const createInserts: (() => Promise<BucketDataInsert>)[] = [];
233
245
 
234
246
  for (const [bucket, ops] of operationsByBucket) {
247
+ this.resetBucketPersistedBytes(definitionId, bucket);
235
248
  for (const chunk of chunkBucketData(ops)) {
236
249
  const serialized = serializeBucketData(bucket, chunk);
250
+ this.incrementBucketPersistedChunk(definitionId, bucket, serialized.size);
237
251
  if (lifecycle == null || serialized.size <= this.inlineThresholdBytes) {
238
252
  createInserts.push(async () => ({
239
253
  insertOne: {
@@ -243,13 +257,12 @@ export class PersistedBatchV3 extends PersistedBatch {
243
257
  continue;
244
258
  }
245
259
 
246
- uploadCount += 1;
247
260
  createInserts.push(async () => {
248
261
  const minOp = chunk[0].o;
249
262
  const maxOp = chunk[chunk.length - 1].o;
250
263
  const { ops: bucketOps, ...metadata } = serialized;
251
264
  const path = lifecycle.allocatePath(definitionId, bucket, minOp, maxOp);
252
- const { fileSize } = await lifecycle.bucketData.store(path, bucketOps!);
265
+ const { fileSize } = await lifecycle.bucketData.store(path, bucketOps!, { signal: this.signal });
253
266
  return {
254
267
  insertOne: {
255
268
  document: {
@@ -279,19 +292,29 @@ export class PersistedBatchV3 extends PersistedBatch {
279
292
  // S3ObjectStorage applies one shared concurrency limit across all callers,
280
293
  // so replication can schedule its uploads together without creating a
281
294
  // separate limiter here.
282
- const writes = await createAllInserts();
283
-
284
- for (const { definitionId, inserts } of writes) {
295
+ const bucketWrites = await createAllInserts();
296
+
297
+ const usageDeltas = this.objectStorageUsage == null ? undefined : new Map<BucketDefinitionId, bigint>();
298
+ for (const { definitionId, inserts } of bucketWrites) {
299
+ if (usageDeltas != null) {
300
+ const delta = inserts.reduce(
301
+ (sum, operation) => sum + ObjectStorageUsage.bytes(operation.insertOne.document),
302
+ 0n
303
+ );
304
+ if (delta !== 0n) {
305
+ usageDeltas.set(definitionId, delta);
306
+ }
307
+ }
285
308
  if (inserts.length > 0) {
286
- await this.db.bucketData(this.group_id, definitionId).bulkWrite(inserts, {
287
- session,
288
- ordered: false
289
- });
309
+ writes.bulkWriteUnordered(this.db.bucketData(this.group_id, definitionId), inserts);
290
310
  }
291
311
  }
312
+ if (usageDeltas != null) {
313
+ this.objectStorageUsage!.applyDeltas(usageDeltas, writes);
314
+ }
292
315
  }
293
316
 
294
- protected async flushBucketParameters(session: mongo.ClientSession) {
317
+ protected queueBucketParameters(writes: MongoWriteBatch): void {
295
318
  const operationsByIndex = new Map<string, typeof this.bucketParameters>();
296
319
  for (const document of this.bucketParameters) {
297
320
  const existing = operationsByIndex.get(document.index) ?? [];
@@ -300,23 +323,20 @@ export class PersistedBatchV3 extends PersistedBatch {
300
323
  }
301
324
 
302
325
  for (const [indexId, documents] of operationsByIndex.entries()) {
303
- await this.db.parameterIndex(this.group_id, indexId).bulkWrite(
326
+ writes.bulkWriteUnordered(
327
+ this.db.parameterIndex(this.group_id, indexId),
304
328
  documents.map((document) => ({
305
329
  insertOne: {
306
330
  document: taggedBucketParameterDocumentToTagged(document)
307
331
  }
308
- })),
309
- {
310
- session,
311
- ordered: false
312
- }
332
+ }))
313
333
  );
314
334
  }
315
335
  }
316
336
 
317
- protected async flushCurrentData(session: mongo.ClientSession) {
318
- const operationsBySourceTable = new Map<string, typeof this.currentData>();
319
- for (const operation of this.currentData) {
337
+ protected queueCurrentData(writes: MongoWriteBatch): void {
338
+ const operationsBySourceTable = new Map<string, SourceRecordWrite[]>();
339
+ for (const operation of this.currentData.values()) {
320
340
  const sourceTableId = operation.sourceTableId.toHexString();
321
341
  const existing = operationsBySourceTable.get(sourceTableId) ?? [];
322
342
  existing.push(operation);
@@ -339,33 +359,31 @@ export class PersistedBatchV3 extends PersistedBatch {
339
359
  });
340
360
 
341
361
  if (sourceTableUpdates.length > 0) {
342
- await this.db.sourceTables(this.group_id).bulkWrite(sourceTableUpdates, { session, ordered: false });
362
+ writes.bulkWriteUnordered(this.db.sourceTables(this.group_id), sourceTableUpdates);
343
363
  }
344
364
 
345
365
  for (const operations of operationsBySourceTable.values()) {
346
366
  const sourceTableId = operations[0]!.sourceTableId;
347
- await this.db.sourceRecords(this.group_id, sourceTableId).bulkWrite(
348
- operations.map((entry) => entry.operation),
349
- {
350
- session,
351
- ordered: true
352
- }
367
+ writes.bulkWriteUnordered(
368
+ this.db.sourceRecords(this.group_id, sourceTableId),
369
+ operations.map((entry) => entry.operation)
353
370
  );
354
371
  }
355
372
  }
356
373
 
357
- protected async flushBucketStates(session: mongo.ClientSession) {
358
- await this.db.bucketState(this.group_id).bulkWrite(this.getBucketStateUpdates(), {
359
- session,
360
- ordered: false
361
- });
374
+ protected queueBucketStates(writes: MongoWriteBatch): void {
375
+ writes.bulkWriteUnordered(this.db.bucketState(this.group_id), this.getBucketStateUpdates());
362
376
  }
363
377
 
364
378
  protected resetCurrentData() {
365
- this.currentData = [];
379
+ this.currentData.clear();
366
380
  this.sourceTablePendingDeletes.clear();
367
381
  }
368
382
 
383
+ private currentDataKey(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId): string {
384
+ return Buffer.from(bson.serialize({ t: sourceTableId, k: replicaId })).toString('base64');
385
+ }
386
+
369
387
  private getBucketStateUpdates(): mongo.AnyBulkWriteOperation<BucketStateDocumentV3>[] {
370
388
  return Array.from(this.bucketStates.values()).map((state: BucketStateUpdate) => {
371
389
  if (state.definitionId == null) {
@@ -379,15 +397,42 @@ export class PersistedBatchV3 extends PersistedBatch {
379
397
  b: state.bucket
380
398
  }
381
399
  },
382
- update: {
383
- $set: {
384
- last_op: state.lastOp
385
- },
386
- $inc: {
387
- 'estimate_since_compact.count': state.incrementCount,
388
- 'estimate_since_compact.bytes': state.incrementBytes
400
+ // A pipeline update makes initialisation and scheduling one atomic
401
+ // writer operation. In particular, a later write cannot move an
402
+ // already-due compact check into the future.
403
+ update: [
404
+ {
405
+ $set: {
406
+ last_op: state.lastOp,
407
+ bucket_stats: {
408
+ count: { $add: [{ $ifNull: ['$bucket_stats.count', 0] }, state.incrementCount] },
409
+ bytes: { $add: [{ $ifNull: ['$bucket_stats.bytes', 0n] }, BigInt(state.incrementBytes)] },
410
+ chunks: { $add: [{ $ifNull: ['$bucket_stats.chunks', 0] }, state.incrementChunks] }
411
+ },
412
+ first_uncompacted_write: { $ifNull: ['$first_uncompacted_write', '$$NOW'] },
413
+ next_compact_check: {
414
+ $let: {
415
+ vars: {
416
+ requested: {
417
+ $dateAdd: {
418
+ startDate: '$$NOW',
419
+ unit: 'millisecond',
420
+ amount: DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS
421
+ }
422
+ }
423
+ },
424
+ in: {
425
+ $cond: [
426
+ { $lt: [{ $ifNull: ['$next_compact_check', '$$requested'] }, '$$requested'] },
427
+ '$next_compact_check',
428
+ '$$requested'
429
+ ]
430
+ }
431
+ }
432
+ }
433
+ }
389
434
  }
390
- },
435
+ ],
391
436
  upsert: true
392
437
  }
393
438
  } satisfies mongo.AnyBulkWriteOperation<BucketStateDocumentV3>;
@@ -1,5 +1,5 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { BucketDefinitionId, ParameterIndexId } from '@powersync/service-sync-rules';
2
+ import { BucketDefinitionId, EventDefinitionId, ParameterIndexId } from '@powersync/service-sync-rules';
3
3
  import { BaseVersionedPowerSyncMongo } from '../common/VersionedPowerSyncMongoBase.js';
4
4
  import { CommonSourceTableDocument } from '../models.js';
5
5
  import {
@@ -7,11 +7,19 @@ import {
7
7
  BucketParameterDocumentV3,
8
8
  BucketStateDocumentV3,
9
9
  CurrentDataDocumentV3,
10
+ CustomCheckpointRequestDocumentV3,
10
11
  ObjectStorageDeletionMarker,
12
+ ObjectStorageUsageDocument,
11
13
  SourceTableDocumentV3,
12
14
  SyncConfigDefinition
13
15
  } from './models.js';
14
16
 
17
+ export const OBJECT_STORAGE_USAGE_COLLECTION = 'object_storage_usage';
18
+ export type CustomCheckpointRequestCollectionSpecifier = {
19
+ replicationStreamId: number;
20
+ eventId: EventDefinitionId;
21
+ };
22
+
15
23
  export class VersionedPowerSyncMongoV3 extends BaseVersionedPowerSyncMongo {
16
24
  constructor(
17
25
  upstream: ConstructorParameters<typeof BaseVersionedPowerSyncMongo>[0],
@@ -74,6 +82,42 @@ export class VersionedPowerSyncMongoV3 extends BaseVersionedPowerSyncMongo {
74
82
  return this.db.collection<ObjectStorageDeletionMarker>(`pending_object_storage_deletes_${replicationStreamId}`);
75
83
  }
76
84
 
85
+ get objectStorageUsage(): mongo.Collection<ObjectStorageUsageDocument> {
86
+ return this.db.collection<ObjectStorageUsageDocument>(OBJECT_STORAGE_USAGE_COLLECTION);
87
+ }
88
+
89
+ customCheckpointRequests(options: CustomCheckpointRequestCollectionSpecifier) {
90
+ const collectionName = this.customCheckpointRequestCollectionName(options);
91
+ return this.db.collection<CustomCheckpointRequestDocumentV3>(collectionName);
92
+ }
93
+
94
+ async initializeCustomCheckpointRequestsCollection(options: CustomCheckpointRequestCollectionSpecifier) {
95
+ // createIndexes also creates the collection on first use. Callers invoke
96
+ // this lazily before the transaction which writes the first checkpoint.
97
+ await this.customCheckpointRequests(options).createIndexes([
98
+ {
99
+ key: { user_id: 1 },
100
+ name: 'user_unique',
101
+ unique: true
102
+ },
103
+ {
104
+ // Find checkpoint changes over an operation-id range.
105
+ key: { op_id: 1 },
106
+ name: 'op_id'
107
+ },
108
+ {
109
+ // Limit retention scans to temporary client-requested checkpoints.
110
+ key: { checkpoint_requested_at: 1 },
111
+ name: 'checkpoint_requested_at',
112
+ partialFilterExpression: { checkpoint_requested_at: { $exists: true } }
113
+ }
114
+ ]);
115
+ }
116
+
117
+ listCustomCheckpointRequestCollections(replicationStreamId: number) {
118
+ return this.upstream.listCustomCheckpointRequestCollections(replicationStreamId);
119
+ }
120
+
77
121
  listBucketDataCollections(replicationStreamId: number) {
78
122
  return this.listCollectionsByPrefix(`bucket_data_${replicationStreamId}_`);
79
123
  }
@@ -117,11 +161,18 @@ export class VersionedPowerSyncMongoV3 extends BaseVersionedPowerSyncMongo {
117
161
  },
118
162
  { name: 'bucket_updates', unique: true }
119
163
  );
164
+ // V3 workers claim only buckets that have reached their scheduled check
165
+ // time, keeping scheduled scans proportional to pending work.
120
166
  await bucketState.createIndex(
167
+ { next_compact_check: 1 },
121
168
  {
122
- 'estimate_since_compact.count': -1
123
- },
124
- { name: 'dirty_count' }
169
+ name: 'next_compact_check',
170
+ partialFilterExpression: { next_compact_check: { $exists: true } }
171
+ }
125
172
  );
126
173
  }
174
+
175
+ customCheckpointRequestCollectionName(options: CustomCheckpointRequestCollectionSpecifier) {
176
+ return this.upstream.customCheckpointRequestCollectionName(options);
177
+ }
127
178
  }
@@ -32,6 +32,7 @@ export function serializeBucketData(
32
32
  op: op.op,
33
33
  source_table: op.source_table,
34
34
  source_key: op.source_key,
35
+ subkey: op.subkey,
35
36
  table: op.table,
36
37
  row_id: op.row_id,
37
38
  checksum: op.checksum,
@@ -78,6 +79,7 @@ export function* loadBucketDataDocument(
78
79
  op: op.op,
79
80
  source_table: op.source_table,
80
81
  source_key: op.source_key,
82
+ subkey: op.subkey,
81
83
  table: op.table,
82
84
  row_id: op.row_id,
83
85
  checksum: op.checksum,