@powersync/service-module-mongodb-storage 0.18.3 → 0.20.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 (145) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
  3. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
  4. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
  5. package/dist/storage/MongoBucketStorage.d.ts +5 -1
  6. package/dist/storage/MongoBucketStorage.js +5 -1
  7. package/dist/storage/MongoBucketStorage.js.map +1 -1
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +3 -0
  9. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  10. package/dist/storage/implementation/MongoCompactor.d.ts +15 -19
  11. package/dist/storage/implementation/MongoCompactor.js +68 -260
  12. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  13. package/dist/storage/implementation/MongoStorageProvider.js +26 -3
  14. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  15. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +10 -4
  16. package/dist/storage/implementation/MongoSyncBucketStorage.js +13 -12
  17. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  18. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
  19. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +146 -34
  20. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  21. package/dist/storage/implementation/common/PersistedBatch.d.ts +6 -0
  22. package/dist/storage/implementation/common/PersistedBatch.js +15 -8
  23. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  24. package/dist/storage/implementation/models.d.ts +20 -0
  25. package/dist/storage/implementation/models.js.map +1 -1
  26. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +4 -0
  27. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +58 -84
  28. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  29. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
  30. package/dist/storage/implementation/v1/MongoCompactorV1.js +250 -1
  31. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  32. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +19 -11
  33. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  34. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
  35. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
  36. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
  37. package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
  38. package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
  39. package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
  40. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +35 -23
  41. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  42. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +1 -0
  43. package/dist/storage/implementation/v3/MongoChecksumsV3.js +59 -103
  44. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  45. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -2
  46. package/dist/storage/implementation/v3/MongoCompactorV3.js +354 -219
  47. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  48. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +5 -0
  49. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +12 -11
  50. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  51. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +5 -3
  52. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +108 -106
  53. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  54. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +5 -2
  55. package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -15
  56. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  57. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +2 -1
  58. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
  59. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  60. package/dist/storage/implementation/v3/bucket-format.d.ts +10 -1
  61. package/dist/storage/implementation/v3/bucket-format.js +28 -9
  62. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  63. package/dist/storage/implementation/v3/chunking.js +3 -1
  64. package/dist/storage/implementation/v3/chunking.js.map +1 -1
  65. package/dist/storage/implementation/v3/models.d.ts +43 -2
  66. package/dist/storage/implementation/v3/models.js.map +1 -1
  67. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
  68. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
  69. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
  70. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
  71. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
  72. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
  73. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
  74. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
  75. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
  76. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
  77. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
  78. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
  79. package/dist/storage/implementation/v3/source-table-utils.d.ts +9 -1
  80. package/dist/storage/implementation/v3/source-table-utils.js +20 -21
  81. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  82. package/dist/types/types.d.ts +35 -0
  83. package/dist/types/types.js +35 -1
  84. package/dist/types/types.js.map +1 -1
  85. package/dist/utils/test-utils.js +3 -1
  86. package/dist/utils/test-utils.js.map +1 -1
  87. package/dist/utils/util.d.ts +8 -1
  88. package/dist/utils/util.js +125 -2
  89. package/dist/utils/util.js.map +1 -1
  90. package/package.json +10 -7
  91. package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
  92. package/src/storage/MongoBucketStorage.ts +10 -2
  93. package/src/storage/implementation/MongoBucketBatch.ts +4 -0
  94. package/src/storage/implementation/MongoCompactor.ts +90 -314
  95. package/src/storage/implementation/MongoStorageProvider.ts +32 -3
  96. package/src/storage/implementation/MongoSyncBucketStorage.ts +23 -22
  97. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +164 -37
  98. package/src/storage/implementation/common/PersistedBatch.ts +22 -10
  99. package/src/storage/implementation/models.ts +20 -0
  100. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +76 -96
  101. package/src/storage/implementation/v1/MongoCompactorV1.ts +292 -8
  102. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +53 -40
  103. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
  104. package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
  105. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +40 -23
  106. package/src/storage/implementation/v3/MongoChecksumsV3.ts +75 -105
  107. package/src/storage/implementation/v3/MongoCompactorV3.ts +456 -258
  108. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +21 -19
  109. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +122 -126
  110. package/src/storage/implementation/v3/PersistedBatchV3.ts +69 -16
  111. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +5 -0
  112. package/src/storage/implementation/v3/bucket-format.ts +36 -9
  113. package/src/storage/implementation/v3/chunking.ts +3 -1
  114. package/src/storage/implementation/v3/models.ts +45 -1
  115. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
  116. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
  117. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
  118. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
  119. package/src/storage/implementation/v3/source-table-utils.ts +43 -35
  120. package/src/types/types.ts +42 -1
  121. package/src/utils/test-utils.ts +3 -1
  122. package/src/utils/util.ts +182 -4
  123. package/test/src/__snapshots__/storage_sync.test.ts.snap +1 -583
  124. package/test/src/cleanup-stopped-sync-configs.test.ts +24 -1
  125. package/test/src/helpers/MemoryObjectStorage.ts +53 -0
  126. package/test/src/helpers/s3TestFactory.ts +69 -0
  127. package/test/src/object_storage_lifecycle.test.ts +59 -0
  128. package/test/src/setup.ts +6 -1
  129. package/test/src/storage.test.ts +216 -2
  130. package/test/src/storage_compacting.test.ts +176 -187
  131. package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
  132. package/test/src/storage_s3_checksums.test.ts +191 -0
  133. package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
  134. package/test/src/storage_s3_reading.test.ts +526 -0
  135. package/test/src/storage_s3_writing.test.ts +135 -0
  136. package/test/src/storage_sync.test.ts +124 -5
  137. package/tsconfig.tsbuildinfo +1 -1
  138. package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
  139. package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
  140. package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
  141. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
  142. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -84
  143. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
  144. package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
  145. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -108
@@ -16,7 +16,15 @@ import {
16
16
  import { JSONBig } from '@powersync/service-jsonbig';
17
17
  import { ParameterLookupRows, ScopedParameterLookup, SqliteJsonRow } from '@powersync/service-sync-rules';
18
18
  import * as bson from 'bson';
19
- import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '../../../utils/util.js';
19
+ import {
20
+ clearCollectionInIdBatches,
21
+ clearCollectionInIdRanges,
22
+ idPrefixFilter,
23
+ mapOpEntry,
24
+ readSingleBatch,
25
+ retryOnMongoMaxTimeMSExpired,
26
+ setSessionSnapshotTime
27
+ } from '../../../utils/util.js';
20
28
  import { MongoBucketStorage } from '../../MongoBucketStorage.js';
21
29
  import { MongoSyncBucketStorageCheckpoint } from '../common/MongoSyncBucketStorageCheckpoint.js';
22
30
  import { SourceKey } from '../models.js';
@@ -214,64 +222,59 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
214
222
  }
215
223
 
216
224
  protected async clearBucketData(signal?: AbortSignal): Promise<void> {
217
- await this.clearDeleteMany(
225
+ await clearCollectionInIdRanges(
226
+ this.logger,
218
227
  'bucket data',
219
- () =>
220
- this.db.bucket_data.deleteMany(
221
- {
222
- _id: idPrefixFilter<BucketDataKeyV1>({ g: this.replicationStreamId }, ['b', 'o'])
223
- },
224
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
225
- ),
226
- signal
228
+ this.db.bucket_data,
229
+ {
230
+ _id: idPrefixFilter<BucketDataKeyV1>({ g: this.replicationStreamId }, ['b', 'o'])
231
+ },
232
+ signal,
233
+ this.clearBatchThrottleRate
227
234
  );
228
235
  }
229
236
 
230
237
  protected async clearParameterIndexes(signal?: AbortSignal): Promise<void> {
231
- await this.clearDeleteMany(
238
+ await clearCollectionInIdBatches(
239
+ this.logger,
232
240
  'parameter index',
233
- () =>
234
- this.db.parameterIndexV1.deleteMany(
235
- {
236
- 'key.g': this.replicationStreamId
237
- },
238
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
239
- ),
240
- signal
241
+ this.db.parameterIndexV1,
242
+ {
243
+ 'key.g': this.replicationStreamId
244
+ },
245
+ signal,
246
+ this.clearBatchThrottleRate
241
247
  );
242
248
  }
243
249
 
244
250
  protected async clearSourceRecords(signal?: AbortSignal): Promise<void> {
245
- await this.clearDeleteMany(
251
+ await clearCollectionInIdRanges(
252
+ this.logger,
246
253
  'source records',
247
- () =>
248
- this.db.sourceRecordsV1.deleteMany(
249
- {
250
- _id: idPrefixFilter<SourceKey>({ g: this.replicationStreamId }, ['t', 'k'])
251
- },
252
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
253
- ),
254
- signal
254
+ this.db.sourceRecordsV1,
255
+ {
256
+ _id: idPrefixFilter<SourceKey>({ g: this.replicationStreamId }, ['t', 'k'])
257
+ },
258
+ signal,
259
+ this.clearBatchThrottleRate
255
260
  );
256
261
  }
257
262
 
258
263
  protected async clearBucketState(signal?: AbortSignal): Promise<void> {
259
- await this.clearDeleteMany(
264
+ await clearCollectionInIdRanges(
265
+ this.logger,
260
266
  'bucket state',
261
- () =>
262
- this.db.bucketStateV1.deleteMany(
263
- {
264
- _id: idPrefixFilter<BucketStateDocument['_id']>({ g: this.replicationStreamId }, ['b'])
265
- },
266
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
267
- ),
268
- signal
267
+ this.db.bucketStateV1,
268
+ {
269
+ _id: idPrefixFilter<BucketStateDocument['_id']>({ g: this.replicationStreamId }, ['b'])
270
+ },
271
+ signal,
272
+ this.clearBatchThrottleRate
269
273
  );
270
274
  }
271
275
 
272
276
  protected async clearSourceTables(signal?: AbortSignal): Promise<void> {
273
- await this.clearDeleteMany(
274
- 'source tables',
277
+ await retryOnMongoMaxTimeMSExpired(
275
278
  () =>
276
279
  this.db.sourceTablesV1(this.replicationStreamId).deleteMany(
277
280
  {
@@ -279,7 +282,17 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
279
282
  },
280
283
  { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
281
284
  ),
282
- signal
285
+ {
286
+ signal,
287
+ abortMessage: 'Aborted clearing data',
288
+ // This is a fairly long delay - only expected to hit this when the storage database is under high load.
289
+ retryDelayMs: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS,
290
+ onRetry: () => {
291
+ this.logger.info(
292
+ `Clearing batch of source tables timed out after ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, retrying...`
293
+ );
294
+ }
295
+ }
283
296
  );
284
297
  }
285
298
 
@@ -1,58 +1,50 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import { InternalOpId } from '@powersync/service-core';
3
3
  import { BucketDataDoc, BucketKey } from '../common/BucketDataDoc.js';
4
- import {
5
- BucketDataDocumentGeneric,
6
- BucketDataDocumentGenericId,
7
- SingleBucketStore
8
- } from '../common/SingleBucketStore.js';
9
4
  import { BucketDataProperties } from '../models.js';
10
5
  import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
11
6
  import { BucketDataDocumentV1, BucketDataKeyV1, serializeBucketDataV1 } from './models.js';
12
7
 
13
- export class SingleBucketStoreV1 implements SingleBucketStore {
14
- public readonly collection: mongo.Collection<BucketDataDocumentGeneric>;
8
+ export class SingleBucketStoreV1 {
9
+ public readonly collection: mongo.Collection<BucketDataDocumentV1>;
15
10
 
16
11
  constructor(
17
- private db: VersionedPowerSyncMongoV1,
12
+ db: VersionedPowerSyncMongoV1,
18
13
  public readonly key: BucketKey
19
14
  ) {
20
- this.collection = db.bucketDataV1 as unknown as mongo.Collection<BucketDataDocumentGeneric>;
15
+ this.collection = db.bucketDataV1;
21
16
  }
22
17
 
23
- docId(o: InternalOpId): BucketDataDocumentGenericId {
24
- // `satisfies BucketDataKeyV1` checks that we use the correct type for V1 storage
25
- // `as anyt` is to allow casting to the interface virtual type
18
+ docId(o: InternalOpId): BucketDataKeyV1 {
26
19
  return {
27
20
  g: this.key.replicationStreamId,
28
21
  b: this.key.bucket,
29
22
  o
30
- } satisfies BucketDataKeyV1 as any;
23
+ };
31
24
  }
32
25
 
33
- get minId(): BucketDataDocumentGenericId {
26
+ get minId(): BucketDataKeyV1 {
34
27
  return {
35
28
  g: this.key.replicationStreamId,
36
29
  b: this.key.bucket,
37
- o: new mongo.MinKey()
38
- } as any;
30
+ o: new mongo.MinKey() as any
31
+ };
39
32
  }
40
33
 
41
- get maxId(): BucketDataDocumentGenericId {
34
+ get maxId(): BucketDataKeyV1 {
42
35
  return {
43
36
  g: this.key.replicationStreamId,
44
37
  b: this.key.bucket,
45
- o: new mongo.MaxKey()
46
- } as any;
38
+ o: new mongo.MaxKey() as any
39
+ };
47
40
  }
48
41
 
49
- toPersistedDocument(source: Omit<BucketDataDoc, 'bucketKey'>): BucketDataDocumentGeneric {
50
- return serializeBucketDataV1({ bucketKey: this.key, ...source }) as unknown as BucketDataDocumentGeneric;
42
+ toPersistedDocument(source: Omit<BucketDataDoc, 'bucketKey'>): BucketDataDocumentV1 {
43
+ return serializeBucketDataV1({ bucketKey: this.key, ...source });
51
44
  }
52
45
 
53
- fromPersistedDocument(doc: BucketDataDocumentGeneric): BucketDataDoc {
54
- const document = doc as unknown as BucketDataDocumentV1;
55
- const { _id, ...rest } = document;
46
+ fromPersistedDocument(doc: BucketDataDocumentV1): BucketDataDoc {
47
+ const { _id, ...rest } = doc;
56
48
  return {
57
49
  bucketKey: this.key,
58
50
  o: _id.o,
@@ -61,10 +53,9 @@ export class SingleBucketStoreV1 implements SingleBucketStore {
61
53
  }
62
54
 
63
55
  fromPartialPersistedDocument<T extends keyof BucketDataProperties>(
64
- doc: Pick<BucketDataDocumentGeneric, '_id' | T>
56
+ doc: Pick<BucketDataDocumentV1, '_id' | T>
65
57
  ): Pick<BucketDataDoc, 'bucketKey' | 'o' | T> {
66
- const document = doc as Pick<BucketDataDocumentV1, '_id' | T>;
67
- const { _id, ...rest } = document;
58
+ const { _id, ...rest } = doc;
68
59
  return {
69
60
  bucketKey: this.key,
70
61
  o: _id.o,
@@ -0,0 +1,32 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { InternalOpId } from '@powersync/service-core';
3
+ import { BucketKey } from '../common/BucketDataDoc.js';
4
+ import { BucketDataKey } from '../models.js';
5
+ import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
6
+ import { BucketDataDocumentV3 } from './models.js';
7
+
8
+ export class BucketDataContextV3 {
9
+ public readonly collection: mongo.Collection<BucketDataDocumentV3>;
10
+
11
+ constructor(
12
+ db: VersionedPowerSyncMongoV3,
13
+ public readonly key: BucketKey
14
+ ) {
15
+ this.collection = db.bucketData(key.replicationStreamId, key.definitionId);
16
+ }
17
+
18
+ docId(o: InternalOpId): BucketDataKey {
19
+ return {
20
+ b: this.key.bucket,
21
+ o
22
+ };
23
+ }
24
+
25
+ get minId(): BucketDataKey {
26
+ // MongoDB's MinKey sentinel does not match the bigint type for _id.o.
27
+ return {
28
+ b: this.key.bucket,
29
+ o: new mongo.MinKey()
30
+ } as unknown as BucketDataKey;
31
+ }
32
+ }
@@ -43,7 +43,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
43
43
 
44
44
  protected createPersistedBatch(writtenSize: number): PersistedBatch {
45
45
  return new PersistedBatchV3(this.db, this.replicationStreamId, this.mapping, writtenSize, {
46
- logger: this.logger
46
+ logger: this.logger,
47
+ objectStorage: this.options.objectStorage,
48
+ inlineThresholdBytes: this.options.inlineThresholdBytes
47
49
  });
48
50
  }
49
51
 
@@ -146,23 +148,16 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
146
148
  const mapping = parsedOverride?.mapping ?? this.mapping;
147
149
 
148
150
  const { connection_id, source } = options;
149
- const context: SourceTableReconciliationContext = {
150
- connectionId: connection_id,
151
- connectionTag: source.connectionTag,
152
- identity: {
153
- schema: source.schema,
154
- name: source.name,
155
- objectId: source.objectId,
156
- replicaIdColumns: source.replicaIdColumns.map((column) => ({
157
- name: column.name,
158
- type: column.type,
159
- type_oid: column.typeId
160
- }))
161
- },
162
- storeCurrentData: source.sendsCompleteRows !== true,
163
- syncConfig,
164
- mapping,
165
- desired: sourceTableDesiredResolution(syncConfig, source, mapping)
151
+ const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
152
+ const identity = {
153
+ schema: source.schema,
154
+ name: source.name,
155
+ objectId: source.objectId,
156
+ replicaIdColumns: source.replicaIdColumns.map((column) => ({
157
+ name: column.name,
158
+ type: column.type,
159
+ type_oid: column.typeId
160
+ }))
166
161
  };
167
162
 
168
163
  let result: storage.ResolveTablesResult | null = null;
@@ -172,14 +167,36 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
172
167
  await session.withTransaction(async () => {
173
168
  const col = this.db.sourceTables(this.replicationStreamId);
174
169
 
175
- // Fetch every persisted source-table doc that can overlap this physical table.
176
- // Exact-identity docs are candidates for reuse; non-exact overlaps are possible drops.
170
+ // Find records that overlap by name or relation id.
177
171
  const candidateDocs = await col
178
- .find(overlappingSourceTableFilter(connection_id, context.identity), { session })
172
+ .find(overlappingSourceTableFilter(connection_id, identity), { session })
179
173
  .toArray();
180
174
 
181
- // Pure planning: which docs to retain (and narrow), whether a new doc is needed for
182
- // uncovered memberships, and which docs conflict with the current identity.
175
+ const candidateTables = candidateDocs.map((doc) =>
176
+ sourceTableFromDocument(doc, source.connectionTag, syncConfig, mapping)
177
+ );
178
+ const candidates = candidateTables.map((table) => table.clone());
179
+ const resolution = await reconcile({ source, candidates });
180
+ storage.validateSourceTableCandidateResolution(candidates, resolution);
181
+
182
+ // Persist metadata from the reconciler without mutating the queried documents.
183
+ for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
184
+ await col.updateOne({ _id: mongoTableId(id) }, { $set: { source_metadata: sourceMetadata } }, { session });
185
+ }
186
+
187
+ const context: SourceTableReconciliationContext = {
188
+ connectionId: connection_id,
189
+ connectionTag: source.connectionTag,
190
+ identity,
191
+ storeCurrentData: source.sendsCompleteRows !== true,
192
+ syncConfig,
193
+ mapping,
194
+ desired: sourceTableDesiredResolution(syncConfig, source, mapping),
195
+ sourceCompatibleTables: resolution.compatibleTables,
196
+ newTableSourceMetadata: resolution.newTableValues.sourceMetadata
197
+ };
198
+
199
+ // Plan record reuse, membership changes, creation, and removal.
183
200
  const plan = planSourceTableReconciliation(candidateDocs, context);
184
201
 
185
202
  // Persist narrowing for incomplete snapshots only. Snapshot-complete docs keep stale
@@ -1,7 +1,9 @@
1
1
  import * as lib_mongo from '@powersync/lib-service-mongodb';
2
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
2
3
  import {
3
4
  bson,
4
5
  BucketChecksum,
6
+ CheckpointChecksumInvalidatedError,
5
7
  FetchPartialBucketChecksum,
6
8
  InternalOpId,
7
9
  isPartialChecksum,
@@ -179,9 +181,42 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
179
181
  requests.set(request.bucket, request);
180
182
  }
181
183
 
182
- const pipeline = this.buildPartialChecksumPipeline(requests);
183
- const aggregate = await collection
184
- .aggregate(pipeline, {
184
+ const aggregate = await this.aggregatePartialChecksums(collection, requests, context);
185
+ const { checksums, startStraddledBuckets } = this.normalizePartialChecksumResults(batch, aggregate);
186
+
187
+ if (startStraddledBuckets.size == 0) {
188
+ return checksums;
189
+ }
190
+
191
+ // A cached base ends inside a compaction-produced document. Its checksum cannot
192
+ // be safely combined with the document-level checksum, so recalculate this
193
+ // bucket from the beginning. A full result replaces the cached base upstream.
194
+ const fullRequests = batch
195
+ .filter((request) => startStraddledBuckets.has(request.bucket))
196
+ .map((request) => ({ ...request, start: undefined }));
197
+ const fullRequestMap = new Map(fullRequests.map((request) => [request.bucket, request]));
198
+ const fullAggregate = await this.aggregatePartialChecksums(collection, fullRequestMap, context);
199
+ const fullResults = this.normalizePartialChecksumResults(fullRequests, fullAggregate);
200
+
201
+ if (fullResults.startStraddledBuckets.size > 0) {
202
+ throw new ServiceAssertionError(
203
+ `Unexpected start-boundary straddle while recalculating bucket(s) ${[...fullResults.startStraddledBuckets].join(', ')}`
204
+ );
205
+ }
206
+ for (const [bucket, checksum] of fullResults.checksums) {
207
+ checksums.set(bucket, checksum);
208
+ }
209
+
210
+ return checksums;
211
+ }
212
+
213
+ private async aggregatePartialChecksums(
214
+ collection: lib_mongo.mongo.Collection<BucketDataDocumentV3>,
215
+ requests: Map<string, FetchPartialBucketChecksumByBucket>,
216
+ context: MongoChecksumSessionContext
217
+ ): Promise<bson.Document[]> {
218
+ return collection
219
+ .aggregate(this.buildPartialChecksumPipeline(requests), {
185
220
  ...context.readOptions,
186
221
  maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS
187
222
  })
@@ -189,8 +224,6 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
189
224
  .catch((e) => {
190
225
  throw lib_mongo.mapQueryError(e, 'while reading checksums');
191
226
  });
192
-
193
- return this.normalizePartialChecksumResults(batch, aggregate);
194
227
  }
195
228
 
196
229
  private buildPartialChecksumPipeline(requests: Map<string, FetchPartialBucketChecksumByBucket>): bson.Document[] {
@@ -203,128 +236,64 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
203
236
  $or: filters
204
237
  }
205
238
  },
206
- {
207
- $addFields: {
208
- bucket_start: {
209
- $switch: {
210
- branches: Array.from(requests.entries()).map(([bucket, req]) => ({
211
- case: { $eq: ['$_id.b', bucket] },
212
- then: req.start ?? new bson.MinKey()
213
- })),
214
- default: new bson.MinKey()
215
- }
216
- },
217
- bucket_end: {
218
- $switch: {
219
- branches: Array.from(requests.entries()).map(([bucket, req]) => ({
220
- case: { $eq: ['$_id.b', bucket] },
221
- then: req.end
222
- })),
223
- default: new bson.MaxKey()
224
- }
225
- }
226
- }
227
- },
228
- // Determine if document is fully included within [start, end] range
239
+ // Only document-level metadata is used below. Bucket-data operations may
240
+ // be offloaded, and checksum queries must never inspect the ops array.
229
241
  {
230
242
  $project: {
231
243
  _id: 1,
232
244
  min_op: 1,
233
245
  checksum: 1,
234
246
  count: 1,
235
- ops: 1,
236
- bucket_start: 1,
237
- bucket_end: 1,
238
- is_fully_included: {
239
- $and: [{ $gt: ['$min_op', '$bucket_start'] }, { $lte: ['$_id.o', '$bucket_end'] }]
240
- }
241
- }
242
- },
243
- // Compute included checksum, count, and clear op detection
244
- {
245
- $project: {
246
- _id: 1,
247
- checksum_total: {
248
- $cond: {
249
- if: '$is_fully_included',
250
- then: '$checksum',
251
- else: {
252
- $sum: {
253
- $map: {
254
- input: {
255
- $filter: {
256
- input: '$ops',
257
- cond: {
258
- $and: [{ $gt: ['$$this.o', '$bucket_start'] }, { $lte: ['$$this.o', '$bucket_end'] }]
259
- }
260
- }
261
- },
262
- in: '$$this.checksum'
263
- }
264
- }
265
- }
266
- }
267
- },
268
- count_total: {
269
- $cond: {
270
- if: '$is_fully_included',
271
- then: '$count',
272
- else: {
273
- $size: {
274
- $filter: {
275
- input: '$ops',
276
- cond: {
277
- $and: [{ $gt: ['$$this.o', '$bucket_start'] }, { $lte: ['$$this.o', '$bucket_end'] }]
278
- }
279
- }
280
- }
281
- }
282
- }
283
- },
284
- has_clear_op: {
285
- $max: {
286
- $map: {
287
- input: {
288
- $filter: {
289
- input: '$ops',
290
- cond: {
291
- $and: [{ $gt: ['$$this.o', '$bucket_start'] }, { $lte: ['$$this.o', '$bucket_end'] }]
292
- }
293
- }
294
- },
295
- in: { $cond: [{ $eq: ['$$this.op', 'CLEAR'] }, 1, 0] }
296
- }
297
- }
298
- },
299
- last_op: { $max: '$_id.o' }
247
+ has_clear_op: 1
300
248
  }
301
249
  },
302
- // Group by bucket
250
+ // Aggregate document metadata per bucket. A CLEAR makes the result a full
251
+ // checksum that replaces any cached prefix; storage guarantees that
252
+ // documents preceding the CLEAR have already been removed.
303
253
  {
304
254
  $group: {
305
255
  _id: '$_id.b',
306
- checksum_total: { $sum: '$checksum_total' },
307
- count: { $sum: '$count_total' },
308
- has_clear_op: { $max: '$has_clear_op' },
309
- last_op: { $max: '$last_op' }
256
+ checksum_total: { $sum: '$checksum' },
257
+ count: { $sum: '$count' },
258
+ has_clear_op: {
259
+ $max: { $cond: ['$has_clear_op', 1, 0] }
260
+ },
261
+ min_op: { $min: '$min_op' },
262
+ max_op: { $max: '$_id.o' }
310
263
  }
311
- },
312
- // $sort results
313
- { $sort: { _id: 1 } }
264
+ }
314
265
  ];
315
266
  }
316
267
 
317
268
  private normalizePartialChecksumResults(
318
269
  batch: FetchPartialBucketChecksumByBucket[],
319
270
  aggregate: bson.Document[]
320
- ): PartialChecksumMap {
271
+ ): { checksums: PartialChecksumMap; startStraddledBuckets: Set<string> } {
272
+ const requests = new Map(batch.map((request) => [request.bucket, request]));
273
+ const startStraddledBuckets = new Set<string>();
321
274
  const partialChecksums = new Map<string, PartialOrFullChecksum>();
322
- for (let doc of aggregate) {
323
- const bucket = doc._id;
275
+
276
+ for (const doc of aggregate) {
277
+ const bucket = doc._id as string;
278
+ const request = requests.get(bucket)!;
279
+
280
+ // A maximum beyond end means a matched document straddles the checkpoint.
281
+ // Its document-level checksum includes later operations, so this checkpoint
282
+ // cannot be calculated from document metadata.
283
+ if (doc.max_op > request.end) {
284
+ throw new CheckpointChecksumInvalidatedError(request.end, bucket);
285
+ }
286
+ // The _id filter excludes documents ending at or before start. A minimum
287
+ // at or below start therefore means a matched document straddles it.
288
+ if (request.start != null && doc.min_op <= request.start) {
289
+ startStraddledBuckets.add(bucket);
290
+ continue;
291
+ }
292
+
324
293
  partialChecksums.set(bucket, checksumFromAggregate(doc));
325
294
  }
326
295
 
327
- return new Map<string, PartialOrFullChecksum>(
296
+ const checksums = new Map<string, PartialOrFullChecksum>(
328
297
  batch.map((request) => {
329
298
  const bucket = request.bucket;
330
299
  let partialChecksum = partialChecksums.get(bucket);
@@ -346,6 +315,7 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
346
315
  return [bucket, partialChecksum];
347
316
  })
348
317
  );
318
+ return { checksums, startStraddledBuckets };
349
319
  }
350
320
  }
351
321