@powersync/service-module-mongodb-storage 0.0.0-dev-20260803144812 → 0.0.0-dev-20260909133214

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +83 -7
  2. package/dist/storage/MongoBucketStorage.d.ts +31 -3
  3. package/dist/storage/MongoBucketStorage.js +365 -206
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +15 -4
  6. package/dist/storage/implementation/MongoBucketBatch.js +35 -9
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
  9. package/dist/storage/implementation/MongoCompactor.js +27 -238
  10. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  11. package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
  12. package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
  13. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
  15. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
  16. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  17. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
  18. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  19. package/dist/storage/implementation/MongoStorageProvider.js +3 -1
  20. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  21. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
  22. package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
  23. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  24. package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
  25. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  26. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
  27. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +7 -28
  28. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  29. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  30. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  31. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  32. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
  33. package/dist/storage/implementation/common/PersistedBatch.d.ts +27 -0
  34. package/dist/storage/implementation/common/PersistedBatch.js +51 -10
  35. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  36. package/dist/storage/implementation/db.d.ts +4 -1
  37. package/dist/storage/implementation/db.js +14 -1
  38. package/dist/storage/implementation/db.js.map +1 -1
  39. package/dist/storage/implementation/models.d.ts +35 -0
  40. package/dist/storage/implementation/models.js.map +1 -1
  41. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +6 -0
  42. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +80 -84
  43. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  44. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +36 -5
  45. package/dist/storage/implementation/v1/MongoCompactorV1.js +229 -8
  46. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  47. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  48. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  49. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  50. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  51. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
  52. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  53. package/dist/storage/implementation/v1/models.js +1 -0
  54. package/dist/storage/implementation/v1/models.js.map +1 -1
  55. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  56. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  57. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  58. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -1
  59. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +120 -39
  60. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  61. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +77 -25
  62. package/dist/storage/implementation/v3/MongoCompactorV3.js +585 -141
  63. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  64. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  65. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  66. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  67. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  68. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  69. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  70. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  71. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +92 -32
  72. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  73. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  74. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  75. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  76. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -0
  77. package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -14
  78. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  79. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  80. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  81. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  82. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  83. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  84. package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
  85. package/dist/storage/implementation/v3/compact-utils.js +216 -0
  86. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  87. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  88. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  89. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  90. package/dist/storage/implementation/v3/models.d.ts +89 -5
  91. package/dist/storage/implementation/v3/models.js.map +1 -1
  92. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  93. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -4
  94. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  95. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  96. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -5
  97. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +10 -2
  98. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  99. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
  100. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
  101. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  102. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  103. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +177 -23
  104. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  105. package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
  106. package/dist/storage/implementation/v3/source-table-utils.js +49 -57
  107. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  108. package/dist/storage/storage-index.d.ts +2 -1
  109. package/dist/storage/storage-index.js +1 -0
  110. package/dist/storage/storage-index.js.map +1 -1
  111. package/dist/types/types.d.ts +23 -1
  112. package/dist/types/types.js +16 -7
  113. package/dist/types/types.js.map +1 -1
  114. package/dist/utils/util.js +3 -1
  115. package/dist/utils/util.js.map +1 -1
  116. package/package.json +10 -9
  117. package/src/storage/MongoBucketStorage.ts +495 -268
  118. package/src/storage/implementation/MongoBucketBatch.ts +54 -14
  119. package/src/storage/implementation/MongoCompactor.ts +35 -346
  120. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  121. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
  122. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
  123. package/src/storage/implementation/MongoStorageProvider.ts +3 -1
  124. package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
  125. package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
  126. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +12 -44
  127. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  128. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  129. package/src/storage/implementation/common/PersistedBatch.ts +64 -12
  130. package/src/storage/implementation/db.ts +21 -2
  131. package/src/storage/implementation/models.ts +38 -0
  132. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +107 -96
  133. package/src/storage/implementation/v1/MongoCompactorV1.ts +303 -19
  134. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  135. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
  136. package/src/storage/implementation/v1/models.ts +1 -0
  137. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  138. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +159 -42
  139. package/src/storage/implementation/v3/MongoCompactorV3.ts +744 -190
  140. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  141. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  142. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +112 -42
  143. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  144. package/src/storage/implementation/v3/PersistedBatchV3.ts +58 -15
  145. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  146. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  147. package/src/storage/implementation/v3/compact-utils.ts +320 -0
  148. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  149. package/src/storage/implementation/v3/models.ts +96 -3
  150. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +16 -8
  151. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  152. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +15 -4
  153. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
  154. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +257 -27
  155. package/src/storage/implementation/v3/source-table-utils.ts +82 -76
  156. package/src/storage/storage-index.ts +2 -0
  157. package/src/types/types.ts +24 -7
  158. package/src/utils/util.ts +3 -1
  159. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  160. package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
  161. package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
  162. package/test/src/compact-utils.test.ts +226 -0
  163. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  164. package/test/src/object_storage_usage.test.ts +255 -0
  165. package/test/src/parameter_compacting_v1.test.ts +120 -0
  166. package/test/src/parameter_compacting_v3.test.ts +192 -0
  167. package/test/src/parameter_compaction_fence.test.ts +276 -0
  168. package/test/src/storage.test.ts +273 -22
  169. package/test/src/storage_compacting.test.ts +714 -244
  170. package/test/src/storage_s3_checksums.test.ts +3 -4
  171. package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
  172. package/test/src/storage_s3_reading.test.ts +243 -1
  173. package/test/src/storage_s3_writing.test.ts +1 -0
  174. package/test/src/storage_sync.test.ts +365 -41
  175. package/test/src/util.ts +1 -1
  176. package/tsconfig.tsbuildinfo +1 -1
@@ -65,6 +65,11 @@ export interface PersistedBatchOptions {
65
65
  logger?: Logger;
66
66
  objectStorage?: ObjectStorage;
67
67
  inlineThresholdBytes?: number;
68
+ /**
69
+ * Aborts in-flight object storage uploads when replication stops.
70
+ */
71
+ signal?: AbortSignal;
72
+ objectStorageUsageWriterId?: string;
68
73
  }
69
74
 
70
75
  /**
@@ -81,6 +86,8 @@ export abstract class PersistedBatch {
81
86
 
82
87
  protected readonly objectStorage?: ObjectStorage;
83
88
  protected readonly inlineThresholdBytes: number = DEFAULT_INLINE_THRESHOLD_BYTES;
89
+ protected readonly signal?: AbortSignal;
90
+ protected readonly objectStorageUsageWriterId?: string;
84
91
 
85
92
  /**
86
93
  * For debug logging only.
@@ -102,6 +109,8 @@ export abstract class PersistedBatch {
102
109
  this.currentSize = writtenSize;
103
110
  this.logger = options?.logger ?? defaultLogger;
104
111
  this.objectStorage = options?.objectStorage;
112
+ this.signal = options?.signal;
113
+ this.objectStorageUsageWriterId = options?.objectStorageUsageWriterId;
105
114
  if (options?.inlineThresholdBytes != null) {
106
115
  this.inlineThresholdBytes = options.inlineThresholdBytes;
107
116
  }
@@ -124,7 +133,8 @@ export abstract class PersistedBatch {
124
133
  remaining_buckets.set(currentBucketKey(mapped), mapped);
125
134
  }
126
135
 
127
- const dchecksum = BigInt(utils.hashDelete(replicaIdToSubkey(options.table.id, options.sourceKey)));
136
+ const subkey = this.persistedSubkey(options.table.id, options.sourceKey);
137
+ const dchecksum = BigInt(utils.hashDelete(subkey));
128
138
 
129
139
  for (const evaluated of options.evaluated) {
130
140
  const definitionId = this.getBucketDefinitionId(evaluated.source);
@@ -159,6 +169,7 @@ export abstract class PersistedBatch {
159
169
  bucket: evaluated.bucket,
160
170
  sourceTableId: options.table.id,
161
171
  sourceKey: options.sourceKey,
172
+ subkey,
162
173
  table: evaluated.table,
163
174
  rowId: evaluated.id,
164
175
  checksum: BigInt(checksum),
@@ -181,6 +192,7 @@ export abstract class PersistedBatch {
181
192
  op_id,
182
193
  sourceTableId: options.table.id,
183
194
  sourceKey: options.sourceKey,
195
+ subkey,
184
196
  table: bucket.table,
185
197
  rowId: bucket.id,
186
198
  checksum: dchecksum
@@ -217,6 +229,14 @@ export abstract class PersistedBatch {
217
229
  protected abstract checkDefinitionId(definitionId: BucketDefinitionId | null): BucketDefinitionId;
218
230
  protected abstract getBucketDefinitionId(bucketSource: BucketDataSource): BucketDefinitionId;
219
231
 
232
+ /**
233
+ * New bucket operations persist protocol subkeys during replication. Older
234
+ * V1 documents may not have one, so sync reads retain a fallback.
235
+ */
236
+ protected persistedSubkey(table: storage.SourceTableId, key: storage.ReplicaId): string {
237
+ return replicaIdToSubkey(table, key);
238
+ }
239
+
220
240
  protected get bucketDataCount(): number {
221
241
  return this.bucketData.length;
222
242
  }
@@ -234,17 +254,45 @@ export abstract class PersistedBatch {
234
254
  bucket,
235
255
  lastOp: op_id,
236
256
  incrementCount: 1,
237
- incrementBytes: bytes
257
+ incrementBytes: bytes,
258
+ incrementChunks: 0
238
259
  });
239
260
  }
240
261
  }
241
262
 
263
+ /**
264
+ * V3 persists operations in chunks. Keep this separate from incrementBucket:
265
+ * operation counts are known while evaluating rows, while chunk counts and
266
+ * exact persisted bytes are only known after the writer has chunked a flush.
267
+ */
268
+ protected incrementBucketPersistedChunk(definitionId: BucketDefinitionId, bucket: string, bytes: number) {
269
+ const key = `${definitionId ?? ''}:${bucket}`;
270
+ const existingState = this.bucketStates.get(key);
271
+ if (existingState != null) {
272
+ existingState.incrementChunks += 1;
273
+ existingState.incrementBytes += bytes;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * V3's compactor calculates byte deltas from persisted chunk metadata.
279
+ * Replace the writer's per-operation estimate with those exact sizes before
280
+ * flushing the corresponding bucket state.
281
+ */
282
+ protected resetBucketPersistedBytes(definitionId: BucketDefinitionId, bucket: string) {
283
+ const state = this.bucketStates.get(`${definitionId ?? ''}:${bucket}`);
284
+ if (state != null) {
285
+ state.incrementBytes = 0;
286
+ }
287
+ }
288
+
242
289
  protected addBucketDataPut(options: {
243
290
  op_id: InternalOpId;
244
291
  bucketKey: BucketKey;
245
292
  bucket: string;
246
293
  sourceTableId: storage.SourceTable['id'];
247
294
  sourceKey: storage.ReplicaId;
295
+ subkey: string;
248
296
  table: string;
249
297
  rowId: string;
250
298
  checksum: bigint;
@@ -256,6 +304,7 @@ export abstract class PersistedBatch {
256
304
  op: 'PUT',
257
305
  source_table: mongoTableId(options.sourceTableId),
258
306
  source_key: options.sourceKey,
307
+ subkey: options.subkey,
259
308
  table: options.table,
260
309
  row_id: options.rowId,
261
310
  checksum: options.checksum,
@@ -268,6 +317,7 @@ export abstract class PersistedBatch {
268
317
  bucketKey: BucketKey;
269
318
  sourceTableId: storage.SourceTable['id'];
270
319
  sourceKey: storage.ReplicaId;
320
+ subkey: string;
271
321
  table: string;
272
322
  rowId: string;
273
323
  checksum: bigint;
@@ -278,6 +328,7 @@ export abstract class PersistedBatch {
278
328
  op: 'REMOVE',
279
329
  source_table: mongoTableId(options.sourceTableId),
280
330
  source_key: options.sourceKey,
331
+ subkey: options.subkey,
281
332
  table: options.table,
282
333
  row_id: options.rowId,
283
334
  checksum: options.checksum,
@@ -321,32 +372,32 @@ export abstract class PersistedBatch {
321
372
  const replicationLag = Math.round((Date.now() - options.oldestUncommittedChange.getTime()) / 1000);
322
373
 
323
374
  this.logger.info(
324
- `Flushed ${this.bucketDataCount} + ${this.bucketParameters.length} + ${
375
+ `Flushed: ${this.bucketDataCount} ops, ${this.bucketParameters.length} index entries, ${
325
376
  this.currentDataCount
326
- } updates, ${Math.round(this.currentSize / 1024)}kb in ${duration}ms. Last op_id: ${this.debugLastOpId}. Replication lag: ${replicationLag}s`,
377
+ } records. ${Math.round(this.currentSize / 1024)}kb in ${duration}ms. Last op_id: ${this.debugLastOpId}. Replication lag: ${replicationLag}s`,
327
378
  {
328
379
  flushed: {
329
380
  duration: duration,
330
381
  size: this.currentSize,
331
- bucket_data_count: this.bucketDataCount,
332
- parameter_data_count: this.bucketParameters.length,
333
- current_data_count: this.currentDataCount,
382
+ bucket_ops_count: this.bucketDataCount,
383
+ parameter_indexes_count: this.bucketParameters.length,
384
+ source_records_count: this.currentDataCount,
334
385
  replication_lag_seconds: replicationLag
335
386
  }
336
387
  }
337
388
  );
338
389
  } else {
339
390
  this.logger.info(
340
- `Flushed ${this.bucketDataCount} + ${this.bucketParameters.length} + ${
391
+ `Flushed: ${this.bucketDataCount} ops, ${this.bucketParameters.length} index entries, ${
341
392
  this.currentDataCount
342
- } updates, ${Math.round(this.currentSize / 1024)}kb in ${duration}ms. Last op_id: ${this.debugLastOpId}`,
393
+ } records. ${Math.round(this.currentSize / 1024)}kb in ${duration}ms. Last op_id: ${this.debugLastOpId}`,
343
394
  {
344
395
  flushed: {
345
396
  duration: duration,
346
397
  size: this.currentSize,
347
- bucket_data_count: this.bucketDataCount,
348
- parameter_data_count: this.bucketParameters.length,
349
- current_data_count: this.currentDataCount
398
+ bucket_ops_count: this.bucketDataCount,
399
+ parameter_indexes_count: this.bucketParameters.length,
400
+ source_records_count: this.currentDataCount
350
401
  }
351
402
  }
352
403
  );
@@ -377,4 +428,5 @@ export interface BucketStateUpdate {
377
428
  lastOp: InternalOpId;
378
429
  incrementCount: number;
379
430
  incrementBytes: number;
431
+ incrementChunks: number;
380
432
  }
@@ -24,8 +24,9 @@ import {
24
24
  CurrentDataDocument
25
25
  } from './v1/models.js';
26
26
  import { VersionedPowerSyncMongoV1 } from './v1/VersionedPowerSyncMongoV1.js';
27
- import { BucketDataDocumentV3 } from './v3/models.js';
28
- import { VersionedPowerSyncMongoV3 } from './v3/VersionedPowerSyncMongoV3.js';
27
+ import { BucketDataDocumentV3, CustomCheckpointRequestDocumentV3 } from './v3/models.js';
28
+ import type { CustomCheckpointRequestCollectionSpecifier } from './v3/VersionedPowerSyncMongoV3.js';
29
+ import { OBJECT_STORAGE_USAGE_COLLECTION, VersionedPowerSyncMongoV3 } from './v3/VersionedPowerSyncMongoV3.js';
29
30
 
30
31
  export interface PowerSyncMongoOptions {
31
32
  /**
@@ -137,6 +138,20 @@ export class PowerSyncMongo {
137
138
  return `source_table_${replicationStreamId}`;
138
139
  }
139
140
 
141
+ customCheckpointRequestCollectionName(options: CustomCheckpointRequestCollectionSpecifier) {
142
+ return `custom_checkpoint_requests_${options.replicationStreamId}_${options.eventId}`;
143
+ }
144
+
145
+ async listCustomCheckpointRequestCollections(
146
+ replicationStreamId?: number
147
+ ): Promise<mongo.Collection<CustomCheckpointRequestDocumentV3>[]> {
148
+ const prefix =
149
+ replicationStreamId == null
150
+ ? 'custom_checkpoint_requests_'
151
+ : `custom_checkpoint_requests_${replicationStreamId}_`;
152
+ return this.collectionsByPrefix(prefix);
153
+ }
154
+
140
155
  async listSourceTableCollections(
141
156
  replicationStreamId?: number
142
157
  ): Promise<mongo.Collection<CommonSourceTableDocument>[]> {
@@ -178,6 +193,10 @@ export class PowerSyncMongo {
178
193
  await this.locks.deleteMany({});
179
194
  await this.bucket_state.deleteMany({});
180
195
  await this.custom_write_checkpoints.deleteMany({});
196
+ await this.db.collection(OBJECT_STORAGE_USAGE_COLLECTION).deleteMany({});
197
+ for (const collection of await this.listCustomCheckpointRequestCollections()) {
198
+ await collection.drop();
199
+ }
181
200
  }
182
201
 
183
202
  /**
@@ -74,6 +74,12 @@ export interface BucketDataProperties {
74
74
  op: OpType;
75
75
  source_table?: bson.ObjectId;
76
76
  source_key?: ReplicaId;
77
+ /**
78
+ * Stable identifier for the source record in the client protocol.
79
+ *
80
+ * This is derived from (source_table, source_key), so persistence is optional but preferred.
81
+ */
82
+ subkey?: string;
77
83
  table?: string;
78
84
  row_id?: string;
79
85
  checksum: bigint;
@@ -110,6 +116,10 @@ export interface SourceTableDocument {
110
116
  replica_id_columns2: { name: string; type_oid?: number; type?: string }[] | undefined;
111
117
  snapshot_done: boolean | undefined;
112
118
  snapshot_status: SourceTableDocumentSnapshotStatus | undefined;
119
+ /**
120
+ * Source-specific metadata. Absent for legacy records.
121
+ */
122
+ source_metadata?: storage.JsonValue;
113
123
  }
114
124
 
115
125
  export interface SourceTableDocumentSnapshotStatus {
@@ -211,6 +221,34 @@ export interface SyncRuleDocumentBase {
211
221
  } | null;
212
222
 
213
223
  storage_version?: number;
224
+
225
+ /** Optional operator-supplied label identifying the sync config version. */
226
+ version_label?: string | null;
227
+
228
+ /**
229
+ * Incremental parameter compaction state for the replication stream.
230
+ *
231
+ * Operation ids are allocated across all parameter indexes of a stream, so this is
232
+ * stream-level state, shared by all sync configs of the stream.
233
+ */
234
+ parameter_compaction?: {
235
+ /**
236
+ * The exclusive operation-id boundary through which every parameter index in this stream has
237
+ * been compacted.
238
+ */
239
+ compacted_before: InternalOpId;
240
+
241
+ /**
242
+ * Parameter entries below this boundary may no longer be available for checkpoint change
243
+ * detection, since compaction may have deleted them.
244
+ *
245
+ * This is advanced before the first delete of a compaction pass, while
246
+ * {@link compacted_before} is only advanced after every delete of the pass completed.
247
+ * A checkpoint transition starting below this boundary must conservatively invalidate all
248
+ * parameter buckets, since the individual changes may no longer be available.
249
+ */
250
+ checkpoint_changes_invalid_before?: InternalOpId;
251
+ };
214
252
  }
215
253
 
216
254
  export interface SyncRuleCheckpointFields {
@@ -1,5 +1,7 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
1
2
  import { ReplicationAssertionError } from '@powersync/lib-services-framework';
2
3
  import { ColumnDescriptor, InternalOpId, SourceTable, storage } from '@powersync/service-core';
4
+ import { HydratedSyncConfig } from '@powersync/service-sync-rules';
3
5
  import * as bson from 'bson';
4
6
  import { mongoTableId } from '../../../utils/util.js';
5
7
  import { calculateCheckpointState } from '../CheckpointState.js';
@@ -9,7 +11,7 @@ import { SourceRecordStore } from '../common/SourceRecordStore.js';
9
11
  import { PersistedBatchV1 } from './PersistedBatchV1.js';
10
12
  import { SourceRecordStoreV1 } from './SourceRecordStoreV1.js';
11
13
  import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
12
- import { SyncRuleDocumentV1 } from './models.js';
14
+ import { SourceTableDocumentV1, SyncRuleDocumentV1 } from './models.js';
13
15
 
14
16
  export class MongoBucketBatchV1 extends MongoBucketBatch {
15
17
  declare readonly db: VersionedPowerSyncMongoV1;
@@ -49,8 +51,39 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
49
51
  // No-op for V1: source records live in a shared collection.
50
52
  }
51
53
 
54
+ protected override async batchCreateCustomWriteCheckpoints(
55
+ session: mongo.ClientSession,
56
+ opId: InternalOpId
57
+ ): Promise<void> {
58
+ await this.db.custom_write_checkpoints.bulkWrite(
59
+ this.write_checkpoint_batch.map((checkpointOptions) => {
60
+ const set: Record<string, unknown> = {
61
+ checkpoint: checkpointOptions.checkpoint,
62
+ sync_rules_id: checkpointOptions.sync_rules_id,
63
+ op_id: opId
64
+ };
65
+ if (checkpointOptions.checkpoint_requested_at != null) {
66
+ set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
67
+ }
68
+
69
+ return {
70
+ updateOne: {
71
+ filter: { user_id: checkpointOptions.user_id, sync_rules_id: checkpointOptions.sync_rules_id },
72
+ update: {
73
+ $set: set,
74
+ ...(checkpointOptions.checkpoint_requested_at == null ? { $unset: { checkpoint_requested_at: 1 } } : {})
75
+ },
76
+ upsert: true
77
+ }
78
+ };
79
+ }),
80
+ { session }
81
+ );
82
+ }
83
+
52
84
  async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
53
85
  const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
86
+ const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
54
87
  const { connection_id, source } = options;
55
88
  const { schema, name, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
56
89
 
@@ -63,20 +96,38 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
63
96
  let result: storage.ResolveTablesResult | null = null;
64
97
  await this.db.client.withSession(async (session) => {
65
98
  const col = this.db.sourceTablesV1(this.replicationStreamId);
66
- const filter: any = {
67
- group_id: this.replicationStreamId,
68
- connection_id,
69
- schema_name: schema,
70
- table_name: name,
71
- replica_id_columns2: normalizedReplicaIdColumns
72
- };
99
+
100
+ // Find records that overlap by name or relation id.
101
+ const orClauses: Record<string, unknown>[] = [{ schema_name: schema, table_name: name }];
73
102
  if (objectId != null) {
74
- filter.relation_id = objectId;
103
+ orClauses.push({ relation_id: objectId });
75
104
  }
105
+ const candidateDocs = await col
106
+ .find({ group_id: this.replicationStreamId, connection_id, $or: orClauses }, { session })
107
+ .toArray();
76
108
 
77
- let doc = await col.findOne(filter, { session });
78
- if (doc == null) {
79
- doc = {
109
+ const candidateTables = candidateDocs.map((doc) =>
110
+ this.hydrateSourceTable(doc, connectionTag, syncRules, sendsCompleteRows)
111
+ );
112
+ const candidates = candidateTables.map((table) => table.clone());
113
+ const resolution = await reconcile({ source, candidates });
114
+ storage.validateSourceTableCandidateResolution(candidates, resolution);
115
+
116
+ for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
117
+ await col.updateOne({ _id: mongoTableId(id) }, { $set: { source_metadata: sourceMetadata } }, { session });
118
+ }
119
+
120
+ const materializedResolution = storage.materializeSourceTableResolution(candidateTables, resolution);
121
+ const compatibleTables = materializedResolution.compatibleTables;
122
+
123
+ // V1 keeps one record, preferring one that has already been snapshotted.
124
+ const reuse = compatibleTables.find((table) => table.snapshotComplete) ?? compatibleTables[0] ?? null;
125
+
126
+ let sourceTable: storage.SourceTable;
127
+ if (reuse != null) {
128
+ sourceTable = reuse;
129
+ } else {
130
+ const doc: SourceTableDocumentV1 = {
80
131
  _id: options.idGenerator ? (options.idGenerator() as bson.ObjectId) : new bson.ObjectId(),
81
132
  group_id: this.replicationStreamId,
82
133
  connection_id,
@@ -86,69 +137,18 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
86
137
  replica_id_columns: null,
87
138
  replica_id_columns2: normalizedReplicaIdColumns,
88
139
  snapshot_done: false,
89
- snapshot_status: undefined
140
+ snapshot_status: undefined,
141
+ source_metadata: resolution.newTableValues.sourceMetadata
90
142
  };
91
143
  await col.insertOne(doc, { session });
144
+ sourceTable = this.hydrateSourceTable(doc, connectionTag, syncRules, sendsCompleteRows);
92
145
  }
93
146
 
94
- const sourceTable = new storage.SourceTable({
95
- id: doc._id,
96
- ref: source,
97
- objectId,
98
- replicaIdColumns,
99
- snapshotComplete: doc.snapshot_done ?? true,
100
- ...syncRules.getMatchingSources(source)
101
- });
102
- sourceTable.syncEvent = syncRules.tableTriggersEvent(source);
103
- sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
104
- sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
105
- sourceTable.storeCurrentData = sendsCompleteRows !== true;
106
- sourceTable.snapshotStatus =
107
- doc.snapshot_status == null
108
- ? undefined
109
- : {
110
- lastKey: doc.snapshot_status.last_key?.buffer ?? null,
111
- totalEstimatedCount: doc.snapshot_status.total_estimated_count,
112
- replicatedCount: doc.snapshot_status.replicated_count
113
- };
114
-
115
- const truncateFilter = [{ schema_name: schema, table_name: name }] as any[];
116
- if (objectId != null) {
117
- truncateFilter.push({ relation_id: objectId });
118
- }
119
- const truncate = await col
120
- .find(
121
- {
122
- group_id: this.replicationStreamId,
123
- connection_id,
124
- _id: { $ne: doc._id },
125
- $or: truncateFilter
126
- },
127
- { session }
128
- )
129
- .toArray();
130
- const dropTables = truncate.map((dropDoc) => {
131
- const ref = {
132
- connectionTag,
133
- schema: dropDoc.schema_name,
134
- name: dropDoc.table_name
135
- };
136
- const table = new storage.SourceTable({
137
- id: dropDoc._id,
138
- ref,
139
- objectId: dropDoc.relation_id,
140
- replicaIdColumns:
141
- dropDoc.replica_id_columns2?.map(
142
- (c) => ({ name: c.name, typeId: c.type_oid, type: c.type }) satisfies ColumnDescriptor
143
- ) ?? [],
144
- snapshotComplete: dropDoc.snapshot_done ?? true,
145
- ...syncRules.getMatchingSources(ref)
146
- });
147
- table.syncEvent = syncRules.tableTriggersEvent(ref);
148
- table.syncData = table.bucketDataSources.length > 0;
149
- table.syncParameters = table.parameterLookupSources.length > 0;
150
- return table;
151
- });
147
+ const dropTables = [
148
+ ...materializedResolution.incompatibleTables,
149
+ // V1 only keeps one SourceTable per physical table.
150
+ ...compatibleTables.filter((table) => !storage.sourceTableIdEquals(table.id, sourceTable.id))
151
+ ];
152
152
 
153
153
  result = { tables: [sourceTable], dropTables };
154
154
  });
@@ -156,38 +156,33 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
156
156
  return result!;
157
157
  }
158
158
 
159
- async getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null> {
160
- const doc = await this.db.sourceTablesV1(this.replicationStreamId).findOne(
161
- {
162
- group_id: this.replicationStreamId,
163
- _id: mongoTableId(table.id)
164
- },
165
- { session: this.session }
166
- );
167
- if (doc == null) {
168
- return null;
169
- }
170
-
171
- const ref = {
172
- connectionTag: table.ref.connectionTag,
173
- schema: doc.schema_name,
174
- name: doc.table_name
175
- };
176
- const sourceTable = new storage.SourceTable({
159
+ /**
160
+ * Build a SourceTable from its persisted v1 record.
161
+ */
162
+ private hydrateSourceTable(
163
+ doc: SourceTableDocumentV1,
164
+ connectionTag: string,
165
+ syncRules: HydratedSyncConfig,
166
+ sendsCompleteRows: boolean | undefined
167
+ ): storage.SourceTable {
168
+ const ref = { connectionTag, schema: doc.schema_name, name: doc.table_name };
169
+ const table = new storage.SourceTable({
177
170
  id: doc._id,
178
171
  ref,
179
172
  objectId: doc.relation_id,
180
173
  replicaIdColumns:
181
174
  doc.replica_id_columns2?.map(
182
- (c) => ({ name: c.name, typeId: c.type_oid, type: c.type }) satisfies ColumnDescriptor
175
+ (column) => ({ name: column.name, typeId: column.type_oid, type: column.type }) satisfies ColumnDescriptor
183
176
  ) ?? [],
184
177
  snapshotComplete: doc.snapshot_done ?? true,
185
- ...this.sync_rules.getMatchingSources(ref)
178
+ sourceMetadata: doc.source_metadata ?? null,
179
+ ...syncRules.getMatchingSources(ref)
186
180
  });
187
- sourceTable.syncEvent = this.sync_rules.tableTriggersEvent(ref);
188
- sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
189
- sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
190
- sourceTable.snapshotStatus =
181
+ table.syncEvent = syncRules.tableTriggersEvent(ref);
182
+ table.syncData = table.bucketDataSources.length > 0;
183
+ table.syncParameters = table.parameterLookupSources.length > 0;
184
+ table.storeCurrentData = sendsCompleteRows !== true;
185
+ table.snapshotStatus =
191
186
  doc.snapshot_status == null
192
187
  ? undefined
193
188
  : {
@@ -195,7 +190,23 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
195
190
  totalEstimatedCount: doc.snapshot_status.total_estimated_count,
196
191
  replicatedCount: doc.snapshot_status.replicated_count
197
192
  };
198
- return sourceTable;
193
+ return table;
194
+ }
195
+
196
+ async getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null> {
197
+ const doc = await this.db.sourceTablesV1(this.replicationStreamId).findOne(
198
+ {
199
+ group_id: this.replicationStreamId,
200
+ _id: mongoTableId(table.id)
201
+ },
202
+ { session: this.session }
203
+ );
204
+ if (doc == null) {
205
+ return null;
206
+ }
207
+
208
+ // Use the default storeCurrentData value when reading outside resolution.
209
+ return this.hydrateSourceTable(doc, table.ref.connectionTag, this.sync_rules, undefined);
199
210
  }
200
211
 
201
212
  async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {