@powersync/service-module-mongodb-storage 0.16.0 → 0.18.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 (206) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +35 -15
  3. package/dist/storage/MongoBucketStorage.js +309 -86
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/CheckpointState.d.ts +20 -0
  6. package/dist/storage/implementation/CheckpointState.js +31 -0
  7. package/dist/storage/implementation/CheckpointState.js.map +1 -0
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +69 -39
  9. package/dist/storage/implementation/MongoBucketBatch.js +104 -313
  10. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  11. package/dist/storage/implementation/MongoChecksums.d.ts +11 -9
  12. package/dist/storage/implementation/MongoChecksums.js +3 -111
  13. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  14. package/dist/storage/implementation/MongoCompactor.d.ts +3 -1
  15. package/dist/storage/implementation/MongoCompactor.js +18 -9
  16. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  17. package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
  18. package/dist/storage/implementation/MongoHydrationState.js +49 -0
  19. package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
  20. package/dist/storage/implementation/MongoParameterCompactor.d.ts +6 -5
  21. package/dist/storage/implementation/MongoParameterCompactor.js +24 -1
  22. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  23. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
  24. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -0
  25. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
  26. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
  27. package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
  28. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
  29. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
  30. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +121 -0
  31. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  32. package/dist/storage/implementation/MongoStorageProvider.js +5 -2
  33. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +50 -21
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js +62 -199
  36. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  37. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -1
  38. package/dist/storage/implementation/MongoSyncRulesLock.js +9 -5
  39. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  40. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
  41. package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
  42. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
  43. package/dist/storage/implementation/common/BucketDataDoc.d.ts +1 -1
  44. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +6 -0
  45. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  47. package/dist/storage/implementation/common/PersistedBatch.d.ts +2 -3
  48. package/dist/storage/implementation/common/PersistedBatch.js +5 -0
  49. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  50. package/dist/storage/implementation/common/SourceRecordStore.d.ts +1 -2
  51. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +1 -1
  52. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
  53. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  54. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  55. package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
  56. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  57. package/dist/storage/implementation/db.d.ts +10 -2
  58. package/dist/storage/implementation/db.js +9 -14
  59. package/dist/storage/implementation/db.js.map +1 -1
  60. package/dist/storage/implementation/models.d.ts +25 -50
  61. package/dist/storage/implementation/models.js.map +1 -1
  62. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +23 -1
  63. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +403 -2
  64. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  65. package/dist/storage/implementation/v1/MongoChecksumsV1.js +114 -3
  66. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  67. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +1 -1
  68. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +22 -15
  69. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +100 -27
  70. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  71. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +1 -2
  72. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  73. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  74. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  75. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  76. package/dist/storage/implementation/v1/models.d.ts +21 -1
  77. package/dist/storage/implementation/v1/models.js.map +1 -1
  78. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +21 -0
  79. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +459 -3
  80. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  81. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +27 -7
  82. package/dist/storage/implementation/v3/MongoChecksumsV3.js +202 -18
  83. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +20 -3
  85. package/dist/storage/implementation/v3/MongoCompactorV3.js +511 -22
  86. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  87. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +62 -0
  88. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +350 -0
  89. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  90. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +50 -16
  91. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +292 -75
  92. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  93. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -3
  94. package/dist/storage/implementation/v3/PersistedBatchV3.js +43 -20
  95. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  96. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +52 -16
  97. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -1
  98. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  99. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  100. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  101. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +16 -14
  102. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +32 -27
  103. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  104. package/dist/storage/implementation/v3/bucket-format.d.ts +4 -0
  105. package/dist/storage/implementation/v3/bucket-format.js +58 -0
  106. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  107. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  108. package/dist/storage/implementation/v3/chunking.js +28 -0
  109. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  110. package/dist/storage/implementation/v3/models.d.ts +111 -10
  111. package/dist/storage/implementation/v3/models.js +11 -28
  112. package/dist/storage/implementation/v3/models.js.map +1 -1
  113. package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
  114. package/dist/storage/implementation/v3/source-table-utils.js +214 -0
  115. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  116. package/dist/storage/storage-index.d.ts +4 -4
  117. package/dist/storage/storage-index.js +4 -4
  118. package/dist/storage/storage-index.js.map +1 -1
  119. package/dist/types/types.d.ts +2 -2
  120. package/dist/utils/test-utils.d.ts +1 -2
  121. package/dist/utils/test-utils.js +5 -1
  122. package/dist/utils/test-utils.js.map +1 -1
  123. package/dist/utils/util.d.ts +1 -1
  124. package/dist/utils/util.js +3 -2
  125. package/dist/utils/util.js.map +1 -1
  126. package/package.json +9 -19
  127. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +1 -1
  128. package/src/storage/MongoBucketStorage.ts +464 -95
  129. package/src/storage/implementation/CheckpointState.ts +59 -0
  130. package/src/storage/implementation/MongoBucketBatch.ts +159 -405
  131. package/src/storage/implementation/MongoChecksums.ts +7 -138
  132. package/src/storage/implementation/MongoCompactor.ts +20 -10
  133. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  134. package/src/storage/implementation/MongoParameterCompactor.ts +24 -5
  135. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
  136. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  137. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +152 -0
  138. package/src/storage/implementation/MongoStorageProvider.ts +5 -2
  139. package/src/storage/implementation/MongoSyncBucketStorage.ts +89 -250
  140. package/src/storage/implementation/MongoSyncRulesLock.ts +14 -5
  141. package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
  142. package/src/storage/implementation/common/BucketDataDoc.ts +1 -1
  143. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +7 -0
  144. package/src/storage/implementation/common/PersistedBatch.ts +8 -4
  145. package/src/storage/implementation/common/SourceRecordStore.ts +1 -2
  146. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  147. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  148. package/src/storage/implementation/db.ts +16 -21
  149. package/src/storage/implementation/models.ts +28 -61
  150. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +503 -5
  151. package/src/storage/implementation/v1/MongoChecksumsV1.ts +137 -4
  152. package/src/storage/implementation/v1/MongoCompactorV1.ts +1 -1
  153. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +150 -49
  154. package/src/storage/implementation/v1/PersistedBatchV1.ts +1 -2
  155. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  156. package/src/storage/implementation/v1/models.ts +25 -0
  157. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +611 -4
  158. package/src/storage/implementation/v3/MongoChecksumsV3.ts +262 -26
  159. package/src/storage/implementation/v3/MongoCompactorV3.ts +647 -30
  160. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +480 -0
  161. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +397 -111
  162. package/src/storage/implementation/v3/PersistedBatchV3.ts +63 -38
  163. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +59 -19
  164. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  165. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +48 -38
  166. package/src/storage/implementation/v3/bucket-format.ts +70 -0
  167. package/src/storage/implementation/v3/chunking.ts +38 -0
  168. package/src/storage/implementation/v3/models.ts +150 -44
  169. package/src/storage/implementation/v3/source-table-utils.ts +392 -0
  170. package/src/storage/storage-index.ts +12 -4
  171. package/src/utils/test-utils.ts +6 -3
  172. package/src/utils/util.ts +3 -2
  173. package/test/src/__snapshots__/storage.test.ts.snap +0 -75
  174. package/test/src/__snapshots__/storage_sync.test.ts.snap +48 -48
  175. package/test/src/cleanup-stopped-sync-configs.test.ts +648 -0
  176. package/test/src/storage.test.ts +11 -11
  177. package/test/src/storage_compacting.test.ts +1956 -6
  178. package/test/src/storage_sync.test.ts +1429 -42
  179. package/test/src/storeCurrentData.test.ts +221 -0
  180. package/test/src/util.ts +2 -1
  181. package/tsconfig.json +1 -4
  182. package/tsconfig.tsbuildinfo +1 -1
  183. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -17
  184. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  185. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  186. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  187. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -64
  188. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  189. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -15
  190. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -42
  191. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  192. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  193. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  194. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  195. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
  196. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
  197. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
  198. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -5
  199. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  200. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  201. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -72
  202. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -76
  203. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -56
  204. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  205. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
  206. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -12
@@ -7,18 +7,16 @@ import {
7
7
  ChecksumMap,
8
8
  FetchPartialBucketChecksum,
9
9
  InternalOpId,
10
- isPartialChecksum,
11
10
  PartialChecksum,
12
11
  PartialChecksumMap,
13
12
  PartialOrFullChecksum
14
13
  } from '@powersync/service-core';
15
14
  import type { VersionedPowerSyncMongo } from './db.js';
16
15
 
17
- import * as lib_mongo from '@powersync/lib-service-mongodb';
18
- import { BucketDefinitionId, BucketDefinitionMapping } from './BucketDefinitionMapping.js';
19
- import { BucketDataDocumentBase, StorageConfig } from './models.js';
16
+ import { BucketDefinitionId } from '@powersync/service-sync-rules';
17
+ import { StorageConfig } from './models.js';
20
18
 
21
- export interface FetchPartialBucketChecksumV3 {
19
+ export interface FetchPartialBucketChecksumByDefinition {
22
20
  bucket: string;
23
21
  definitionId: BucketDefinitionId;
24
22
  start?: InternalOpId;
@@ -46,15 +44,14 @@ export interface MongoChecksumOptions {
46
44
  operationBatchLimit?: number;
47
45
 
48
46
  storageConfig: StorageConfig;
49
- mapping?: BucketDefinitionMapping;
50
47
  }
51
48
 
52
49
  const DEFAULT_BUCKET_BATCH_LIMIT = 200;
53
- const DEFAULT_OPERATION_BATCH_LIMIT = 50_000;
50
+ export const DEFAULT_OPERATION_BATCH_LIMIT = 50_000;
54
51
 
55
52
  export abstract class MongoChecksums {
56
53
  private _cache: ChecksumCache | undefined;
57
- private readonly storageConfig: StorageConfig;
54
+ protected readonly storageConfig: StorageConfig;
58
55
 
59
56
  constructor(
60
57
  protected readonly db: VersionedPowerSyncMongo,
@@ -177,138 +174,10 @@ export abstract class MongoChecksums {
177
174
  protected abstract fetchPreStates(
178
175
  batch: FetchPartialBucketChecksum[]
179
176
  ): Promise<Map<string, { opId: InternalOpId; checksum: BucketChecksum }>>;
180
-
181
- protected async computePartialChecksumsForCollection<
182
- TRequest extends FetchPartialBucketChecksumByBucket,
183
- TBucketDataDocument extends BucketDataDocumentBase
184
- >(
185
- batch: TRequest[],
186
- collection: lib_mongo.mongo.Collection<TBucketDataDocument>,
187
- createFilter: (request: TRequest) => any
188
- ): Promise<PartialChecksumMap> {
189
- const batchLimit = this.options?.operationBatchLimit ?? DEFAULT_OPERATION_BATCH_LIMIT;
190
-
191
- // Map requests by bucket. We adjust this as we get partial results.
192
- let requests = new Map<string, TRequest>();
193
- for (let request of batch) {
194
- requests.set(request.bucket, request);
195
- }
196
-
197
- const partialChecksums = new Map<string, PartialOrFullChecksum>();
198
-
199
- while (requests.size > 0) {
200
- const filters = Array.from(requests.values(), createFilter);
201
-
202
- // Historically, checksum may be stored as 'int' or 'double'.
203
- // More recently, this should be a 'long'.
204
- // $toLong ensures that we always sum it as a long, avoiding inaccuracies in the calculations.
205
- const checksumLong = this.storageConfig.longChecksums ? '$checksum' : { $toLong: '$checksum' };
206
-
207
- // Aggregate over a max of `batchLimit` operations at a time.
208
- // Let's say we have 3 buckets (A, B, C), each with 10 operations, and our batch limit is 12.
209
- // Then we'll do three batches:
210
- // 1. Query: A[1-end], B[1-end], C[1-end]
211
- // Returns: A[1-10], B[1-2]
212
- // 2. Query: B[3-end], C[1-end]
213
- // Returns: B[3-10], C[1-4]
214
- // 3. Query: C[5-end]
215
- // Returns: C[5-10]
216
- const aggregate = await collection
217
- .aggregate(
218
- [
219
- {
220
- $match: {
221
- $or: filters
222
- }
223
- },
224
- // sort and limit _before_ grouping
225
- { $sort: { _id: 1 } },
226
- { $limit: batchLimit },
227
- {
228
- $group: {
229
- _id: '$_id.b',
230
- checksum_total: { $sum: checksumLong },
231
- count: { $sum: 1 },
232
- has_clear_op: {
233
- $max: {
234
- $cond: [{ $eq: ['$op', 'CLEAR'] }, 1, 0]
235
- }
236
- },
237
- last_op: { $max: '$_id.o' }
238
- }
239
- },
240
- // Sort the aggregated results (100 max, so should be fast).
241
- // This is important to identify which buckets we have partial data for.
242
- { $sort: { _id: 1 } }
243
- ],
244
- { session: undefined, readConcern: 'snapshot', maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS }
245
- )
246
- .toArray()
247
- .catch((e) => {
248
- throw lib_mongo.mapQueryError(e, 'while reading checksums');
249
- });
250
-
251
- let batchCount = 0;
252
- let limitReached = false;
253
- for (let doc of aggregate) {
254
- const bucket = doc._id;
255
- const checksum = checksumFromAggregate(doc);
256
-
257
- const existing = partialChecksums.get(bucket);
258
- if (existing != null) {
259
- partialChecksums.set(bucket, addPartialChecksums(bucket, existing, checksum));
260
- } else {
261
- partialChecksums.set(bucket, checksum);
262
- }
263
-
264
- batchCount += doc.count;
265
- if (batchCount == batchLimit) {
266
- // Limit reached. Request more in the next batch.
267
- // Note that this only affects the _last_ bucket in a batch.
268
- limitReached = true;
269
- const req = requests.get(bucket);
270
- requests.set(bucket, {
271
- ...req!,
272
- start: doc.last_op
273
- });
274
- } else {
275
- // All done for this bucket
276
- requests.delete(bucket);
277
- }
278
- }
279
- if (!limitReached) {
280
- break;
281
- }
282
- }
283
-
284
- return new Map<string, PartialOrFullChecksum>(
285
- batch.map((request) => {
286
- const bucket = request.bucket;
287
- // Could be null if we got no data
288
- let partialChecksum = partialChecksums.get(bucket);
289
- if (partialChecksum == null) {
290
- partialChecksum = {
291
- bucket,
292
- partialCount: 0,
293
- partialChecksum: 0
294
- };
295
- }
296
- if (request.start == null && isPartialChecksum(partialChecksum)) {
297
- partialChecksum = {
298
- bucket,
299
- count: partialChecksum.partialCount,
300
- checksum: partialChecksum.partialChecksum
301
- };
302
- }
303
-
304
- return [bucket, partialChecksum];
305
- })
306
- );
307
- }
308
177
  }
309
178
 
310
179
  export function emptyChecksumForRequest(
311
- request: Pick<FetchPartialBucketChecksum | FetchPartialBucketChecksumV3, 'bucket' | 'start'>
180
+ request: Pick<FetchPartialBucketChecksum | FetchPartialBucketChecksumByDefinition, 'bucket' | 'start'>
312
181
  ): PartialOrFullChecksum {
313
182
  if (request.start == null) {
314
183
  return { bucket: request.bucket, count: 0, checksum: 0 };
@@ -319,7 +188,7 @@ export function emptyChecksumForRequest(
319
188
  /**
320
189
  * Convert output of the $group stage into a checksum.
321
190
  */
322
- function checksumFromAggregate(doc: bson.Document): PartialOrFullChecksum {
191
+ export function checksumFromAggregate(doc: bson.Document): PartialOrFullChecksum {
323
192
  const partialChecksum = Number(BigInt(doc.checksum_total) & 0xffffffffn) & 0xffffffff;
324
193
  const bucket = doc._id;
325
194
 
@@ -13,8 +13,8 @@ import {
13
13
  storage,
14
14
  utils
15
15
  } from '@powersync/service-core';
16
+ import { BucketDefinitionId } from '@powersync/service-sync-rules';
16
17
 
17
- import { BucketDefinitionId } from './BucketDefinitionMapping.js';
18
18
  import { BucketDataDoc, BucketKey } from './common/BucketDataDoc.js';
19
19
  import { BucketDataDocumentGeneric, SingleBucketStore } from './common/SingleBucketStore.js';
20
20
  import type { VersionedPowerSyncMongo } from './db.js';
@@ -63,6 +63,7 @@ export interface MongoCompactOptions extends storage.CompactOptions {}
63
63
  const DEFAULT_CLEAR_BATCH_LIMIT = 5000;
64
64
  const DEFAULT_MOVE_BATCH_LIMIT = 2000;
65
65
  const DEFAULT_MOVE_BATCH_QUERY_LIMIT = 10_000;
66
+ const DEFAULT_MOVE_BATCH_BYTE_LIMIT = 16 * 1024 * 1024;
66
67
  const DEFAULT_MIN_BUCKET_CHANGES = 10;
67
68
  const DEFAULT_MIN_CHANGE_RATIO = 0.1;
68
69
  const DIRTY_BUCKET_SCAN_BATCH_SIZE = 2_000;
@@ -83,6 +84,7 @@ export abstract class MongoCompactor {
83
84
  protected readonly idLimitBytes: number;
84
85
  protected readonly moveBatchLimit: number;
85
86
  protected readonly moveBatchQueryLimit: number;
87
+ protected readonly moveBatchByteLimit: number;
86
88
  protected readonly clearBatchLimit: number;
87
89
  protected readonly minBucketChanges: number;
88
90
  protected readonly minChangeRatio: number;
@@ -98,11 +100,15 @@ export abstract class MongoCompactor {
98
100
  protected readonly db: VersionedPowerSyncMongo,
99
101
  options: MongoCompactOptions
100
102
  ) {
101
- this.group_id = storage.group_id;
103
+ this.group_id = storage.replicationStreamId;
102
104
  this.idLimitBytes = (options.memoryLimitMB ?? DEFAULT_MEMORY_LIMIT_MB) * 1024 * 1024;
103
105
  this.moveBatchLimit = options.moveBatchLimit ?? DEFAULT_MOVE_BATCH_LIMIT;
104
106
  this.moveBatchQueryLimit = options.moveBatchQueryLimit ?? DEFAULT_MOVE_BATCH_QUERY_LIMIT;
107
+ this.moveBatchByteLimit = options.moveBatchByteLimit ?? DEFAULT_MOVE_BATCH_BYTE_LIMIT;
105
108
  this.clearBatchLimit = options.clearBatchLimit ?? DEFAULT_CLEAR_BATCH_LIMIT;
109
+ if (this.clearBatchLimit < 2) {
110
+ throw new ReplicationAssertionError('clearBatchLimit must be >= 2');
111
+ }
106
112
  this.minBucketChanges = options.minBucketChanges ?? DEFAULT_MIN_BUCKET_CHANGES;
107
113
  this.minChangeRatio = options.minChangeRatio ?? DEFAULT_MIN_CHANGE_RATIO;
108
114
  this.maxOpId = options.maxOpId ?? 0n;
@@ -133,6 +139,7 @@ export abstract class MongoCompactor {
133
139
  */
134
140
  async populateChecksums(options: { minBucketChanges: number }): Promise<PopulateChecksumCacheResults> {
135
141
  let count = 0;
142
+ // Paginate through dirty buckets in batches until no more buckets meet the criteria.
136
143
  while (true) {
137
144
  this.signal?.throwIfAborted();
138
145
  const buckets = await this.dirtyBucketBatchForChecksums(options);
@@ -172,6 +179,7 @@ export abstract class MongoCompactor {
172
179
  },
173
180
  getDefinitionId: (state: TCollectionBucketState) => BucketDefinitionId | null
174
181
  ): AsyncGenerator<DirtyBucket[]> {
182
+ // Paginate through the bucket state collection using cursor-based scanning.
175
183
  while (true) {
176
184
  // To avoid timeouts from too many buckets not meeting the minBucketChanges criteria, use an aggregation pipeline
177
185
  // to scan a fixed batch of buckets at a time, but only return buckets that meet the criteria.
@@ -304,6 +312,7 @@ export abstract class MongoCompactor {
304
312
  */
305
313
  protected async compactSingleBucketRetried(bucket: string, definitionId: BucketDefinitionId | null = null) {
306
314
  let retryCount = 0;
315
+ // Retry with exponential backoff up to 3 times on MongoDB errors.
307
316
  while (true) {
308
317
  try {
309
318
  await this.compactSingleBucket(bucket, definitionId);
@@ -343,6 +352,7 @@ export abstract class MongoCompactor {
343
352
  // Upper bound is adjusted for each batch.
344
353
  let upperBound = bucketContext.maxId;
345
354
 
355
+ // Paginate through bucket data in batches to avoid cursor timeouts.
346
356
  while (true) {
347
357
  this.signal?.throwIfAborted();
348
358
 
@@ -353,11 +363,7 @@ export abstract class MongoCompactor {
353
363
  _id: {
354
364
  $gte: lowerBound,
355
365
  $lt: upperBound
356
- },
357
- // Workaround for a clustered collection bug where the $lt operator may include upperBound.
358
- // Technically only needed for storage V3.
359
- // https://jira.mongodb.org/browse/SERVER-121822
360
- '_id.o': { $lt: upperBound.o }
366
+ }
361
367
  }
362
368
  },
363
369
  { $sort: { _id: -1 } },
@@ -488,13 +494,13 @@ export abstract class MongoCompactor {
488
494
  await this.flush(bucketContext);
489
495
  }
490
496
 
491
- protected updateBucketChecksums(state: CurrentBucketState) {
497
+ protected collectBucketStateUpdates(state: CurrentBucketState): mongo.AnyBulkWriteOperation<BucketStateDocumentBase> {
492
498
  if (state.opCount < 0) {
493
499
  throw new ServiceAssertionError(
494
500
  `Invalid opCount: ${state.opCount} checksum ${state.checksum} opsSincePut: ${state.opsSincePut} maxOpId: ${this.maxOpId}`
495
501
  );
496
502
  }
497
- this.bucketStateUpdates.push({
503
+ return {
498
504
  updateOne: {
499
505
  filter: this.bucketStateFilter(state.bucket, state.definitionId),
500
506
  update: {
@@ -517,7 +523,11 @@ export abstract class MongoCompactor {
517
523
  // We don't create new ones here, to avoid issues with the unique index on bucket_updates.
518
524
  upsert: false
519
525
  }
520
- });
526
+ };
527
+ }
528
+
529
+ protected updateBucketChecksums(state: CurrentBucketState) {
530
+ this.bucketStateUpdates.push(this.collectBucketStateUpdates(state));
521
531
  }
522
532
 
523
533
  protected async flush(col: SingleBucketStore) {
@@ -0,0 +1,61 @@
1
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
2
+ import { SyncConfigWithRequiredMapping } from '@powersync/service-core';
3
+ import {
4
+ BucketDataScope,
5
+ BucketDataSource,
6
+ HydrationState,
7
+ ParameterIndexLookupCreator,
8
+ ParameterLookupScope
9
+ } from '@powersync/service-sync-rules';
10
+
11
+ export class MongoHydrationState implements HydrationState {
12
+ private bucketDataSourceSyncConfig = new WeakMap<BucketDataSource, SyncConfigWithRequiredMapping>();
13
+ private parameterIndexLookupSyncConfig = new WeakMap<ParameterIndexLookupCreator, SyncConfigWithRequiredMapping>();
14
+
15
+ constructor(
16
+ readonly syncConfigs: SyncConfigWithRequiredMapping[],
17
+ private readonly replicationStreamId: number
18
+ ) {
19
+ for (let syncConfig of syncConfigs) {
20
+ for (let source of syncConfig.syncConfig.config.bucketDataSources) {
21
+ this.bucketDataSourceSyncConfig.set(source, syncConfig);
22
+ }
23
+ for (let source of syncConfig.syncConfig.config.bucketParameterLookupSources) {
24
+ this.parameterIndexLookupSyncConfig.set(source, syncConfig);
25
+ }
26
+ }
27
+ }
28
+
29
+ getBucketSourceScope(source: BucketDataSource): BucketDataScope {
30
+ const syncConfig = this.bucketDataSourceSyncConfig.get(source);
31
+ if (syncConfig == null) {
32
+ throw new ServiceAssertionError(`No sync config found for bucket data source ${source.uniqueName}`);
33
+ }
34
+ const mapping = syncConfig.mapping;
35
+ const defId = mapping.bucketSourceId(source);
36
+ // Currently uniqueName is constant for a BucketDataSource within a replication stream. This may change in the
37
+ // future, in which case we need to reconsider the bucketPrefix here.
38
+ // However, the definition may change without changing the name, so we include the defId in the bucketPrefix.
39
+ // defId is not unique across replication streams, so we include the replicationStreamId as well.
40
+ return {
41
+ bucketPrefix: `${source.uniqueName}.${this.replicationStreamId.toString(16)}.${defId}`,
42
+ source
43
+ };
44
+ }
45
+
46
+ getParameterIndexLookupScope(source: ParameterIndexLookupCreator): ParameterLookupScope {
47
+ const syncConfig = this.parameterIndexLookupSyncConfig.get(source);
48
+ if (syncConfig == null) {
49
+ throw new ServiceAssertionError(
50
+ `No sync config found for parameter index lookup source ${source.sourceId.lookupName}#${source.sourceId.queryId}`
51
+ );
52
+ }
53
+ const mapping = syncConfig.mapping;
54
+ const defId = mapping.parameterLookupId(source);
55
+ return {
56
+ lookupName: defId,
57
+ queryId: '',
58
+ source
59
+ };
60
+ }
61
+ }
@@ -18,12 +18,13 @@ type ParameterCompactionReadDocument = {
18
18
  *
19
19
  * For background, see the `/docs/parameters-lookups.md` file.
20
20
  */
21
- export abstract class MongoParameterCompactor {
21
+ export class MongoParameterCompactor {
22
22
  constructor(
23
23
  protected readonly db: VersionedPowerSyncMongo,
24
24
  protected readonly group_id: number,
25
25
  protected readonly checkpoint: InternalOpId,
26
- protected readonly options: CompactOptions
26
+ protected readonly options: CompactOptions,
27
+ protected readonly getCollectionsCb?: () => Promise<mongo.Collection<mongo.Document>[]>
27
28
  ) {}
28
29
 
29
30
  async compact() {
@@ -33,11 +34,27 @@ export abstract class MongoParameterCompactor {
33
34
  }
34
35
  }
35
36
 
36
- protected abstract getCollections(): Promise<mongo.Collection<mongo.Document>[]>;
37
+ protected async getCollections(): Promise<mongo.Collection<mongo.Document>[]> {
38
+ if (this.getCollectionsCb == null) {
39
+ throw new Error('getCollections callback not provided');
40
+ }
41
+ const collections = await this.getCollectionsCb();
42
+ // Cast from the version-specific collection type to the generic Document type
43
+ // used by the parameter compactor base class.
44
+ return collections.map((collection) => collection as unknown as mongo.Collection<mongo.Document>);
45
+ }
37
46
 
38
- protected abstract collectionFilter(): mongo.Document;
47
+ protected collectionFilter(): mongo.Document {
48
+ return {};
49
+ }
39
50
 
40
- protected abstract deleteFilter(doc: mongo.Document): mongo.Document;
51
+ protected deleteFilter(doc: mongo.Document): mongo.Document {
52
+ return {
53
+ lookup: doc.lookup,
54
+ _id: { $lte: doc._id },
55
+ key: doc.key
56
+ };
57
+ }
41
58
 
42
59
  protected async compactCollection(collection: mongo.Collection<mongo.Document>) {
43
60
  // This is the currently-active checkpoint.
@@ -68,6 +85,7 @@ export abstract class MongoParameterCompactor {
68
85
 
69
86
  const flush = async (force: boolean) => {
70
87
  if (removeIds.length >= 1000 || (force && removeIds.length > 0)) {
88
+ // MongoDB Filter<T> doesn't fully match our dynamic delete filter shape here.
71
89
  const results = await collection.deleteMany({ _id: { $in: removeIds } } as any);
72
90
  logger.info(`Removed ${results.deletedCount} (${removeIds.length}) superseded parameter entries`);
73
91
  removeIds = [];
@@ -81,6 +99,7 @@ export abstract class MongoParameterCompactor {
81
99
  };
82
100
 
83
101
  while (await cursor.hasNext()) {
102
+ // readBufferedDocuments returns a generic type; we know the shape from our projection.
84
103
  const batch = cursor.readBufferedDocuments() as unknown as ParameterCompactionReadDocument[];
85
104
  checkedEntries += batch.length;
86
105
  const now = Date.now();
@@ -0,0 +1,88 @@
1
+ import * as sqlite from 'node:sqlite';
2
+
3
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
4
+ import {
5
+ BucketDefinitionMapping,
6
+ MultiSyncConfigBucketDefinitionMapping,
7
+ SingleSyncConfigBucketDefinitionMapping,
8
+ storage,
9
+ SyncConfigWithMapping,
10
+ SyncConfigWithRequiredMapping
11
+ } from '@powersync/service-core';
12
+ import {
13
+ CompatibilityOption,
14
+ DEFAULT_HYDRATION_STATE,
15
+ HydratedSyncConfig,
16
+ HydrationState,
17
+ nodeSqlite,
18
+ SyncConfigWithErrors,
19
+ versionedHydrationState
20
+ } from '@powersync/service-sync-rules';
21
+ import { StorageConfig } from './models.js';
22
+ import { MongoHydrationState } from './MongoHydrationState.js';
23
+
24
+ export class MongoParsedSyncConfigSet implements storage.ParsedSyncConfigSet {
25
+ public readonly hydrationState: HydrationState;
26
+ public readonly syncConfigs: SyncConfigWithErrors[];
27
+ public readonly replicationStreamName: string;
28
+ public readonly mapping: BucketDefinitionMapping;
29
+
30
+ constructor(
31
+ public readonly replicationStreamId: number,
32
+ storageConfig: StorageConfig,
33
+ slotName: string,
34
+ syncConfigs: SyncConfigWithMapping[]
35
+ ) {
36
+ this.replicationStreamName = slotName;
37
+ this.syncConfigs = syncConfigs.map((config) => config.syncConfig);
38
+ if (this.syncConfigs.length == 0) {
39
+ throw new ServiceAssertionError(`At least one sync config is required`);
40
+ }
41
+ const [firstConfig] = this.syncConfigs;
42
+ const compatibility = firstConfig.config.compatibility;
43
+ for (const config of this.syncConfigs) {
44
+ if (config.config.compatibility.equals(compatibility)) {
45
+ continue;
46
+ }
47
+ throw new ServiceAssertionError(
48
+ `All sync configs in a replication stream must use the same compatibility options`
49
+ );
50
+ }
51
+
52
+ if (storageConfig.incrementalReprocessing) {
53
+ if (syncConfigs.some((c) => c.mapping == null)) {
54
+ throw new ServiceAssertionError(`mapping is required for v3 storage`);
55
+ }
56
+ const mappedConfigs = syncConfigs as SyncConfigWithRequiredMapping[];
57
+ this.hydrationState = new MongoHydrationState(mappedConfigs, this.replicationStreamId);
58
+ this.mapping = new MultiSyncConfigBucketDefinitionMapping(mappedConfigs);
59
+ } else if (!compatibility.isEnabled(CompatibilityOption.versionedBucketIds) && !storageConfig.versionedBuckets) {
60
+ const [syncConfig] = syncConfigs;
61
+ if (syncConfigs.length != 1 || syncConfig == null) {
62
+ throw new ServiceAssertionError(`Non-incremental storage requires exactly one sync config`);
63
+ }
64
+ this.hydrationState = DEFAULT_HYDRATION_STATE;
65
+ this.mapping = syncConfig.mapping ?? new SingleSyncConfigBucketDefinitionMapping();
66
+ } else {
67
+ const [syncConfig] = syncConfigs;
68
+ if (syncConfigs.length != 1 || syncConfig == null) {
69
+ throw new ServiceAssertionError(`Non-incremental storage requires exactly one sync config`);
70
+ }
71
+ this.hydrationState = versionedHydrationState(this.replicationStreamId);
72
+ this.mapping = syncConfig.mapping ?? new SingleSyncConfigBucketDefinitionMapping();
73
+ }
74
+ }
75
+
76
+ #hydratedSyncConfig: HydratedSyncConfig | undefined;
77
+
78
+ get hydratedSyncConfig(): HydratedSyncConfig {
79
+ this.#hydratedSyncConfig ??= new HydratedSyncConfig({
80
+ definitions: this.syncConfigs.map((config) => config.config),
81
+ createParams: {
82
+ hydrationState: this.hydrationState,
83
+ sqlite: nodeSqlite(sqlite)
84
+ }
85
+ });
86
+ return this.#hydratedSyncConfig;
87
+ }
88
+ }
@@ -0,0 +1,118 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
3
+ import { ReplicationStreamStorageIds, SingleSyncConfigBucketDefinitionMapping, storage } from '@powersync/service-core';
4
+ import * as bson from 'bson';
5
+ import { ReplicationStreamDocumentV3, SyncConfigDefinition } from '../storage-index.js';
6
+ import { PowerSyncMongo } from './db.js';
7
+ import { getMongoStorageConfig } from './models.js';
8
+ import { MongoParsedSyncConfigSet } from './MongoParsedSyncConfigSet.js';
9
+ import {
10
+ MongoPersistedSyncConfigContentBase,
11
+ MongoPersistedSyncConfigContentV1,
12
+ MongoPersistedSyncConfigContentV3
13
+ } from './MongoPersistedSyncConfigContent.js';
14
+ import { MongoSyncRulesLock } from './MongoSyncRulesLock.js';
15
+ import { SyncRuleDocumentV1 } from './v1/models.js';
16
+
17
+ export class MongoPersistedReplicationStream extends storage.PersistedReplicationStream {
18
+ public current_lock: MongoSyncRulesLock | null = null;
19
+ public readonly syncConfigContent: readonly MongoPersistedSyncConfigContentBase[];
20
+
21
+ constructor(
22
+ private readonly db: PowerSyncMongo,
23
+ private readonly doc: SyncRuleDocumentV1 | ReplicationStreamDocumentV3,
24
+ private readonly configs: SyncConfigDefinition[] = []
25
+ ) {
26
+ const storageVersion = doc.storage_version ?? storage.LEGACY_STORAGE_VERSION;
27
+ const replicationJobId =
28
+ configs.length == 0
29
+ ? String(doc._id)
30
+ : `${doc._id}:${configs
31
+ .map((config) => config._id.toHexString())
32
+ .sort()
33
+ .join(',')}`;
34
+
35
+ super({
36
+ replicationStreamId: doc._id,
37
+ replicationStreamName: doc.slot_name ?? `powersync_${doc._id}`,
38
+ state: doc.state,
39
+ storageVersion,
40
+ replicationJobId
41
+ });
42
+
43
+ this.syncConfigContent = this.createSyncConfigContent();
44
+ }
45
+
46
+ getStorageConfig() {
47
+ return getMongoStorageConfig(this.storageVersion);
48
+ }
49
+
50
+ private createSyncConfigContent(): MongoPersistedSyncConfigContentBase[] {
51
+ if (this.getStorageConfig().incrementalReprocessing) {
52
+ if (this.configs.length == 0) {
53
+ throw new ServiceAssertionError(`Cannot create v3 storage without sync config definitions`);
54
+ }
55
+ return this.configs.map(
56
+ (config) => new MongoPersistedSyncConfigContentV3(this.db, this.doc as ReplicationStreamDocumentV3, config)
57
+ );
58
+ }
59
+
60
+ return [new MongoPersistedSyncConfigContentV1(this.db, this.doc as SyncRuleDocumentV1)];
61
+ }
62
+
63
+ get syncConfigIds(): bson.ObjectId[] {
64
+ return this.configs.map((config) => config._id);
65
+ }
66
+
67
+ #storageIds: ReplicationStreamStorageIds | undefined;
68
+
69
+ /**
70
+ * Persisted storage ids for all sync configs in this replication stream.
71
+ *
72
+ * Derived from the persisted rule_mapping documents - requires no parsed sync configs.
73
+ */
74
+ get storageIds(): ReplicationStreamStorageIds {
75
+ this.#storageIds ??= new ReplicationStreamStorageIds(this.syncConfigContent.map((content) => content.mapping));
76
+ return this.#storageIds;
77
+ }
78
+
79
+ /**
80
+ * Parse the sync configs for this replication stream.
81
+ *
82
+ * This creates a fresh parse on every call. Do not call this from operational paths -
83
+ * use {@link MongoSyncBucketStorage.getParsedSyncConfigSet} instead, which returns a
84
+ * canonical instance so that parsed source objects and mappings stay associated.
85
+ */
86
+ parsed(options: storage.ParseSyncConfigOptions): MongoParsedSyncConfigSet {
87
+ const storageConfig = this.getStorageConfig();
88
+ if (!storageConfig.incrementalReprocessing) {
89
+ return this.syncConfigContent[0].parsed(options);
90
+ }
91
+
92
+ const syncConfigs = this.configs.map((config) => {
93
+ return {
94
+ syncConfigId: config._id.toHexString(),
95
+ syncConfig: storage.parsePersistedSyncConfigContent({
96
+ content: config.content,
97
+ compiledPlan: config.serialized_plan ?? null,
98
+ storageVersion: this.storageVersion,
99
+ parseOptions: options
100
+ }),
101
+ mapping: SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(config.rule_mapping)
102
+ };
103
+ });
104
+
105
+ return new MongoParsedSyncConfigSet(
106
+ this.replicationStreamId,
107
+ storageConfig,
108
+ this.replicationStreamName,
109
+ syncConfigs
110
+ );
111
+ }
112
+
113
+ async lock(session?: mongo.ClientSession) {
114
+ const lock = await MongoSyncRulesLock.createLock(this.db.versioned(this.getStorageConfig()), this, session);
115
+ this.current_lock = lock;
116
+ return lock;
117
+ }
118
+ }