@powersync/service-module-mongodb-storage 0.0.0-dev-20250910154512 → 0.0.0-dev-20251030082344

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 (73) hide show
  1. package/CHANGELOG.md +35 -11
  2. package/dist/index.d.ts +0 -1
  3. package/dist/index.js +0 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/migrations/db/migrations/1760433882550-bucket-state-index2.js +25 -0
  6. package/dist/migrations/db/migrations/1760433882550-bucket-state-index2.js.map +1 -0
  7. package/dist/storage/MongoBucketStorage.js +1 -1
  8. package/dist/storage/MongoBucketStorage.js.map +1 -1
  9. package/dist/storage/implementation/MongoBucketBatch.js +1 -1
  10. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  11. package/dist/storage/implementation/MongoCompactor.d.ts +13 -3
  12. package/dist/storage/implementation/MongoCompactor.js +86 -90
  13. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoStorageProvider.d.ts +1 -1
  15. package/dist/storage/implementation/MongoStorageProvider.js +3 -7
  16. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  17. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +2 -2
  18. package/dist/storage/implementation/MongoSyncBucketStorage.js +62 -19
  19. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  20. package/dist/storage/implementation/MongoTestStorageFactoryGenerator.d.ts +9 -0
  21. package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js +20 -0
  22. package/dist/storage/implementation/MongoTestStorageFactoryGenerator.js.map +1 -0
  23. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +6 -2
  24. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  25. package/dist/storage/implementation/PersistedBatch.js +1 -1
  26. package/dist/storage/implementation/PersistedBatch.js.map +1 -1
  27. package/dist/storage/implementation/db.d.ts +3 -4
  28. package/dist/storage/implementation/db.js +9 -14
  29. package/dist/storage/implementation/db.js.map +1 -1
  30. package/dist/storage/implementation/models.d.ts +0 -3
  31. package/dist/{utils → storage/implementation}/util.d.ts +7 -2
  32. package/dist/{utils → storage/implementation}/util.js +16 -1
  33. package/dist/storage/implementation/util.js.map +1 -0
  34. package/dist/storage/storage-index.d.ts +2 -3
  35. package/dist/storage/storage-index.js +2 -3
  36. package/dist/storage/storage-index.js.map +1 -1
  37. package/package.json +9 -9
  38. package/src/index.ts +0 -1
  39. package/src/migrations/db/migrations/{1752661449910-connection-reporting.ts → 1760433882550-bucket-state-index2.ts} +6 -30
  40. package/src/storage/MongoBucketStorage.ts +1 -1
  41. package/src/storage/implementation/MongoBucketBatch.ts +1 -1
  42. package/src/storage/implementation/MongoCompactor.ts +100 -96
  43. package/src/storage/implementation/MongoStorageProvider.ts +4 -9
  44. package/src/storage/implementation/MongoSyncBucketStorage.ts +64 -21
  45. package/src/storage/implementation/MongoTestStorageFactoryGenerator.ts +32 -0
  46. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +6 -2
  47. package/src/storage/implementation/PersistedBatch.ts +1 -1
  48. package/src/storage/implementation/db.ts +12 -16
  49. package/src/storage/implementation/models.ts +0 -3
  50. package/src/{utils → storage/implementation}/util.ts +19 -3
  51. package/src/storage/storage-index.ts +2 -3
  52. package/test/src/storage.test.ts +51 -3
  53. package/test/src/storage_compacting.test.ts +17 -2
  54. package/test/src/util.ts +2 -6
  55. package/tsconfig.tsbuildinfo +1 -1
  56. package/dist/migrations/db/migrations/1752661449910-connection-reporting.js +0 -36
  57. package/dist/migrations/db/migrations/1752661449910-connection-reporting.js.map +0 -1
  58. package/dist/storage/MongoReportStorage.d.ts +0 -17
  59. package/dist/storage/MongoReportStorage.js +0 -152
  60. package/dist/storage/MongoReportStorage.js.map +0 -1
  61. package/dist/utils/test-utils.d.ts +0 -13
  62. package/dist/utils/test-utils.js +0 -40
  63. package/dist/utils/test-utils.js.map +0 -1
  64. package/dist/utils/util.js.map +0 -1
  65. package/dist/utils/utils-index.d.ts +0 -2
  66. package/dist/utils/utils-index.js +0 -3
  67. package/dist/utils/utils-index.js.map +0 -1
  68. package/src/storage/MongoReportStorage.ts +0 -174
  69. package/src/utils/test-utils.ts +0 -57
  70. package/src/utils/utils-index.ts +0 -2
  71. package/test/src/__snapshots__/connection-report-storage.test.ts.snap +0 -215
  72. package/test/src/connection-report-storage.test.ts +0 -133
  73. /package/dist/migrations/db/migrations/{1752661449910-connection-reporting.d.ts → 1760433882550-bucket-state-index2.d.ts} +0 -0
@@ -1,7 +1,9 @@
1
1
  import * as bson from 'bson';
2
2
  import * as crypto from 'crypto';
3
3
  import * as uuid from 'uuid';
4
+ import { mongo } from '@powersync/lib-service-mongodb';
4
5
  import { storage, utils } from '@powersync/service-core';
6
+ import { PowerSyncMongo } from './db.js';
5
7
  import { ServiceAssertionError } from '@powersync/lib-services-framework';
6
8
  export function idPrefixFilter(prefix, rest) {
7
9
  let filter = {
@@ -31,7 +33,7 @@ export function generateSlotName(prefix, sync_rules_id) {
31
33
  * However, that makes `has_more` detection very difficult, since the cursor is always closed
32
34
  * after the first batch. Instead, we do a workaround to only fetch a single batch below.
33
35
  *
34
- * For this to be effective, set batchSize = limit in the find command.
36
+ * For this to be effective, set batchSize = limit + 1 in the find command.
35
37
  */
36
38
  export async function readSingleBatch(cursor) {
37
39
  try {
@@ -94,6 +96,19 @@ export function replicaIdToSubkey(table, id) {
94
96
  return uuid.v5(repr, utils.ID_NAMESPACE);
95
97
  }
96
98
  }
99
+ /**
100
+ * Helper for unit tests
101
+ */
102
+ export const connectMongoForTests = (url, isCI) => {
103
+ // Short timeout for tests, to fail fast when the server is not available.
104
+ // Slightly longer timeouts for CI, to avoid arbitrary test failures
105
+ const client = new mongo.MongoClient(url, {
106
+ connectTimeoutMS: isCI ? 15_000 : 5_000,
107
+ socketTimeoutMS: isCI ? 15_000 : 5_000,
108
+ serverSelectionTimeoutMS: isCI ? 15_000 : 2_500
109
+ });
110
+ return new PowerSyncMongo(client);
111
+ };
97
112
  export function setSessionSnapshotTime(session, time) {
98
113
  // This is a workaround for the lack of direct support for snapshot reads in the MongoDB driver.
99
114
  if (!session.snapshotEnabled) {
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../../../src/storage/implementation/util.ts"],"names":[],"mappings":"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"}
@@ -7,9 +7,8 @@ export * from './implementation/MongoPersistedSyncRulesContent.js';
7
7
  export * from './implementation/MongoStorageProvider.js';
8
8
  export * from './implementation/MongoSyncBucketStorage.js';
9
9
  export * from './implementation/MongoSyncRulesLock.js';
10
+ export * from './implementation/MongoTestStorageFactoryGenerator.js';
10
11
  export * from './implementation/OperationBatch.js';
11
12
  export * from './implementation/PersistedBatch.js';
12
- export * from '../utils/util.js';
13
+ export * from './implementation/util.js';
13
14
  export * from './MongoBucketStorage.js';
14
- export * from './MongoReportStorage.js';
15
- export * as test_utils from '../utils/test-utils.js';
@@ -7,10 +7,9 @@ export * from './implementation/MongoPersistedSyncRulesContent.js';
7
7
  export * from './implementation/MongoStorageProvider.js';
8
8
  export * from './implementation/MongoSyncBucketStorage.js';
9
9
  export * from './implementation/MongoSyncRulesLock.js';
10
+ export * from './implementation/MongoTestStorageFactoryGenerator.js';
10
11
  export * from './implementation/OperationBatch.js';
11
12
  export * from './implementation/PersistedBatch.js';
12
- export * from '../utils/util.js';
13
+ export * from './implementation/util.js';
13
14
  export * from './MongoBucketStorage.js';
14
- export * from './MongoReportStorage.js';
15
- export * as test_utils from '../utils/test-utils.js';
16
15
  //# sourceMappingURL=storage-index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"storage-index.js","sourceRoot":"","sources":["../../src/storage/storage-index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sCAAsC,CAAC;AACrD,cAAc,qCAAqC,CAAC;AACpD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,oDAAoD,CAAC;AACnE,cAAc,0CAA0C,CAAC;AACzD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AACvD,cAAc,oCAAoC,CAAC;AACnD,cAAc,oCAAoC,CAAC;AACnD,cAAc,kBAAkB,CAAC;AACjC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"storage-index.js","sourceRoot":"","sources":["../../src/storage/storage-index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sCAAsC,CAAC;AACrD,cAAc,qCAAqC,CAAC;AACpD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,oDAAoD,CAAC;AACnE,cAAc,0CAA0C,CAAC;AACzD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AACvD,cAAc,sDAAsD,CAAC;AACrE,cAAc,oCAAoC,CAAC;AACnD,cAAc,oCAAoC,CAAC;AACnD,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@powersync/service-module-mongodb-storage",
3
3
  "repository": "https://github.com/powersync-ja/powersync-service",
4
4
  "types": "dist/index.d.ts",
5
- "version": "0.0.0-dev-20250910154512",
5
+ "version": "0.0.0-dev-20251030082344",
6
6
  "main": "dist/index.js",
7
7
  "license": "FSL-1.1-ALv2",
8
8
  "type": "module",
@@ -22,20 +22,20 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "bson": "^6.10.3",
25
+ "bson": "^6.10.4",
26
26
  "ix": "^5.0.0",
27
27
  "lru-cache": "^10.2.2",
28
28
  "ts-codec": "^1.3.0",
29
29
  "uuid": "^11.1.0",
30
- "@powersync/lib-service-mongodb": "0.0.0-dev-20250910154512",
31
- "@powersync/lib-services-framework": "0.0.0-dev-20250910154512",
32
- "@powersync/service-core": "0.0.0-dev-20250910154512",
33
- "@powersync/service-types": "0.0.0-dev-20250910154512",
34
- "@powersync/service-jsonbig": "0.17.11",
35
- "@powersync/service-sync-rules": "0.29.4"
30
+ "@powersync/lib-service-mongodb": "0.0.0-dev-20251030082344",
31
+ "@powersync/lib-services-framework": "0.0.0-dev-20251030082344",
32
+ "@powersync/service-core": "0.0.0-dev-20251030082344",
33
+ "@powersync/service-jsonbig": "0.0.0-dev-20251030082344",
34
+ "@powersync/service-sync-rules": "0.0.0-dev-20251030082344",
35
+ "@powersync/service-types": "0.0.0-dev-20251030082344"
36
36
  },
37
37
  "devDependencies": {
38
- "@powersync/service-core-tests": "0.0.0-dev-20250910154512"
38
+ "@powersync/service-core-tests": "0.0.0-dev-20251030082344"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -b",
package/src/index.ts CHANGED
@@ -5,4 +5,3 @@ export * as storage from './storage/storage-index.js';
5
5
 
6
6
  export * from './types/types.js';
7
7
  export * as types from './types/types.js';
8
- export * as utils from './utils/utils-index.js';
@@ -2,6 +2,8 @@ import { migrations } from '@powersync/service-core';
2
2
  import * as storage from '../../../storage/storage-index.js';
3
3
  import { MongoStorageConfig } from '../../../types/types.js';
4
4
 
5
+ const INDEX_NAME = 'dirty_buckets';
6
+
5
7
  export const up: migrations.PowerSyncMigrationFunction = async (context) => {
6
8
  const {
7
9
  service_context: { configuration }
@@ -9,35 +11,7 @@ export const up: migrations.PowerSyncMigrationFunction = async (context) => {
9
11
  const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig);
10
12
 
11
13
  try {
12
- await db.createConnectionReportingCollection();
13
-
14
- await db.connection_report_events.createIndex(
15
- {
16
- connected_at: 1,
17
- jwt_exp: 1,
18
- disconnected_at: 1
19
- },
20
- { name: 'connection_list_index' }
21
- );
22
-
23
- await db.connection_report_events.createIndex(
24
- {
25
- user_id: 1
26
- },
27
- { name: 'connection_user_id_index' }
28
- );
29
- await db.connection_report_events.createIndex(
30
- {
31
- client_id: 1
32
- },
33
- { name: 'connection_client_id_index' }
34
- );
35
- await db.connection_report_events.createIndex(
36
- {
37
- sdk: 1
38
- },
39
- { name: 'connection_index' }
40
- );
14
+ await db.createBucketStateIndex2();
41
15
  } finally {
42
16
  await db.client.close();
43
17
  }
@@ -51,7 +25,9 @@ export const down: migrations.PowerSyncMigrationFunction = async (context) => {
51
25
  const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig);
52
26
 
53
27
  try {
54
- await db.db.dropCollection('connection_report_events');
28
+ if (await db.bucket_state.indexExists(INDEX_NAME)) {
29
+ await db.bucket_state.dropIndex(INDEX_NAME);
30
+ }
55
31
  } finally {
56
32
  await db.client.close();
57
33
  }
@@ -12,7 +12,7 @@ import { PowerSyncMongo } from './implementation/db.js';
12
12
  import { SyncRuleDocument } from './implementation/models.js';
13
13
  import { MongoPersistedSyncRulesContent } from './implementation/MongoPersistedSyncRulesContent.js';
14
14
  import { MongoSyncBucketStorage, MongoSyncBucketStorageOptions } from './implementation/MongoSyncBucketStorage.js';
15
- import { generateSlotName } from '../utils/util.js';
15
+ import { generateSlotName } from './implementation/util.js';
16
16
 
17
17
  export class MongoBucketStorage
18
18
  extends BaseObserver<storage.BucketStorageFactoryListener>
@@ -28,7 +28,7 @@ import { MongoIdSequence } from './MongoIdSequence.js';
28
28
  import { batchCreateCustomWriteCheckpoints } from './MongoWriteCheckpointAPI.js';
29
29
  import { cacheKey, OperationBatch, RecordOperation } from './OperationBatch.js';
30
30
  import { PersistedBatch } from './PersistedBatch.js';
31
- import { idPrefixFilter } from '../../utils/util.js';
31
+ import { idPrefixFilter } from './util.js';
32
32
 
33
33
  /**
34
34
  * 15MB
@@ -1,6 +1,13 @@
1
1
  import { mongo, MONGO_OPERATION_TIMEOUT_MS } from '@powersync/lib-service-mongodb';
2
2
  import { logger, ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
3
- import { addChecksums, InternalOpId, isPartialChecksum, storage, utils } from '@powersync/service-core';
3
+ import {
4
+ addChecksums,
5
+ InternalOpId,
6
+ isPartialChecksum,
7
+ PopulateChecksumCacheResults,
8
+ storage,
9
+ utils
10
+ } from '@powersync/service-core';
4
11
 
5
12
  import { PowerSyncMongo } from './db.js';
6
13
  import { BucketDataDocument, BucketDataKey, BucketStateDocument } from './models.js';
@@ -10,6 +17,7 @@ import { cacheKey } from './OperationBatch.js';
10
17
  interface CurrentBucketState {
11
18
  /** Bucket name */
12
19
  bucket: string;
20
+
13
21
  /**
14
22
  * Rows seen in the bucket, with the last op_id of each.
15
23
  */
@@ -96,67 +104,56 @@ export class MongoCompactor {
96
104
  // We can make this more efficient later on by iterating
97
105
  // through the buckets in a single query.
98
106
  // That makes batching more tricky, so we leave for later.
99
- await this.compactInternal(bucket);
107
+ await this.compactSingleBucket(bucket);
100
108
  }
101
109
  } else {
102
- await this.compactInternal(undefined);
110
+ await this.compactDirtyBuckets();
103
111
  }
104
112
  }
105
113
 
106
- async compactInternal(bucket: string | undefined) {
107
- const idLimitBytes = this.idLimitBytes;
114
+ private async compactDirtyBuckets() {
115
+ while (!this.signal?.aborted) {
116
+ // Process all buckets with 1 or more changes since last time
117
+ const buckets = await this.dirtyBucketBatch({ minBucketChanges: 1 });
118
+ if (buckets.length == 0) {
119
+ // All done
120
+ break;
121
+ }
122
+ for (let bucket of buckets) {
123
+ await this.compactSingleBucket(bucket);
124
+ }
125
+ }
126
+ }
108
127
 
109
- let currentState: CurrentBucketState | null = null;
128
+ private async compactSingleBucket(bucket: string) {
129
+ const idLimitBytes = this.idLimitBytes;
110
130
 
111
- let bucketLower: string | mongo.MinKey;
112
- let bucketUpper: string | mongo.MaxKey;
131
+ let currentState: CurrentBucketState = {
132
+ bucket,
133
+ seen: new Map(),
134
+ trackingSize: 0,
135
+ lastNotPut: null,
136
+ opsSincePut: 0,
113
137
 
114
- if (bucket == null) {
115
- bucketLower = new mongo.MinKey();
116
- bucketUpper = new mongo.MaxKey();
117
- } else if (bucket.includes('[')) {
118
- // Exact bucket name
119
- bucketLower = bucket;
120
- bucketUpper = bucket;
121
- } else {
122
- // Bucket definition name
123
- bucketLower = `${bucket}[`;
124
- bucketUpper = `${bucket}[\uFFFF`;
125
- }
138
+ checksum: 0,
139
+ opCount: 0,
140
+ opBytes: 0
141
+ };
126
142
 
127
143
  // Constant lower bound
128
144
  const lowerBound: BucketDataKey = {
129
145
  g: this.group_id,
130
- b: bucketLower as string,
146
+ b: bucket,
131
147
  o: new mongo.MinKey() as any
132
148
  };
133
149
 
134
150
  // Upper bound is adjusted for each batch
135
151
  let upperBound: BucketDataKey = {
136
152
  g: this.group_id,
137
- b: bucketUpper as string,
153
+ b: bucket,
138
154
  o: new mongo.MaxKey() as any
139
155
  };
140
156
 
141
- const doneWithBucket = async () => {
142
- if (currentState == null) {
143
- return;
144
- }
145
- // Free memory before clearing bucket
146
- currentState.seen.clear();
147
- if (currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
148
- logger.info(
149
- `Inserting CLEAR at ${this.group_id}:${currentState.bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
150
- );
151
- // Need flush() before clear()
152
- await this.flush();
153
- await this.clearBucket(currentState);
154
- }
155
-
156
- // Do this _after_ clearBucket so that we have accurate counts.
157
- this.updateBucketChecksums(currentState);
158
- };
159
-
160
157
  while (!this.signal?.aborted) {
161
158
  // Query one batch at a time, to avoid cursor timeouts
162
159
  const cursor = this.db.bucket_data.aggregate<BucketDataDocument & { size: number | bigint }>(
@@ -184,7 +181,11 @@ export class MongoCompactor {
184
181
  }
185
182
  }
186
183
  ],
187
- { batchSize: this.moveBatchQueryLimit }
184
+ {
185
+ // batchSize is 1 more than limit to auto-close the cursor.
186
+ // See https://github.com/mongodb/node-mongodb-native/pull/4580
187
+ batchSize: this.moveBatchQueryLimit + 1
188
+ }
188
189
  );
189
190
  // We don't limit to a single batch here, since that often causes MongoDB to scan through more than it returns.
190
191
  // Instead, we load up to the limit.
@@ -199,22 +200,6 @@ export class MongoCompactor {
199
200
  upperBound = batch[batch.length - 1]._id;
200
201
 
201
202
  for (let doc of batch) {
202
- if (currentState == null || doc._id.b != currentState.bucket) {
203
- await doneWithBucket();
204
-
205
- currentState = {
206
- bucket: doc._id.b,
207
- seen: new Map(),
208
- trackingSize: 0,
209
- lastNotPut: null,
210
- opsSincePut: 0,
211
-
212
- checksum: 0,
213
- opCount: 0,
214
- opBytes: 0
215
- };
216
- }
217
-
218
203
  if (doc._id.o > this.maxOpId) {
219
204
  continue;
220
205
  }
@@ -285,12 +270,22 @@ export class MongoCompactor {
285
270
  }
286
271
  }
287
272
 
288
- if (currentState != null) {
289
- logger.info(`Processed batch of length ${batch.length} current bucket: ${currentState.bucket}`);
290
- }
273
+ logger.info(`Processed batch of length ${batch.length} current bucket: ${bucket}`);
274
+ }
275
+
276
+ // Free memory before clearing bucket
277
+ currentState.seen.clear();
278
+ if (currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
279
+ logger.info(
280
+ `Inserting CLEAR at ${this.group_id}:${bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
281
+ );
282
+ // Need flush() before clear()
283
+ await this.flush();
284
+ await this.clearBucket(currentState);
291
285
  }
292
286
 
293
- await doneWithBucket();
287
+ // Do this _after_ clearBucket so that we have accurate counts.
288
+ this.updateBucketChecksums(currentState);
294
289
 
295
290
  // Need another flush after updateBucketChecksums()
296
291
  await this.flush();
@@ -478,50 +473,55 @@ export class MongoCompactor {
478
473
  /**
479
474
  * Subset of compact, only populating checksums where relevant.
480
475
  */
481
- async populateChecksums() {
482
- // This is updated after each batch
483
- let lowerBound: BucketStateDocument['_id'] = {
484
- g: this.group_id,
485
- b: new mongo.MinKey() as any
486
- };
487
- // This is static
488
- const upperBound: BucketStateDocument['_id'] = {
489
- g: this.group_id,
490
- b: new mongo.MaxKey() as any
491
- };
476
+ async populateChecksums(options: { minBucketChanges: number }): Promise<PopulateChecksumCacheResults> {
477
+ let count = 0;
492
478
  while (!this.signal?.aborted) {
493
- // By filtering buckets, we effectively make this "resumeable".
494
- const filter: mongo.Filter<BucketStateDocument> = {
495
- _id: {
496
- $gt: lowerBound,
497
- $lt: upperBound
498
- },
499
- compacted_state: { $exists: false }
500
- };
479
+ const buckets = await this.dirtyBucketBatch(options);
480
+ if (buckets.length == 0) {
481
+ // All done
482
+ break;
483
+ }
484
+ const start = Date.now();
485
+ logger.info(`Calculating checksums for batch of ${buckets.length} buckets, starting at ${buckets[0]}`);
486
+
487
+ await this.updateChecksumsBatch(buckets);
488
+ logger.info(`Updated checksums for batch of ${buckets.length} buckets in ${Date.now() - start}ms`);
489
+ count += buckets.length;
490
+ }
491
+ return { buckets: count };
492
+ }
501
493
 
502
- const bucketsWithoutChecksums = await this.db.bucket_state
503
- .find(filter, {
494
+ /**
495
+ * Returns a batch of dirty buckets - buckets with most changes first.
496
+ *
497
+ * This cannot be used to iterate on its own - the client is expected to process these buckets and
498
+ * set estimate_since_compact.count: 0 when done, before fetching the next batch.
499
+ */
500
+ private async dirtyBucketBatch(options: { minBucketChanges: number }): Promise<string[]> {
501
+ if (options.minBucketChanges <= 0) {
502
+ throw new ReplicationAssertionError('minBucketChanges must be >= 1');
503
+ }
504
+ // We make use of an index on {_id.g: 1, 'estimate_since_compact.count': -1}
505
+ const dirtyBuckets = await this.db.bucket_state
506
+ .find(
507
+ {
508
+ '_id.g': this.group_id,
509
+ 'estimate_since_compact.count': { $gte: options.minBucketChanges }
510
+ },
511
+ {
504
512
  projection: {
505
513
  _id: 1
506
514
  },
507
515
  sort: {
508
- _id: 1
516
+ 'estimate_since_compact.count': -1
509
517
  },
510
518
  limit: 5_000,
511
519
  maxTimeMS: MONGO_OPERATION_TIMEOUT_MS
512
- })
513
- .toArray();
514
- if (bucketsWithoutChecksums.length == 0) {
515
- // All done
516
- break;
517
- }
518
-
519
- logger.info(`Calculating checksums for batch of ${bucketsWithoutChecksums.length} buckets`);
520
-
521
- await this.updateChecksumsBatch(bucketsWithoutChecksums.map((b) => b._id.b));
520
+ }
521
+ )
522
+ .toArray();
522
523
 
523
- lowerBound = bucketsWithoutChecksums[bucketsWithoutChecksums.length - 1]._id;
524
- }
524
+ return dirtyBuckets.map((bucket) => bucket._id.b);
525
525
  }
526
526
 
527
527
  private async updateChecksumsBatch(buckets: string[]) {
@@ -555,6 +555,10 @@ export class MongoCompactor {
555
555
  count: bucketChecksum.count,
556
556
  checksum: BigInt(bucketChecksum.checksum),
557
557
  bytes: null
558
+ },
559
+ estimate_since_compact: {
560
+ count: 0,
561
+ bytes: 0
558
562
  }
559
563
  }
560
564
  },
@@ -4,9 +4,8 @@ import { POWERSYNC_VERSION, storage } from '@powersync/service-core';
4
4
  import { MongoStorageConfig } from '../../types/types.js';
5
5
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
6
6
  import { PowerSyncMongo } from './db.js';
7
- import { MongoReportStorage } from '../MongoReportStorage.js';
8
7
 
9
- export class MongoStorageProvider implements storage.StorageProvider {
8
+ export class MongoStorageProvider implements storage.BucketStorageProvider {
10
9
  get type() {
11
10
  return lib_mongo.MONGO_CONNECTION_TYPE;
12
11
  }
@@ -38,19 +37,15 @@ export class MongoStorageProvider implements storage.StorageProvider {
38
37
  await client.connect();
39
38
 
40
39
  const database = new PowerSyncMongo(client, { database: resolvedConfig.storage.database });
41
- const syncStorageFactory = new MongoBucketStorage(database, {
40
+ const factory = new MongoBucketStorage(database, {
42
41
  // TODO currently need the entire resolved config due to this
43
42
  slot_name_prefix: resolvedConfig.slot_name_prefix
44
43
  });
45
-
46
- // Storage factory for reports
47
- const reportStorageFactory = new MongoReportStorage(database);
48
44
  return {
49
- storage: syncStorageFactory,
50
- reportStorage: reportStorageFactory,
45
+ storage: factory,
51
46
  shutDown: async () => {
52
47
  shuttingDown = true;
53
- await syncStorageFactory[Symbol.asyncDispose]();
48
+ await factory[Symbol.asyncDispose]();
54
49
  await client.close();
55
50
  },
56
51
  tearDown: () => {