@powersync/service-module-mongodb-storage 0.19.0 → 0.21.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 (176) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +31 -3
  3. package/dist/storage/MongoBucketStorage.js +365 -206
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +15 -4
  6. package/dist/storage/implementation/MongoBucketBatch.js +35 -9
  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 +3 -1
  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/MongoWriteCheckpointAPI.d.ts +9 -7
  27. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +7 -28
  28. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  29. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  30. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  31. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  32. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
  33. package/dist/storage/implementation/common/PersistedBatch.d.ts +27 -0
  34. package/dist/storage/implementation/common/PersistedBatch.js +43 -2
  35. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  36. package/dist/storage/implementation/db.d.ts +4 -1
  37. package/dist/storage/implementation/db.js +14 -1
  38. package/dist/storage/implementation/db.js.map +1 -1
  39. package/dist/storage/implementation/models.d.ts +35 -0
  40. package/dist/storage/implementation/models.js.map +1 -1
  41. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +6 -0
  42. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +80 -84
  43. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  44. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +36 -5
  45. package/dist/storage/implementation/v1/MongoCompactorV1.js +229 -8
  46. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  47. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  48. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  49. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  50. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  51. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
  52. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  53. package/dist/storage/implementation/v1/models.js +1 -0
  54. package/dist/storage/implementation/v1/models.js.map +1 -1
  55. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  56. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  57. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  58. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -1
  59. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +120 -39
  60. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  61. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +77 -25
  62. package/dist/storage/implementation/v3/MongoCompactorV3.js +585 -141
  63. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  64. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  65. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  66. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  67. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  68. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  69. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  70. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  71. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
  72. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  73. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  74. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  75. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  76. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -0
  77. package/dist/storage/implementation/v3/PersistedBatchV3.js +52 -11
  78. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  79. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  80. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  81. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  82. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  83. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  84. package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
  85. package/dist/storage/implementation/v3/compact-utils.js +216 -0
  86. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  87. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  88. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  89. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  90. package/dist/storage/implementation/v3/models.d.ts +89 -5
  91. package/dist/storage/implementation/v3/models.js.map +1 -1
  92. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  93. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -4
  94. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  95. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  96. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -5
  97. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +10 -2
  98. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  99. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
  100. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
  101. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  102. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  103. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +177 -23
  104. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  105. package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
  106. package/dist/storage/implementation/v3/source-table-utils.js +49 -57
  107. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  108. package/dist/storage/storage-index.d.ts +2 -1
  109. package/dist/storage/storage-index.js +1 -0
  110. package/dist/storage/storage-index.js.map +1 -1
  111. package/dist/types/types.d.ts +23 -1
  112. package/dist/types/types.js +16 -7
  113. package/dist/types/types.js.map +1 -1
  114. package/dist/utils/util.js +3 -1
  115. package/dist/utils/util.js.map +1 -1
  116. package/package.json +10 -9
  117. package/src/storage/MongoBucketStorage.ts +495 -268
  118. package/src/storage/implementation/MongoBucketBatch.ts +54 -14
  119. package/src/storage/implementation/MongoCompactor.ts +35 -346
  120. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  121. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
  122. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
  123. package/src/storage/implementation/MongoStorageProvider.ts +3 -1
  124. package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
  125. package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
  126. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +12 -44
  127. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  128. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  129. package/src/storage/implementation/common/PersistedBatch.ts +54 -2
  130. package/src/storage/implementation/db.ts +21 -2
  131. package/src/storage/implementation/models.ts +38 -0
  132. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +107 -96
  133. package/src/storage/implementation/v1/MongoCompactorV1.ts +303 -19
  134. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  135. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
  136. package/src/storage/implementation/v1/models.ts +1 -0
  137. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  138. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +159 -42
  139. package/src/storage/implementation/v3/MongoCompactorV3.ts +744 -190
  140. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  141. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  142. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
  143. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  144. package/src/storage/implementation/v3/PersistedBatchV3.ts +57 -12
  145. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  146. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  147. package/src/storage/implementation/v3/compact-utils.ts +320 -0
  148. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  149. package/src/storage/implementation/v3/models.ts +96 -3
  150. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +16 -8
  151. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  152. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +15 -4
  153. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
  154. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +257 -27
  155. package/src/storage/implementation/v3/source-table-utils.ts +82 -76
  156. package/src/storage/storage-index.ts +2 -0
  157. package/src/types/types.ts +24 -7
  158. package/src/utils/util.ts +3 -1
  159. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  160. package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
  161. package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
  162. package/test/src/compact-utils.test.ts +226 -0
  163. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  164. package/test/src/object_storage_usage.test.ts +255 -0
  165. package/test/src/parameter_compacting_v1.test.ts +120 -0
  166. package/test/src/parameter_compacting_v3.test.ts +192 -0
  167. package/test/src/parameter_compaction_fence.test.ts +276 -0
  168. package/test/src/storage.test.ts +273 -22
  169. package/test/src/storage_compacting.test.ts +714 -244
  170. package/test/src/storage_s3_checksums.test.ts +3 -4
  171. package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
  172. package/test/src/storage_s3_reading.test.ts +243 -1
  173. package/test/src/storage_s3_writing.test.ts +1 -0
  174. package/test/src/storage_sync.test.ts +365 -41
  175. package/test/src/util.ts +1 -1
  176. package/tsconfig.tsbuildinfo +1 -1
@@ -3,6 +3,8 @@ import { mongo } from '@powersync/lib-service-mongodb';
3
3
  import { ServiceAssertionError } from '@powersync/lib-services-framework';
4
4
  import {
5
5
  CheckpointChanges,
6
+ CompactInitialReplicationOptions,
7
+ CompactInitialReplicationResults,
6
8
  GetCheckpointChangesOptions,
7
9
  InternalOpId,
8
10
  internalToExternalOpId,
@@ -18,12 +20,20 @@ import * as bson from 'bson';
18
20
  import { mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '../../../utils/util.js';
19
21
  import { MongoBucketStorage } from '../../MongoBucketStorage.js';
20
22
  import { BucketDataDoc } from '../common/BucketDataDoc.js';
21
- import { MongoSyncBucketStorageCheckpoint } from '../common/MongoSyncBucketStorageCheckpoint.js';
23
+ import {
24
+ MongoGetCheckpointChangesOptions,
25
+ MongoSyncBucketStorageCheckpoint
26
+ } from '../common/MongoSyncBucketStorageCheckpoint.js';
22
27
  import { MongoChecksums } from '../MongoChecksums.js';
23
28
  import { MongoCompactOptions, MongoCompactor } from '../MongoCompactor.js';
24
29
  import { MongoParameterCompactor } from '../MongoParameterCompactor.js';
25
30
  import { MongoPersistedReplicationStream } from '../MongoPersistedReplicationStream.js';
26
- import { MongoSyncBucketStorage, MongoSyncBucketStorageOptions } from '../MongoSyncBucketStorage.js';
31
+ import {
32
+ MongoCheckpointState,
33
+ MongoSyncBucketStorage,
34
+ MongoSyncBucketStorageOptions
35
+ } from '../MongoSyncBucketStorage.js';
36
+ import { MongoCheckpointAPIOptions } from '../MongoWriteCheckpointAPI.js';
27
37
  import { loadBucketDataDocument, maxOpId } from './bucket-format.js';
28
38
  import {
29
39
  BucketDataDocumentV3,
@@ -36,10 +46,13 @@ import {
36
46
  import { MongoBucketBatchV3 } from './MongoBucketBatchV3.js';
37
47
  import { MongoChecksumsV3 } from './MongoChecksumsV3.js';
38
48
  import { MongoCompactorV3 } from './MongoCompactorV3.js';
49
+ import { MongoParameterCompactorV3 } from './MongoParameterCompactorV3.js';
39
50
  import { MongoStoppedSyncConfigCleanup } from './MongoStoppedSyncConfigCleanup.js';
51
+ import { MongoWriteCheckpointAPIV3 } from './MongoWriteCheckpointAPIV3.js';
40
52
  import { hydrateBucketDataDocuments } from './object-storage/BucketDataObjectStorage.js';
41
53
  import { ObjectStorage } from './object-storage/ObjectStorage.js';
42
54
  import { ObjectStorageLifecycle } from './object-storage/ObjectStorageLifecycle.js';
55
+ import { ObjectStorageUsage } from './object-storage/ObjectStorageUsage.js';
43
56
  import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
44
57
 
45
58
  export interface MongoSyncBucketStorageContextV3 {
@@ -188,21 +201,39 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
188
201
  });
189
202
  }
190
203
 
204
+ protected override createWriteCheckpointAPI(options: MongoCheckpointAPIOptions): MongoWriteCheckpointAPIV3 {
205
+ return new MongoWriteCheckpointAPIV3({
206
+ ...options,
207
+ db: this.db,
208
+ syncConfigMapping: () => this.singleSyncConfigMapping()
209
+ });
210
+ }
211
+
191
212
  createMongoCompactor(options: MongoCompactOptions): MongoCompactor {
192
213
  return new MongoCompactorV3(this, this.db, options);
193
214
  }
194
215
 
216
+ override async compactInitialReplication(
217
+ options: CompactInitialReplicationOptions
218
+ ): Promise<CompactInitialReplicationResults> {
219
+ this.logger.info(`Compacting chunks after initial replication...`);
220
+ const start = Date.now();
221
+ const maxOpId = options.maxOpId ?? (await this.fetchPersistedOpHead()) ?? undefined;
222
+ const compactedBuckets = await this.createMongoCompactor({
223
+ ...options,
224
+ maxOpId,
225
+ compactChunksOnly: true,
226
+ logger: this.logger
227
+ }).compact();
228
+ this.logger.info(`Compacted chunks after initial replication in ${(Date.now() - start) / 1000}s`);
229
+ return { buckets: compactedBuckets };
230
+ }
231
+
195
232
  protected createMongoParameterCompactor(
196
233
  checkpoint: InternalOpId,
197
234
  options: storage.CompactOptions
198
235
  ): MongoParameterCompactor {
199
- return new MongoParameterCompactor(this.db, this.replicationStreamId, checkpoint, options, () =>
200
- this.db
201
- .listParameterIndexCollections(this.replicationStreamId)
202
- .then((collections) =>
203
- collections.map((c) => c.collection as unknown as lib_mongo.mongo.Collection<lib_mongo.mongo.Document>)
204
- )
205
- );
236
+ return new MongoParameterCompactorV3(this.db, this.replicationStreamId, checkpoint, options);
206
237
  }
207
238
 
208
239
  protected async fetchPersistedOpHead(): Promise<InternalOpId | null> {
@@ -214,13 +245,14 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
214
245
  }
215
246
 
216
247
  protected async createWriterImpl(options: storage.CreateWriterOptions): Promise<storage.BucketStorageBatch> {
248
+ const batchOptions = this.writerBatchOptions(options);
217
249
  const doc = await this.syncRulesCollection.findOne(
218
250
  { _id: this.replicationStreamId },
219
251
  { projection: { resume_lsn: 1 } }
220
252
  );
221
253
 
222
254
  return new MongoBucketBatchV3({
223
- ...this.writerBatchOptions(options),
255
+ ...batchOptions,
224
256
  // The stream-level replication position - per-config checkpoint LSNs are consistency
225
257
  // markers and do not affect where replication resumes.
226
258
  resumeFromLsn: doc?.resume_lsn ?? null,
@@ -228,16 +260,15 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
228
260
  });
229
261
  }
230
262
 
231
- protected async fetchCheckpointState(
232
- session: mongo.ClientSession
233
- ): Promise<{ checkpoint: bigint; lsn: string | null } | null> {
263
+ protected async fetchCheckpointState(session: mongo.ClientSession): Promise<MongoCheckpointState | null> {
234
264
  const doc = await this.syncRulesCollection.findOne(
235
265
  this.syncConfigMatch({
236
266
  state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
237
267
  }),
238
268
  {
239
269
  session,
240
- projection: this.syncConfigProjection()
270
+ // The invalidation fence must be read in the same snapshot as the checkpoint.
271
+ projection: this.syncConfigProjection({ 'parameter_compaction.checkpoint_changes_invalid_before': 1 })
241
272
  }
242
273
  );
243
274
  // Checkpoints are served from the single active config. A PROCESSING config in the same
@@ -258,7 +289,10 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
258
289
  }
259
290
  return {
260
291
  checkpoint: syncConfig.last_checkpoint ?? 0n,
261
- lsn: syncConfig.last_checkpoint_lsn ?? null
292
+ lsn: syncConfig.last_checkpoint_lsn ?? null,
293
+ // Stream-level state: shared by all sync configs. Defaults to 0n for streams that have
294
+ // never been compacted.
295
+ parameterChangesInvalidBefore: doc?.parameter_compaction?.checkpoint_changes_invalid_before ?? 0n
262
296
  };
263
297
  }
264
298
 
@@ -311,7 +345,8 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
311
345
  },
312
346
  $unset: {
313
347
  resume_lsn: 1,
314
- last_persisted_op: 1
348
+ last_persisted_op: 1,
349
+ parameter_compaction: 1
315
350
  }
316
351
  },
317
352
  {
@@ -376,6 +411,8 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
376
411
  for (const collection of await this.db.listBucketDataCollections(this.replicationStreamId)) {
377
412
  await collection.drop();
378
413
  }
414
+ const usage = new ObjectStorageUsage(this.db, this.replicationStreamId);
415
+ await this.db.client.withSession((session) => session.withTransaction(() => usage.removeStream(session)));
379
416
  if (this.objectStorage) {
380
417
  const lifecycle = new ObjectStorageLifecycle(this.db, this.replicationStreamId, this.objectStorage);
381
418
  await lifecycle.deletePrefix(lifecycle.streamPrefix(), { signal });
@@ -384,7 +421,7 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
384
421
  .pendingObjectStorageDeletes(this.replicationStreamId)
385
422
  .drop({ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS })
386
423
  .catch((error) => {
387
- if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
424
+ if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
388
425
  return;
389
426
  }
390
427
  throw error;
@@ -408,7 +445,7 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
408
445
  .bucketState(this.replicationStreamId)
409
446
  .drop({ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS })
410
447
  .catch((error) => {
411
- if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
448
+ if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
412
449
  return;
413
450
  }
414
451
  throw error;
@@ -420,11 +457,23 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
420
457
  .sourceTables(this.replicationStreamId)
421
458
  .drop({ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS })
422
459
  .catch((error) => {
460
+ if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
461
+ return;
462
+ }
463
+ throw error;
464
+ });
465
+ }
466
+
467
+ protected override async clearCustomCheckpointRequests(signal?: AbortSignal): Promise<void> {
468
+ for (const collection of await this.db.listCustomCheckpointRequestCollections(this.replicationStreamId)) {
469
+ signal?.throwIfAborted();
470
+ await collection.drop({ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }).catch((error) => {
423
471
  if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
424
472
  return;
425
473
  }
426
474
  throw error;
427
475
  });
476
+ }
428
477
  }
429
478
 
430
479
  async cleanupStoppedSyncConfigs(
@@ -449,7 +498,7 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
449
498
  }
450
499
 
451
500
  protected getParameterBucketChangesImpl(
452
- options: GetCheckpointChangesOptions
501
+ options: MongoGetCheckpointChangesOptions
453
502
  ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
454
503
  return getParameterBucketChangesV3(this.versionContext, options);
455
504
  }
@@ -647,7 +696,7 @@ export async function* getBucketDataBatchV3(
647
696
  // Hydrate any operations from object storage.
648
697
  // In the future we can do this in a more pipelined fashion, but for now we hydrate
649
698
  // the entire batch at once.
650
- await hydrateBucketDataDocuments(docs, ctx.objectStorage, { signal: options?.signal });
699
+ await hydrateBucketDataDocuments(docs, ctx.objectStorage, { signal: options?.signal, tracer: options?.tracer });
651
700
 
652
701
  let currentChunkSizeBytes = 0;
653
702
  let currentChunk: utils.SyncBucketData | null = null;
@@ -784,9 +833,17 @@ export async function getDataBucketChangesV3(
784
833
  };
785
834
  }
786
835
 
836
+ /**
837
+ * Query the parameter entries changed between the two checkpoints, to determine which parameter
838
+ * lookups need to be re-evaluated.
839
+ *
840
+ * This runs at the next checkpoint's snapshot, so it still sees entries that parameter compaction
841
+ * deleted after that snapshot. Compaction that deleted entries before the snapshot is covered by
842
+ * the invalidation fence, checked before we get here.
843
+ */
787
844
  export async function getParameterBucketChangesV3(
788
845
  ctx: MongoSyncBucketStorageContextV3,
789
- options: GetCheckpointChangesOptions
846
+ options: MongoGetCheckpointChangesOptions
790
847
  ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
791
848
  const limit = 1000;
792
849
  const indexIds = ctx.mapping.allParameterIndexIds();
@@ -816,28 +873,41 @@ export async function getParameterBucketChangesV3(
816
873
  }
817
874
  ];
818
875
  const [firstCollection, ...remainingCollections] = collections;
819
- const parameterUpdates = await firstCollection.collection
820
- .aggregate<{ lookup: bson.Binary; indexId: string }>(
821
- [
822
- ...pipelineForCollection(firstCollection.indexId),
823
- ...remainingCollections.map((collection) => {
824
- return {
825
- $unionWith: {
826
- coll: collection.collection.collectionName,
827
- pipeline: pipelineForCollection(collection.indexId)
828
- }
829
- };
830
- }),
876
+ const parameterUpdates = await ctx.db.client.withSession({ snapshot: true }, async (session) => {
877
+ setSessionSnapshotTime(session, options.nextCheckpoint.snapshotTime);
878
+ return await firstCollection.collection
879
+ .aggregate<{ lookup: bson.Binary; indexId: string }>(
880
+ [
881
+ ...pipelineForCollection(firstCollection.indexId),
882
+ ...remainingCollections.map((collection) => {
883
+ return {
884
+ $unionWith: {
885
+ coll: collection.collection.collectionName,
886
+ pipeline: pipelineForCollection(collection.indexId)
887
+ }
888
+ };
889
+ }),
890
+ {
891
+ $limit: limit + 1
892
+ }
893
+ ],
831
894
  {
832
- $limit: limit + 1
895
+ session,
896
+ readConcern: 'snapshot',
897
+ batchSize: limit + 2,
898
+ maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
833
899
  }
834
- ],
835
- {
836
- batchSize: limit + 2,
837
- maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
838
- }
839
- )
840
- .toArray();
900
+ )
901
+ .toArray()
902
+ .catch((e) => {
903
+ // Includes the case where the checkpoint snapshot has expired. Degrading to
904
+ // invalidateParameterBuckets would be safe in itself - it reads nothing - but it gains
905
+ // nothing: the caller responds to that by re-evaluating the parameter queries at this
906
+ // same snapshot, which fails too. The checkpoint has to be refetched instead, which is
907
+ // what the existing sync retry behavior does.
908
+ throw lib_mongo.mapQueryError(e, 'while querying parameter changes');
909
+ });
910
+ });
841
911
 
842
912
  const invalidateParameterUpdates = parameterUpdates.length > limit;
843
913
 
@@ -0,0 +1,111 @@
1
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
2
+ import {
3
+ CustomWriteCheckpointFilters,
4
+ GetCheckpointChangesOptions,
5
+ SingleSyncConfigBucketDefinitionMapping,
6
+ storage
7
+ } from '@powersync/service-core';
8
+ import { EventDefinitionId } from '@powersync/service-sync-rules';
9
+ import { MongoCheckpointAPIOptions, MongoWriteCheckpointAPI } from '../MongoWriteCheckpointAPI.js';
10
+ import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
11
+
12
+ export type MongoCheckpointAPIV3Options = Omit<MongoCheckpointAPIOptions, 'db'> & {
13
+ db: VersionedPowerSyncMongoV3;
14
+ syncConfigMapping: () => SingleSyncConfigBucketDefinitionMapping;
15
+ };
16
+
17
+ export class MongoWriteCheckpointAPIV3 extends MongoWriteCheckpointAPI {
18
+ declare db: VersionedPowerSyncMongoV3;
19
+
20
+ // Supplied via the constructor or the setWriteCheckpointMode setter.
21
+ private _customWriteCheckpointEventName: string | undefined;
22
+ private readonly syncConfigMapping: () => SingleSyncConfigBucketDefinitionMapping;
23
+
24
+ constructor(options: MongoCheckpointAPIV3Options) {
25
+ super(options);
26
+ this.syncConfigMapping = options.syncConfigMapping;
27
+ // Enforce the CUSTOM-requires-event-name invariant on construction too, not only setWriteCheckpointMode.
28
+ this._customWriteCheckpointEventName = this.requireCustomEventName(options.writeCheckpointMode);
29
+ }
30
+
31
+ protected resolveEventId(): EventDefinitionId {
32
+ const eventName = this._customWriteCheckpointEventName;
33
+ if (!eventName) {
34
+ throw new ServiceAssertionError(`No eventName has been supplied via setWriteCheckpointMode.`);
35
+ }
36
+ return this.syncConfigMapping().eventDefinitionIdByName(eventName);
37
+ }
38
+
39
+ protected override async lastCustomWriteCheckpoint(filters: CustomWriteCheckpointFilters): Promise<bigint | null> {
40
+ const { user_id } = filters;
41
+ const eventId = this.resolveEventId();
42
+
43
+ const lastWriteCheckpoint = await this.db
44
+ .customCheckpointRequests({
45
+ eventId,
46
+ replicationStreamId: this.replicationStreamId
47
+ })
48
+ .findOne({
49
+ user_id
50
+ });
51
+ return lastWriteCheckpoint?.checkpoint ?? null;
52
+ }
53
+
54
+ override setWriteCheckpointMode(config: storage.WriteCheckpointModeConfig): void {
55
+ this._customWriteCheckpointEventName = this.requireCustomEventName(config);
56
+ super.setWriteCheckpointMode(config);
57
+ }
58
+
59
+ /**
60
+ * Validates and returns the event name for the given mode config. Shared by the constructor and
61
+ * {@link setWriteCheckpointMode} so both paths enforce that CUSTOM mode always names its checkpoint event.
62
+ */
63
+ private requireCustomEventName(config: storage.WriteCheckpointModeConfig): string | undefined {
64
+ if (config.mode != storage.WriteCheckpointMode.CUSTOM) {
65
+ return undefined;
66
+ }
67
+ if (!config.eventName) {
68
+ throw new ServiceAssertionError(`V3 incremental reprocessing requires an eventName to be supplied.`);
69
+ }
70
+ return config.eventName;
71
+ }
72
+
73
+ protected override async getCustomWriteCheckpointChanges(
74
+ options: GetCheckpointChangesOptions
75
+ ): Promise<{ invalidateWriteCheckpoints: boolean; updatedWriteCheckpoints: Map<string, bigint> }> {
76
+ const eventId = this.resolveEventId();
77
+
78
+ const limit = 1000;
79
+ const changes = await this.db
80
+ .customCheckpointRequests({
81
+ replicationStreamId: this.replicationStreamId,
82
+ eventId
83
+ })
84
+ .find(
85
+ {
86
+ op_id: { $gt: options.lastCheckpoint.checkpoint, $lte: options.nextCheckpoint.checkpoint }
87
+ },
88
+ {
89
+ limit: limit + 1,
90
+ // batchSize is 1 more than limit to auto-close the cursor.
91
+ // See https://github.com/mongodb/node-mongodb-native/pull/4580
92
+ batchSize: limit + 2,
93
+ singleBatch: true
94
+ }
95
+ )
96
+ .toArray();
97
+ const invalidate = changes.length > limit;
98
+
99
+ const updatedWriteCheckpoints = new Map<string, bigint>();
100
+ if (!invalidate) {
101
+ for (let c of changes) {
102
+ updatedWriteCheckpoints.set(c.user_id, c.checkpoint);
103
+ }
104
+ }
105
+
106
+ return {
107
+ invalidateWriteCheckpoints: invalidate,
108
+ updatedWriteCheckpoints
109
+ };
110
+ }
111
+ }
@@ -15,6 +15,7 @@ import {
15
15
  import { SourceRecordLookupState } from '../common/SourceRecordStore.js';
16
16
  import { serializeBucketData } from './bucket-format.js';
17
17
  import { chunkBucketData } from './chunking.js';
18
+ import { DEFAULT_MIN_COMPACT_CHUNK_INTERVAL_MS } from './compaction-constants.js';
18
19
  import {
19
20
  BucketDataDocumentV3,
20
21
  BucketStateDocumentV3,
@@ -24,12 +25,14 @@ import {
24
25
  taggedBucketParameterDocumentToTagged
25
26
  } from './models.js';
26
27
  import { ObjectStorageLifecycle } from './object-storage/ObjectStorageLifecycle.js';
28
+ import { ObjectStorageUsage } from './object-storage/ObjectStorageUsage.js';
27
29
  import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
28
30
 
29
31
  export class PersistedBatchV3 extends PersistedBatch {
30
32
  currentData: { sourceTableId: bson.ObjectId; operation: mongo.AnyBulkWriteOperation<CurrentDataDocumentV3> }[] = [];
31
33
  sourceTablePendingDeletes = new Map<string, InternalOpId>();
32
34
  protected readonly objectStorageLifecycle?: ObjectStorageLifecycle;
35
+ protected readonly objectStorageUsage?: ObjectStorageUsage;
33
36
 
34
37
  declare protected readonly db: VersionedPowerSyncMongoV3;
35
38
 
@@ -43,6 +46,7 @@ export class PersistedBatchV3 extends PersistedBatch {
43
46
  super(db, group_id, mapping, writtenSize, options);
44
47
  if (this.objectStorage) {
45
48
  this.objectStorageLifecycle = new ObjectStorageLifecycle(this.db, this.group_id, this.objectStorage);
49
+ this.objectStorageUsage = new ObjectStorageUsage(this.db, this.group_id, this.objectStorageUsageWriterId);
46
50
  }
47
51
  }
48
52
 
@@ -225,15 +229,17 @@ export class PersistedBatchV3 extends PersistedBatch {
225
229
  operationsByDefinition.set(document.bucketKey.definitionId, existing);
226
230
  }
227
231
 
228
- let uploadCount = 0;
229
232
  const plans = Array.from(operationsByDefinition, ([definitionId, documents]) => {
230
233
  const operationsByBucket = Map.groupBy(documents, (document) => document.bucketKey.bucket);
231
234
  const lifecycle = this.objectStorageLifecycle;
232
- const createInserts: (() => Promise<mongo.AnyBulkWriteOperation<BucketDataDocumentV3>>)[] = [];
235
+ type BucketDataInsert = { insertOne: { document: BucketDataDocumentV3 } };
236
+ const createInserts: (() => Promise<BucketDataInsert>)[] = [];
233
237
 
234
238
  for (const [bucket, ops] of operationsByBucket) {
239
+ this.resetBucketPersistedBytes(definitionId, bucket);
235
240
  for (const chunk of chunkBucketData(ops)) {
236
241
  const serialized = serializeBucketData(bucket, chunk);
242
+ this.incrementBucketPersistedChunk(definitionId, bucket, serialized.size);
237
243
  if (lifecycle == null || serialized.size <= this.inlineThresholdBytes) {
238
244
  createInserts.push(async () => ({
239
245
  insertOne: {
@@ -243,13 +249,12 @@ export class PersistedBatchV3 extends PersistedBatch {
243
249
  continue;
244
250
  }
245
251
 
246
- uploadCount += 1;
247
252
  createInserts.push(async () => {
248
253
  const minOp = chunk[0].o;
249
254
  const maxOp = chunk[chunk.length - 1].o;
250
255
  const { ops: bucketOps, ...metadata } = serialized;
251
256
  const path = lifecycle.allocatePath(definitionId, bucket, minOp, maxOp);
252
- const { fileSize } = await lifecycle.bucketData.store(path, bucketOps!);
257
+ const { fileSize } = await lifecycle.bucketData.store(path, bucketOps!, { signal: this.signal });
253
258
  return {
254
259
  insertOne: {
255
260
  document: {
@@ -281,7 +286,17 @@ export class PersistedBatchV3 extends PersistedBatch {
281
286
  // separate limiter here.
282
287
  const writes = await createAllInserts();
283
288
 
289
+ const usageDeltas = this.objectStorageUsage == null ? undefined : new Map<BucketDefinitionId, bigint>();
284
290
  for (const { definitionId, inserts } of writes) {
291
+ if (usageDeltas != null) {
292
+ const delta = inserts.reduce(
293
+ (sum, operation) => sum + ObjectStorageUsage.bytes(operation.insertOne.document),
294
+ 0n
295
+ );
296
+ if (delta !== 0n) {
297
+ usageDeltas.set(definitionId, delta);
298
+ }
299
+ }
285
300
  if (inserts.length > 0) {
286
301
  await this.db.bucketData(this.group_id, definitionId).bulkWrite(inserts, {
287
302
  session,
@@ -289,6 +304,9 @@ export class PersistedBatchV3 extends PersistedBatch {
289
304
  });
290
305
  }
291
306
  }
307
+ if (usageDeltas != null) {
308
+ await this.objectStorageUsage!.applyDeltas(usageDeltas, session);
309
+ }
292
310
  }
293
311
 
294
312
  protected async flushBucketParameters(session: mongo.ClientSession) {
@@ -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,