@powersync/service-module-mongodb-storage 0.0.0-dev-20260602073133 → 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 (275) hide show
  1. package/CHANGELOG.md +177 -5
  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 +71 -18
  6. package/dist/storage/MongoBucketStorage.js +516 -202
  7. package/dist/storage/MongoBucketStorage.js.map +1 -1
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +71 -29
  9. package/dist/storage/implementation/MongoBucketBatch.js +140 -66
  10. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  11. package/dist/storage/implementation/MongoChecksums.d.ts +41 -13
  12. package/dist/storage/implementation/MongoChecksums.js +36 -121
  13. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  14. package/dist/storage/implementation/MongoCompactor.d.ts +28 -90
  15. package/dist/storage/implementation/MongoCompactor.js +87 -481
  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 +82 -8
  21. package/dist/storage/implementation/MongoParameterCompactor.js +312 -76
  22. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  23. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +21 -0
  24. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +93 -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 +125 -0
  31. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  32. package/dist/storage/implementation/MongoStorageProvider.js +41 -4
  33. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +81 -37
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js +159 -86
  36. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  37. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +1 -1
  38. package/dist/storage/implementation/MongoSyncRulesLock.js +23 -11
  39. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  40. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +11 -8
  41. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +159 -33
  42. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  43. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  44. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  45. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +28 -0
  47. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  48. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  49. package/dist/storage/implementation/common/PersistedBatch.d.ts +34 -2
  50. package/dist/storage/implementation/common/PersistedBatch.js +63 -10
  51. package/dist/storage/implementation/common/PersistedBatch.js.map +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 +4 -1
  58. package/dist/storage/implementation/db.js +31 -16
  59. package/dist/storage/implementation/db.js.map +1 -1
  60. package/dist/storage/implementation/models.d.ts +55 -13
  61. package/dist/storage/implementation/models.js.map +1 -1
  62. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +15 -1
  63. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +135 -107
  64. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  65. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +4 -4
  66. package/dist/storage/implementation/v1/MongoChecksumsV1.js +119 -7
  67. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  68. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +47 -7
  69. package/dist/storage/implementation/v1/MongoCompactorV1.js +478 -8
  70. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  71. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  72. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  73. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  74. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +33 -27
  75. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +146 -73
  76. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  77. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
  78. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
  79. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
  80. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  81. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  82. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  83. package/dist/storage/implementation/v1/models.d.ts +10 -1
  84. package/dist/storage/implementation/v1/models.js +1 -0
  85. package/dist/storage/implementation/v1/models.js.map +1 -1
  86. package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
  87. package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
  88. package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
  89. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  90. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  91. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  92. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +16 -5
  93. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +393 -262
  94. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  95. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +30 -9
  96. package/dist/storage/implementation/v3/MongoChecksumsV3.js +161 -20
  97. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  98. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +108 -18
  99. package/dist/storage/implementation/v3/MongoCompactorV3.js +1115 -47
  100. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  101. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +12 -3
  102. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +13 -8
  103. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -1
  104. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +54 -0
  105. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +319 -0
  106. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  107. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +61 -36
  108. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +353 -161
  109. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  110. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  111. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  112. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  113. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +8 -3
  114. package/dist/storage/implementation/v3/PersistedBatchV3.js +131 -29
  115. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  116. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  117. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  118. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  119. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +26 -14
  120. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +73 -29
  121. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  122. package/dist/storage/implementation/v3/bucket-format.d.ts +13 -0
  123. package/dist/storage/implementation/v3/bucket-format.js +79 -0
  124. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  125. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  126. package/dist/storage/implementation/v3/chunking.js +30 -0
  127. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  128. package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
  129. package/dist/storage/implementation/v3/compact-utils.js +216 -0
  130. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  131. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  132. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  133. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  134. package/dist/storage/implementation/v3/models.d.ts +187 -23
  135. package/dist/storage/implementation/v3/models.js +11 -28
  136. package/dist/storage/implementation/v3/models.js.map +1 -1
  137. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +20 -0
  138. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +48 -0
  139. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
  140. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +33 -0
  141. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
  142. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
  143. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +68 -0
  144. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +154 -0
  145. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
  146. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
  147. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
  148. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  149. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +67 -0
  150. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +439 -0
  151. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
  152. package/dist/storage/implementation/v3/source-table-utils.d.ts +63 -0
  153. package/dist/storage/implementation/v3/source-table-utils.js +206 -0
  154. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  155. package/dist/storage/storage-index.d.ts +5 -4
  156. package/dist/storage/storage-index.js +5 -4
  157. package/dist/storage/storage-index.js.map +1 -1
  158. package/dist/types/types.d.ts +71 -3
  159. package/dist/types/types.js +58 -2
  160. package/dist/types/types.js.map +1 -1
  161. package/dist/utils/test-utils.d.ts +1 -2
  162. package/dist/utils/test-utils.js +7 -1
  163. package/dist/utils/test-utils.js.map +1 -1
  164. package/dist/utils/util.d.ts +9 -2
  165. package/dist/utils/util.js +131 -5
  166. package/dist/utils/util.js.map +1 -1
  167. package/package.json +13 -19
  168. package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
  169. package/src/storage/MongoBucketStorage.ts +728 -242
  170. package/src/storage/implementation/MongoBucketBatch.ts +196 -91
  171. package/src/storage/implementation/MongoChecksums.ts +93 -153
  172. package/src/storage/implementation/MongoCompactor.ts +106 -631
  173. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  174. package/src/storage/implementation/MongoParameterCompactor.ts +392 -87
  175. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +122 -0
  176. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  177. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +156 -0
  178. package/src/storage/implementation/MongoStorageProvider.ts +48 -4
  179. package/src/storage/implementation/MongoSyncBucketStorage.ts +226 -118
  180. package/src/storage/implementation/MongoSyncRulesLock.ts +28 -12
  181. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +194 -52
  182. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  183. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +31 -0
  184. package/src/storage/implementation/common/PersistedBatch.ts +83 -15
  185. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  186. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  187. package/src/storage/implementation/db.ts +40 -22
  188. package/src/storage/implementation/models.ts +58 -14
  189. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +174 -125
  190. package/src/storage/implementation/v1/MongoChecksumsV1.ts +154 -12
  191. package/src/storage/implementation/v1/MongoCompactorV1.ts +594 -25
  192. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  193. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +226 -129
  194. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
  195. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  196. package/src/storage/implementation/v1/models.ts +12 -1
  197. package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
  198. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  199. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +502 -324
  200. package/src/storage/implementation/v3/MongoChecksumsV3.ts +250 -32
  201. package/src/storage/implementation/v3/MongoCompactorV3.ts +1439 -70
  202. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +19 -9
  203. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +473 -0
  204. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +453 -211
  205. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  206. package/src/storage/implementation/v3/PersistedBatchV3.ts +169 -45
  207. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  208. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +99 -38
  209. package/src/storage/implementation/v3/bucket-format.ts +99 -0
  210. package/src/storage/implementation/v3/chunking.ts +40 -0
  211. package/src/storage/implementation/v3/compact-utils.ts +320 -0
  212. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  213. package/src/storage/implementation/v3/models.ts +228 -58
  214. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +62 -0
  215. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +44 -0
  216. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +182 -0
  217. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
  218. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +573 -0
  219. package/src/storage/implementation/v3/source-table-utils.ts +398 -0
  220. package/src/storage/storage-index.ts +14 -4
  221. package/src/types/types.ts +76 -2
  222. package/src/utils/test-utils.ts +8 -3
  223. package/src/utils/util.ts +188 -7
  224. package/test/src/__snapshots__/storage.test.ts.snap +4 -54
  225. package/test/src/__snapshots__/storage_sync.test.ts.snap +652 -645
  226. package/test/src/cleanup-stopped-sync-configs.test.ts +698 -0
  227. package/test/src/compact-utils.test.ts +226 -0
  228. package/test/src/helpers/MemoryObjectStorage.ts +61 -0
  229. package/test/src/helpers/s3TestFactory.ts +69 -0
  230. package/test/src/object_storage_lifecycle.test.ts +59 -0
  231. package/test/src/object_storage_usage.test.ts +255 -0
  232. package/test/src/parameter_compacting_v1.test.ts +120 -0
  233. package/test/src/parameter_compacting_v3.test.ts +192 -0
  234. package/test/src/parameter_compaction_fence.test.ts +276 -0
  235. package/test/src/setup.ts +6 -1
  236. package/test/src/storage.test.ts +478 -13
  237. package/test/src/storage_compacting.test.ts +2516 -107
  238. package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
  239. package/test/src/storage_s3_checksums.test.ts +190 -0
  240. package/test/src/storage_s3_compaction_lifecycle.test.ts +543 -0
  241. package/test/src/storage_s3_reading.test.ts +768 -0
  242. package/test/src/storage_s3_writing.test.ts +136 -0
  243. package/test/src/storage_sync.test.ts +1507 -76
  244. package/test/src/storeCurrentData.test.ts +227 -0
  245. package/test/src/util.ts +3 -2
  246. package/tsconfig.json +1 -4
  247. package/tsconfig.tsbuildinfo +1 -1
  248. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -15
  249. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  250. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  251. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  252. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -67
  253. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  254. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -29
  255. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -76
  256. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  257. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  258. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  259. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  260. package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
  261. package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
  262. package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
  263. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -4
  264. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  265. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  266. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
  267. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -48
  268. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
  269. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -75
  270. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -82
  271. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -101
  272. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  273. package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
  274. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
  275. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -68
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { logger } from '@powersync/lib-services-framework';
2
+ import { logger as defaultLogger, Logger } from '@powersync/lib-services-framework';
3
3
  import { bson, CompactOptions, InternalOpId } from '@powersync/service-core';
4
4
  import { LRUCache } from 'lru-cache';
5
5
  import type { VersionedPowerSyncMongo } from './db.js';
@@ -7,125 +7,430 @@ import type { VersionedPowerSyncMongo } from './db.js';
7
7
  type ParameterCompactionReadDocument = {
8
8
  _id: InternalOpId;
9
9
  key: mongo.Document;
10
- lookup: unknown;
10
+ lookup: bson.Binary;
11
11
  bucket_parameters?: unknown[] | null;
12
12
  };
13
13
 
14
+ export type ParameterCompactionResult = {
15
+ collections: number;
16
+ scannedEntries: number;
17
+ deletedEntries: number;
18
+ };
19
+
20
+ const PARAMETER_COMPACTION_BATCH_SIZE = 10_000;
21
+ const PARAMETER_COMPACTION_DELETE_BATCH_SIZE = 1_000;
22
+ const PARAMETER_COMPACTION_CACHE_SIZE = 50_000;
23
+ /**
24
+ * How often progress is persisted during a pass.
25
+ *
26
+ * Kept coarse: replication also updates the `sync_rules` document on every commit.
27
+ */
28
+ const PARAMETER_COMPACTION_PERSIST_INTERVAL_MS = 60_000;
29
+
30
+ type CachedIdentity = {
31
+ /**
32
+ * The `_id` of the document retained for this identity in a previous batch, or null if that
33
+ * document was a tombstone - in which case it has been deleted along with all its history, and
34
+ * nothing remains to delete for the identity.
35
+ */
36
+ retainedId: InternalOpId | null;
37
+ };
38
+
39
+ type LeadingHistoryDelete = {
40
+ lookup: bson.Binary;
41
+ keys: mongo.Document[];
42
+ };
43
+
44
+ /**
45
+ * One collection being compacted, and how far this pass has processed it.
46
+ */
47
+ type CompactionScope = {
48
+ collection: mongo.Collection<mongo.Document>;
49
+ /**
50
+ * Exclusive boundary: every entry below this has been processed in this collection.
51
+ *
52
+ * Set to the target checkpoint once the collection has no more entries in range.
53
+ */
54
+ position: InternalOpId;
55
+ scannedEntries: number;
56
+ deletedEntries: number;
57
+ };
58
+
14
59
  /**
15
60
  * Compacts parameter lookup data (the bucket_parameters collection).
16
61
  *
17
- * This scans through the entire collection to find data to compact.
62
+ * Both storage versions persist a per-stream compaction cursor, so a run only scans entries in the
63
+ * un-compacted operation-id range. V1 scans its shared collection using only the `_id` index, so it
64
+ * additionally filters by stream in code.
18
65
  *
19
- * For background, see the `/docs/parameters-lookups.md` file.
66
+ * The cursor is a single value covering every collection of the stream, so it can only be advanced
67
+ * to a boundary that all collections have passed. To keep it moving during a long pass, collections
68
+ * are processed in lock-step rather than one after another - see {@link compactCollections}.
69
+ *
70
+ * For background, see the `/docs/storage/parameter-lookups.md` file.
20
71
  */
21
72
  export abstract class MongoParameterCompactor {
73
+ protected readonly logger: Logger;
74
+ protected readonly signal?: AbortSignal;
75
+
22
76
  constructor(
23
77
  protected readonly db: VersionedPowerSyncMongo,
24
- protected readonly group_id: number,
78
+ protected readonly replicationStreamId: number,
25
79
  protected readonly checkpoint: InternalOpId,
26
- protected readonly options: CompactOptions
27
- ) {}
80
+ protected readonly options: CompactOptions,
81
+ protected readonly parameterCompactionBatchSize = PARAMETER_COMPACTION_BATCH_SIZE,
82
+ protected readonly parameterCompactionPersistIntervalMs = PARAMETER_COMPACTION_PERSIST_INTERVAL_MS
83
+ ) {
84
+ this.logger = options.logger ?? defaultLogger;
85
+ this.signal = options.signal;
86
+ }
87
+
88
+ /**
89
+ * Set once the invalidation fence for this pass has been persisted. See
90
+ * {@link ensureInvalidationFence}.
91
+ */
92
+ #invalidationFencePersisted = false;
28
93
 
29
94
  async compact() {
30
- logger.info(`Compacting parameters for sync config ${this.group_id} up to checkpoint ${this.checkpoint}`);
31
- for (const collection of await this.getCollections()) {
32
- await this.compactCollection(collection);
95
+ const startedAt = Date.now();
96
+ this.signal?.throwIfAborted();
97
+ const compactedBefore = await this.readCompactedBefore();
98
+ this.logger.info(`Incrementally compacting parameters from ${compactedBefore} up to checkpoint ${this.checkpoint}`);
99
+
100
+ const result = await this.compactCollections(compactedBefore);
101
+
102
+ // Persist only after every collection has completed. This uses $max so an overlapping
103
+ // compactor cannot move the cursor backwards.
104
+ await this.persistCompactedBefore(this.checkpoint);
105
+
106
+ const durationSeconds = (Date.now() - startedAt) / 1000;
107
+ this.logger.info(
108
+ `Incremental parameter compaction completed: ` +
109
+ `collections=${result.collections}, scanned=${result.scannedEntries}, ` +
110
+ `deleted=${result.deletedEntries}, cursor=${compactedBefore}->${this.checkpoint}, ` +
111
+ `fence=${this.#invalidationFencePersisted ? this.checkpoint : 'unchanged'}, duration=${durationSeconds.toFixed(1)}s`
112
+ );
113
+ }
114
+
115
+ /**
116
+ * The exclusive operation-id boundary through which this stream's parameter indexes have all
117
+ * been compacted.
118
+ */
119
+ protected async readCompactedBefore(): Promise<InternalOpId> {
120
+ const stream = await this.db.sync_rules.findOne(
121
+ { _id: this.replicationStreamId },
122
+ { projection: { parameter_compaction: 1 } }
123
+ );
124
+ return stream?.parameter_compaction?.compacted_before == null
125
+ ? 0n
126
+ : BigInt(stream.parameter_compaction.compacted_before);
127
+ }
128
+
129
+ protected async persistCompactedBefore(compactedBefore: InternalOpId): Promise<void> {
130
+ await this.db.sync_rules.updateOne(
131
+ { _id: this.replicationStreamId },
132
+ {
133
+ $max: { 'parameter_compaction.compacted_before': compactedBefore }
134
+ }
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Commits the checkpoint-change invalidation fence before the first delete of this pass.
140
+ *
141
+ * Checkpoint change detection finds changed lookups by querying parameter entries in
142
+ * (lastCheckpoint, nextCheckpoint]. Compaction physically removes entries in that range, so a
143
+ * checkpoint that can no longer see the full history must instead invalidate all parameter
144
+ * buckets. The fence records the boundary below which that history may be missing.
145
+ *
146
+ * The fence must be committed before the first delete: MongoDB snapshot ordering then
147
+ * guarantees that a checkpoint snapshot which observes a deletion also observes the fence.
148
+ *
149
+ * This deliberately isn't the same value as the compaction cursor. If the pass fails halfway,
150
+ * the fence only causes conservative invalidation, while an advanced cursor would skip
151
+ * deletion work that never completed.
152
+ */
153
+ private async ensureInvalidationFence(): Promise<void> {
154
+ // We can consider incrementally updating the fence based on the current cursor position instead
155
+ // of the checkpoint. That would result in lower risk of triggering invalidations, but it would
156
+ // result in a higher number of updates to the `sync_rules` collection, which can make it
157
+ // counter-productive.
158
+ // Another option is to introduce an artificial delay of a couple of seconds before writing the fence,
159
+ // giving some chance for every API process to catch up. Note that the delay would have to apply
160
+ // to both the deletes and the fence - the fence write must still happen before we do any deletes.
161
+ if (this.#invalidationFencePersisted) {
162
+ return;
33
163
  }
164
+ await this.db.sync_rules.updateOne(
165
+ { _id: this.replicationStreamId },
166
+ {
167
+ $max: { 'parameter_compaction.checkpoint_changes_invalid_before': this.checkpoint }
168
+ }
169
+ );
170
+ this.#invalidationFencePersisted = true;
34
171
  }
35
172
 
36
173
  protected abstract getCollections(): Promise<mongo.Collection<mongo.Document>[]>;
37
174
 
38
- protected abstract collectionFilter(): mongo.Document;
39
-
40
- protected abstract deleteFilter(doc: mongo.Document): mongo.Document;
41
-
42
- protected async compactCollection(collection: mongo.Collection<mongo.Document>) {
43
- // This is the currently-active checkpoint.
44
- // We do not remove any data that may be used by this checkpoint.
45
- // snapshot queries ensure that if any clients are still using older checkpoints, they would
46
- // not be affected by this compaction.
47
- const checkpoint = this.checkpoint;
48
-
49
- // Index on {'key.g': 1, lookup: 1, _id: 1}
50
- // In theory, we could let MongoDB do more of the work here, by grouping by (key, lookup)
51
- // in MongoDB already. However, that risks running into cases where MongoDB needs to process
52
- // very large amounts of data before returning results, which could lead to timeouts.
53
- const cursor = collection.find(this.collectionFilter(), {
54
- sort: { lookup: 1, _id: 1 },
55
- batchSize: 10_000,
56
- projection: { _id: 1, key: 1, lookup: 1, bucket_parameters: 1 }
57
- });
175
+ protected abstract shouldCompactDocument(doc: ParameterCompactionReadDocument): boolean;
58
176
 
59
- // The index doesn't cover sorting by key, so we keep our own cache of the last seen key.
60
- let lastByKey = new LRUCache<string, InternalOpId>({
61
- max: this.options.compactParameterCacheLimit ?? 10_000
177
+ /** Deletes history preceding a batch for several identities sharing a lookup. */
178
+ protected abstract leadingHistoryDeleteFilter(
179
+ lookup: bson.Binary,
180
+ keys: mongo.Document[],
181
+ before: InternalOpId
182
+ ): mongo.Document;
183
+
184
+ /**
185
+ * Processes every collection of the stream, in lock-step: each turn takes one batch from the
186
+ * collection that has processed the least so far.
187
+ *
188
+ * The persisted cursor is the minimum position over all collections, which is exactly the
189
+ * boundary that all of them have passed, so it can be advanced periodically during the pass.
190
+ * Always picking the collection that is furthest behind also keeps each of them within one batch
191
+ * of that boundary, bounding the work an interrupted pass has to repeat.
192
+ *
193
+ * V1 storage always has a single collection; V3 has a collection per defined index. So in V1 the
194
+ * ame process collapses to compacting the single collection in order, while V3 can alternate between
195
+ * collections.
196
+ */
197
+ private async compactCollections(compactedBefore: InternalOpId): Promise<ParameterCompactionResult> {
198
+ const scopes: CompactionScope[] = (await this.getCollections()).map((collection) => ({
199
+ collection,
200
+ position: compactedBefore,
201
+ scannedEntries: 0,
202
+ deletedEntries: 0
203
+ }));
204
+ // Shared by all scopes, so the memory bound does not depend on the number of parameter indexes.
205
+ // It is safe for items to be evicted: that just changes deletes from "delete by _id" to
206
+ // the more expensive "delete by range filter".
207
+ const previousByIdentity = new LRUCache<string, CachedIdentity>({
208
+ max: this.options.compactParameterCacheLimit ?? PARAMETER_COMPACTION_CACHE_SIZE
62
209
  });
63
- let removeIds: InternalOpId[] = [];
64
- let removeDeleted: mongo.AnyBulkWriteOperation<mongo.Document>[] = [];
65
- let checkedEntries = 0;
66
- let checkedEntriesAtLastLog = 0;
67
- let lastProgressLogTime = Date.now();
68
-
69
- const flush = async (force: boolean) => {
70
- if (removeIds.length >= 1000 || (force && removeIds.length > 0)) {
71
- const results = await collection.deleteMany({ _id: { $in: removeIds } } as any);
72
- logger.info(`Removed ${results.deletedCount} (${removeIds.length}) superseded parameter entries`);
73
- removeIds = [];
210
+ let persistedFrontier = compactedBefore;
211
+ let lastPersistedAt = Date.now();
212
+
213
+ while (true) {
214
+ // Interrupting between batches is equivalent to a crash: deletes are idempotent, and the
215
+ // cursor never covers a batch that did not complete.
216
+ this.signal?.throwIfAborted();
217
+ const { frontier, scope } = this.frontier(scopes);
218
+
219
+ if (frontier > persistedFrontier && Date.now() - lastPersistedAt >= this.parameterCompactionPersistIntervalMs) {
220
+ await this.persistCompactedBefore(frontier);
221
+ persistedFrontier = frontier;
222
+ lastPersistedAt = Date.now();
223
+ this.logger.info(`Parameter compaction progress: ` + `cursor=${frontier}, target=${this.checkpoint}`);
224
+ }
225
+
226
+ if (scope == null) {
227
+ // All scopes have been processed up to the target checkpoint.
228
+ break;
74
229
  }
75
230
 
76
- if (removeDeleted.length > 10 || (force && removeDeleted.length > 0)) {
77
- const results = await collection.bulkWrite(removeDeleted);
78
- logger.info(`Removed ${results.deletedCount} (${removeDeleted.length}) deleted parameter entries`);
79
- removeDeleted = [];
231
+ // The scope on the frontier has processed the least, so taking its next batch is what keeps
232
+ // every scope within one batch of the cursor.
233
+ await this.compactBatch(scope, previousByIdentity);
234
+
235
+ if (scope.position >= this.checkpoint && scope.scannedEntries > 0) {
236
+ this.logger.info(
237
+ `Parameter compaction completed for ${scope.collection.collectionName}: ` +
238
+ `scanned=${scope.scannedEntries}, deleted=${scope.deletedEntries}`
239
+ );
80
240
  }
241
+ }
242
+
243
+ return {
244
+ collections: scopes.length,
245
+ scannedEntries: scopes.reduce((total, scope) => total + scope.scannedEntries, 0),
246
+ deletedEntries: scopes.reduce((total, scope) => total + scope.deletedEntries, 0)
81
247
  };
248
+ }
82
249
 
83
- while (await cursor.hasNext()) {
84
- const batch = cursor.readBufferedDocuments() as unknown as ParameterCompactionReadDocument[];
85
- checkedEntries += batch.length;
86
- const now = Date.now();
87
- if (now - lastProgressLogTime >= 60_000) {
88
- const elapsedSeconds = (now - lastProgressLogTime) / 1000;
89
- const rate = (checkedEntries - checkedEntriesAtLastLog) / elapsedSeconds;
90
- logger.info(`Checked ${checkedEntries} parameter index entries for compaction (${rate.toFixed(1)} entries/s)`);
91
- lastProgressLogTime = now;
92
- checkedEntriesAtLastLog = checkedEntries;
250
+ /**
251
+ * The boundary that every scope has processed past, capped at the target checkpoint, and the
252
+ * scope sitting on it.
253
+ *
254
+ * The frontier is the furthest the cursor may be advanced. The scope is the one that has
255
+ * processed the least, or null once all of them have reached the target checkpoint.
256
+ */
257
+ private frontier(scopes: CompactionScope[]): { frontier: InternalOpId; scope: CompactionScope | null } {
258
+ let frontier = this.checkpoint;
259
+ let scope: CompactionScope | null = null;
260
+ for (const candidate of scopes) {
261
+ if (candidate.position < frontier) {
262
+ frontier = candidate.position;
263
+ scope = candidate;
93
264
  }
265
+ }
266
+ return { frontier, scope };
267
+ }
94
268
 
95
- for (const doc of batch) {
96
- if (doc._id >= checkpoint) {
97
- continue;
98
- }
99
- const uniqueKey = (
100
- bson.serialize({
101
- k: doc.key,
102
- l: doc.lookup
103
- }) as Buffer
104
- ).toString('base64');
105
- const previous = lastByKey.get(uniqueKey);
106
- if (previous != null && previous < doc._id) {
107
- // We have a newer entry for the same key, so we can remove the old one.
108
- removeIds.push(previous);
269
+ /**
270
+ * Reads and processes one batch from the scope, and advances its position past that batch.
271
+ */
272
+ private async compactBatch(scope: CompactionScope, previousByIdentity: LRUCache<string, CachedIdentity>) {
273
+ const batchStartedAt = Date.now();
274
+ const collection = scope.collection;
275
+ // Typed as Document: `_id` here is an InternalOpId (bigint), not the driver's default ObjectId.
276
+ const filter: mongo.Document = { _id: { $gte: scope.position, $lt: this.checkpoint } };
277
+ const batch = (await collection
278
+ .find(filter, {
279
+ sort: { _id: 1 },
280
+ limit: this.parameterCompactionBatchSize,
281
+ batchSize: this.parameterCompactionBatchSize + 1,
282
+ projection: { _id: 1, key: 1, lookup: 1, bucket_parameters: { $slice: 1 } }
283
+ })
284
+ .toArray()) as unknown as ParameterCompactionReadDocument[];
285
+
286
+ if (batch.length < this.parameterCompactionBatchSize) {
287
+ // Fewer documents than we asked for: this collection has nothing left in the range.
288
+ scope.position = this.checkpoint;
289
+ } else {
290
+ scope.position = batch.at(-1)!._id + 1n;
291
+ }
292
+ if (batch.length == 0) {
293
+ return;
294
+ }
295
+ scope.scannedEntries += batch.length;
296
+ const deletedBeforeBatch = scope.deletedEntries;
297
+
298
+ // Keep the latest document for each identity and remove all earlier documents from this
299
+ // batch by _id, avoiding a range query for documents that have already been read.
300
+ const newestByIdentity = new Map<string, ParameterCompactionReadDocument>();
301
+ const supersededIds: InternalOpId[] = [];
302
+ for (const document of batch) {
303
+ if (!this.shouldCompactDocument(document)) {
304
+ continue;
305
+ }
306
+ const identity = identityKey(scope, document);
307
+ const previous = newestByIdentity.get(identity);
308
+ if (previous != null) {
309
+ supersededIds.push(previous._id);
310
+ }
311
+ newestByIdentity.set(identity, document);
312
+ }
313
+
314
+ const leadingHistoryDeletes = new Map<string, LeadingHistoryDelete>();
315
+ const tombstoneIds: InternalOpId[] = [];
316
+ for (const [identity, document] of newestByIdentity) {
317
+ const previous = previousByIdentity.get(identity);
318
+ if (previous == null) {
319
+ // Have not seen this (key, lookup) before, or it has been evicted from the cache.
320
+ // Delete the entire leading range.
321
+ // This should have decent performance on V3 storage; can be slow in some cases on V1.
322
+ const lookupIdentity = document.lookup.toString('base64');
323
+ const existing = leadingHistoryDeletes.get(lookupIdentity);
324
+ if (existing == null) {
325
+ leadingHistoryDeletes.set(lookupIdentity, { lookup: document.lookup, keys: [document.key] });
326
+ } else {
327
+ existing.keys.push(document.key);
109
328
  }
110
- lastByKey.set(uniqueKey, doc._id);
111
-
112
- if (doc.bucket_parameters?.length == 0) {
113
- // This is a delete operation, so we can remove it completely.
114
- // For this we cannot remove the operation itself only: There is a possibility that
115
- // there is still an earlier operation with the same key and lookup, that we don't have
116
- // in the cache due to cache size limits. So we need to explicitly remove all earlier operations.
117
- removeDeleted.push({
118
- deleteMany: {
119
- filter: this.deleteFilter(doc)
120
- }
121
- });
329
+ } else if (previous.retainedId != null) {
330
+ // We have already deleted the leading range for this (key, lookup). Only delete the last remaining
331
+ // one by _id. This is always fast.
332
+ supersededIds.push(previous.retainedId);
333
+ }
334
+
335
+ if (document.bucket_parameters?.length == 0) {
336
+ tombstoneIds.push(document._id);
337
+ }
338
+ }
339
+
340
+ // Phase 1: Delete documents read in this batch, plus retained documents from a prior batch.
341
+ scope.deletedEntries += await this.deleteByIds(collection, supersededIds);
342
+
343
+ // Phase 2: Delete leading history once per lookup group. The batch is read with
344
+ // `_id < checkpoint`, so this range is checkpoint-bounded.
345
+ const deleteBefore = batch[0]._id;
346
+ // The deletes are collected into bulkWrite commands: With high lookup cardinality there is a
347
+ // group per identity, and a command per group would mean a round trip per identity.
348
+ let deleteOperations: mongo.AnyBulkWriteOperation[] = [];
349
+ let pendingKeys = 0;
350
+ const flushDeleteOperations = async () => {
351
+ if (deleteOperations.length == 0) {
352
+ return;
353
+ }
354
+ // Safe to stop here: an interrupted batch leaves phase 3 tombstones in place, and the
355
+ // remaining deletes are repeated by the next pass.
356
+ this.signal?.throwIfAborted();
357
+ await this.ensureInvalidationFence();
358
+ const result = await collection.bulkWrite(deleteOperations, { ordered: false });
359
+ scope.deletedEntries += result.deletedCount;
360
+ deleteOperations = [];
361
+ pendingKeys = 0;
362
+ };
363
+ for (const { lookup, keys } of leadingHistoryDeletes.values()) {
364
+ for (const keyBatch of chunk(keys, PARAMETER_COMPACTION_DELETE_BATCH_SIZE)) {
365
+ deleteOperations.push({
366
+ deleteMany: { filter: this.leadingHistoryDeleteFilter(lookup, keyBatch, deleteBefore) }
367
+ });
368
+ // Bound the command size by the total number of keys it covers, not by the number of
369
+ // operations: a single group may already cover the entire batch.
370
+ pendingKeys += keyBatch.length;
371
+ if (pendingKeys >= PARAMETER_COMPACTION_DELETE_BATCH_SIZE) {
372
+ await flushDeleteOperations();
122
373
  }
123
374
  }
375
+ }
376
+ // Phase 3 requires all leading history to be deleted first.
377
+ await flushDeleteOperations();
124
378
 
125
- await flush(false);
379
+ // Phase 3: A tombstone is removed only after all preceding history has been removed.
380
+ scope.deletedEntries += await this.deleteByIds(collection, tombstoneIds);
381
+
382
+ // Update the LRU only after all phases succeed. An evicted identity safely falls back to a
383
+ // grouped leading-history delete when it appears again.
384
+ for (const [identity, document] of newestByIdentity) {
385
+ // Tombstones are recorded as `retainedId: null`: phases 2 and 3 removed the entire history
386
+ // for the identity, including the tombstone, so a later sighting needs neither delete.
387
+ previousByIdentity.set(identity, {
388
+ retainedId: document.bucket_parameters?.length == 0 ? null : document._id
389
+ });
126
390
  }
127
391
 
128
- await flush(true);
129
- logger.info(`Parameter compaction completed for ${collection.collectionName}`);
392
+ const batchDurationSeconds = (Date.now() - batchStartedAt) / 1000;
393
+ this.logger.info(
394
+ `Compacted parameter batch in ${collection.collectionName}: ` +
395
+ `_id ${batch[0]._id}..${batch.at(-1)!._id}, scanned=${batch.length} (${scope.scannedEntries} total), ` +
396
+ `batchIdentities=${newestByIdentity.size}, exactIds=${supersededIds.length + tombstoneIds.length}, ` +
397
+ `lookupGroups=${leadingHistoryDeletes.size}, deleted=${scope.deletedEntries - deletedBeforeBatch}, ` +
398
+ `duration=${batchDurationSeconds.toFixed(1)}s`
399
+ );
400
+ }
401
+
402
+ /** Deletes documents by `_id`, chunked to bound the command size. Returns the number deleted. */
403
+ private async deleteByIds(collection: mongo.Collection<mongo.Document>, ids: InternalOpId[]): Promise<number> {
404
+ let deletedEntries = 0;
405
+ for (const idBatch of chunk(ids, PARAMETER_COMPACTION_DELETE_BATCH_SIZE)) {
406
+ this.signal?.throwIfAborted();
407
+ await this.ensureInvalidationFence();
408
+ // Cast: `_id` here is an InternalOpId (bigint), not the driver's default ObjectId.
409
+ const result = await collection.deleteMany({ _id: { $in: idBatch } } as any);
410
+ deletedEntries += result.deletedCount;
411
+ }
412
+ return deletedEntries;
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Identifies a (key, lookup) pair within one collection.
418
+ *
419
+ * The collection is part of the identity: V3 keeps the parameter index id in the collection name
420
+ * rather than in the lookup, so the same (key, lookup) can appear in multiple collections meaning
421
+ * different things. Deleting the history of one says nothing about the other.
422
+ */
423
+ function identityKey(scope: CompactionScope, document: ParameterCompactionReadDocument): string {
424
+ const serialized = bson.serialize({
425
+ c: scope.collection.collectionName,
426
+ k: document.key,
427
+ l: document.lookup
428
+ }) as Buffer;
429
+ return serialized.toString('base64');
430
+ }
431
+
432
+ function* chunk<T>(items: T[], size: number): Iterable<T[]> {
433
+ for (let offset = 0; offset < items.length; offset += size) {
434
+ yield items.slice(offset, offset + size);
130
435
  }
131
436
  }