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

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 (239) hide show
  1. package/CHANGELOG.md +101 -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 +41 -16
  6. package/dist/storage/MongoBucketStorage.js +226 -71
  7. package/dist/storage/MongoBucketStorage.js.map +1 -1
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +56 -25
  9. package/dist/storage/implementation/MongoBucketBatch.js +110 -62
  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 +17 -19
  15. package/dist/storage/implementation/MongoCompactor.js +80 -263
  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 +7 -6
  21. package/dist/storage/implementation/MongoParameterCompactor.js +25 -2
  22. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  23. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
  24. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -0
  25. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
  26. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
  27. package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
  28. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
  29. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
  30. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +123 -0
  31. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  32. package/dist/storage/implementation/MongoStorageProvider.js +39 -4
  33. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +57 -29
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js +87 -61
  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 +3 -3
  39. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  40. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
  41. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +171 -24
  42. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  43. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +8 -0
  44. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  45. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  46. package/dist/storage/implementation/common/PersistedBatch.d.ts +7 -2
  47. package/dist/storage/implementation/common/PersistedBatch.js +12 -0
  48. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  49. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
  50. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  51. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  52. package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
  53. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  54. package/dist/storage/implementation/db.js +17 -15
  55. package/dist/storage/implementation/db.js.map +1 -1
  56. package/dist/storage/implementation/models.d.ts +20 -13
  57. package/dist/storage/implementation/models.js.map +1 -1
  58. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +9 -1
  59. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +61 -29
  60. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  61. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +4 -4
  62. package/dist/storage/implementation/v1/MongoChecksumsV1.js +119 -7
  63. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  64. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
  65. package/dist/storage/implementation/v1/MongoCompactorV1.js +251 -2
  66. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  67. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -19
  68. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +82 -58
  69. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  70. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
  71. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
  72. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
  73. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  74. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  75. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  76. package/dist/storage/implementation/v1/models.d.ts +10 -1
  77. package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
  78. package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
  79. package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
  80. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -4
  81. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +309 -259
  82. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  83. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +30 -9
  84. package/dist/storage/implementation/v3/MongoChecksumsV3.js +161 -20
  85. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  86. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +40 -2
  87. package/dist/storage/implementation/v3/MongoCompactorV3.js +647 -23
  88. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  89. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +67 -0
  90. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +351 -0
  91. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  92. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +45 -30
  93. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +272 -140
  94. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  95. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +6 -3
  96. package/dist/storage/implementation/v3/PersistedBatchV3.js +84 -21
  97. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  98. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  99. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  100. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  101. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +15 -13
  102. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +32 -27
  103. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  104. package/dist/storage/implementation/v3/bucket-format.d.ts +13 -0
  105. package/dist/storage/implementation/v3/bucket-format.js +77 -0
  106. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  107. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  108. package/dist/storage/implementation/v3/chunking.js +30 -0
  109. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  110. package/dist/storage/implementation/v3/models.d.ts +101 -21
  111. package/dist/storage/implementation/v3/models.js +11 -28
  112. package/dist/storage/implementation/v3/models.js.map +1 -1
  113. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
  114. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
  115. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
  116. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
  117. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
  118. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
  119. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
  120. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
  121. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
  122. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
  123. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
  124. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
  125. package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
  126. package/dist/storage/implementation/v3/source-table-utils.js +214 -0
  127. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  128. package/dist/storage/storage-index.d.ts +4 -4
  129. package/dist/storage/storage-index.js +4 -4
  130. package/dist/storage/storage-index.js.map +1 -1
  131. package/dist/types/types.d.ts +49 -3
  132. package/dist/types/types.js +48 -1
  133. package/dist/types/types.js.map +1 -1
  134. package/dist/utils/test-utils.d.ts +1 -2
  135. package/dist/utils/test-utils.js +7 -1
  136. package/dist/utils/test-utils.js.map +1 -1
  137. package/dist/utils/util.d.ts +9 -2
  138. package/dist/utils/util.js +128 -4
  139. package/dist/utils/util.js.map +1 -1
  140. package/package.json +12 -19
  141. package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
  142. package/src/storage/MongoBucketStorage.ts +347 -88
  143. package/src/storage/implementation/MongoBucketBatch.ts +147 -82
  144. package/src/storage/implementation/MongoChecksums.ts +93 -153
  145. package/src/storage/implementation/MongoCompactor.ts +102 -316
  146. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  147. package/src/storage/implementation/MongoParameterCompactor.ts +25 -6
  148. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
  149. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  150. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +154 -0
  151. package/src/storage/implementation/MongoStorageProvider.ts +46 -4
  152. package/src/storage/implementation/MongoSyncBucketStorage.ts +121 -87
  153. package/src/storage/implementation/MongoSyncRulesLock.ts +7 -4
  154. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +202 -28
  155. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +9 -0
  156. package/src/storage/implementation/common/PersistedBatch.ts +19 -3
  157. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  158. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  159. package/src/storage/implementation/db.ts +19 -20
  160. package/src/storage/implementation/models.ts +20 -14
  161. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +75 -37
  162. package/src/storage/implementation/v1/MongoChecksumsV1.ts +154 -12
  163. package/src/storage/implementation/v1/MongoCompactorV1.ts +294 -9
  164. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +142 -105
  165. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
  166. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  167. package/src/storage/implementation/v1/models.ts +11 -1
  168. package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
  169. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +382 -321
  170. package/src/storage/implementation/v3/MongoChecksumsV3.ts +250 -32
  171. package/src/storage/implementation/v3/MongoCompactorV3.ts +847 -32
  172. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +482 -0
  173. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +359 -187
  174. package/src/storage/implementation/v3/PersistedBatchV3.ts +118 -37
  175. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  176. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +46 -36
  177. package/src/storage/implementation/v3/bucket-format.ts +97 -0
  178. package/src/storage/implementation/v3/chunking.ts +40 -0
  179. package/src/storage/implementation/v3/models.ts +132 -55
  180. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
  181. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
  182. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
  183. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
  184. package/src/storage/implementation/v3/source-table-utils.ts +392 -0
  185. package/src/storage/storage-index.ts +12 -4
  186. package/src/types/types.ts +58 -1
  187. package/src/utils/test-utils.ts +8 -3
  188. package/src/utils/util.ts +185 -6
  189. package/test/src/__snapshots__/storage.test.ts.snap +0 -75
  190. package/test/src/__snapshots__/storage_sync.test.ts.snap +35 -617
  191. package/test/src/cleanup-stopped-sync-configs.test.ts +671 -0
  192. package/test/src/helpers/MemoryObjectStorage.ts +53 -0
  193. package/test/src/helpers/s3TestFactory.ts +69 -0
  194. package/test/src/object_storage_lifecycle.test.ts +59 -0
  195. package/test/src/setup.ts +6 -1
  196. package/test/src/storage.test.ts +227 -13
  197. package/test/src/storage_compacting.test.ts +1951 -12
  198. package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
  199. package/test/src/storage_s3_checksums.test.ts +191 -0
  200. package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
  201. package/test/src/storage_s3_reading.test.ts +526 -0
  202. package/test/src/storage_s3_writing.test.ts +135 -0
  203. package/test/src/storage_sync.test.ts +1180 -73
  204. package/test/src/storeCurrentData.test.ts +227 -0
  205. package/test/src/util.ts +2 -1
  206. package/tsconfig.json +1 -4
  207. package/tsconfig.tsbuildinfo +1 -1
  208. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -15
  209. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  210. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  211. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  212. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -67
  213. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  214. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -29
  215. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -76
  216. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  217. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  218. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  219. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  220. package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
  221. package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
  222. package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
  223. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
  224. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
  225. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
  226. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -4
  227. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  228. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  229. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
  230. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -48
  231. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
  232. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -75
  233. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -82
  234. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -101
  235. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  236. package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
  237. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
  238. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
  239. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -68
@@ -0,0 +1,482 @@
1
+ import * as lib_mongo from '@powersync/lib-service-mongodb';
2
+ import { Logger, ReplicationAbortedError } from '@powersync/lib-services-framework';
3
+ import { SingleSyncConfigBucketDefinitionMapping, storage } from '@powersync/service-core';
4
+ import { BucketDefinitionId, ParameterIndexId, SyncConfig } from '@powersync/service-sync-rules';
5
+ import * as bson from 'bson';
6
+ import { clearCollectionInIdRanges, idPrefixFilter } from '../../../utils/util.js';
7
+ import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
8
+ import {
9
+ BucketStateDocumentV3,
10
+ ReplicationStreamDocumentV3,
11
+ SourceTableDocumentV3,
12
+ SyncConfigDefinition
13
+ } from './models.js';
14
+ import { ObjectStorage } from './object-storage/ObjectStorage.js';
15
+ import { ObjectStorageLifecycle } from './object-storage/ObjectStorageLifecycle.js';
16
+
17
+ type SyncConfigState = ReplicationStreamDocumentV3['sync_configs'][number];
18
+
19
+ const LIVE_STATES = new Set<storage.SyncRuleState>([
20
+ storage.SyncRuleState.ACTIVE,
21
+ storage.SyncRuleState.PROCESSING,
22
+ storage.SyncRuleState.ERRORED
23
+ ]);
24
+
25
+ const EMPTY_RESULT: storage.CleanupStoppedSyncConfigsResult = {
26
+ stoppedSyncConfigsRemoved: 0,
27
+ bucketDataCollectionsDropped: 0,
28
+ parameterIndexCollectionsDropped: 0,
29
+ bucketStateDocumentsDeleted: 0,
30
+ sourceRecordCollectionsDropped: 0,
31
+ sourceTablesUpdated: 0,
32
+ sourceTablesDeleted: 0
33
+ };
34
+
35
+ export interface MongoStoppedSyncConfigCleanupOptions extends storage.CleanupStoppedSyncConfigsOptions {
36
+ replicationStreamId: number;
37
+ db: VersionedPowerSyncMongoV3;
38
+ logger: Logger;
39
+ clearBatchThrottleRate: number;
40
+ objectStorage?: ObjectStorage;
41
+ }
42
+
43
+ export class MongoStoppedSyncConfigCleanup {
44
+ private readonly db: VersionedPowerSyncMongoV3;
45
+ private readonly replicationStreamId: number;
46
+ private readonly signal: AbortSignal | undefined;
47
+ private readonly logger: Logger;
48
+ private readonly objectStorage: ObjectStorage | undefined;
49
+ private readonly defaultSchema: string;
50
+ private readonly sourceConnectionTag: string;
51
+ private readonly clearBatchThrottleRate: number;
52
+
53
+ constructor(options: MongoStoppedSyncConfigCleanupOptions) {
54
+ this.db = options.db;
55
+ this.replicationStreamId = options.replicationStreamId;
56
+ this.signal = options.signal;
57
+ this.logger = options.logger;
58
+ this.objectStorage = options.objectStorage;
59
+ this.defaultSchema = options.defaultSchema;
60
+ this.sourceConnectionTag = options.sourceConnectionTag;
61
+ this.clearBatchThrottleRate = options.clearBatchThrottleRate;
62
+ }
63
+
64
+ async run(): Promise<storage.CleanupStoppedSyncConfigsResult> {
65
+ this.throwIfAborted();
66
+
67
+ const doc = await this.db.sync_rules.findOne<ReplicationStreamDocumentV3>(
68
+ { _id: this.replicationStreamId },
69
+ { projection: { sync_configs: 1 } }
70
+ );
71
+ if (doc == null || doc.sync_configs.length == 0) {
72
+ return { ...EMPTY_RESULT };
73
+ }
74
+
75
+ const stoppedStates = doc.sync_configs.filter((config) => config.state == storage.SyncRuleState.STOP);
76
+ if (stoppedStates.length == 0) {
77
+ return { ...EMPTY_RESULT };
78
+ }
79
+
80
+ const liveStates = doc.sync_configs.filter((config) => LIVE_STATES.has(config.state));
81
+ const configDocs = await this.loadSyncConfigDefinitions([...stoppedStates, ...liveStates]);
82
+ const stoppedIds = new Set(stoppedStates.map((state) => state._id.toHexString()));
83
+ const liveIds = new Set(liveStates.map((state) => state._id.toHexString()));
84
+ const stoppedConfigDocs = configDocs.filter((config) => stoppedIds.has(config._id.toHexString()));
85
+ const liveConfigDocs = configDocs.filter((config) => liveIds.has(config._id.toHexString()));
86
+
87
+ const stoppedStorageIds = this.storageIdsFor(stoppedConfigDocs);
88
+ const liveStorageIds = this.storageIdsFor(liveConfigDocs);
89
+ const unusedBucketDefinitionIds = difference(
90
+ stoppedStorageIds.bucketDefinitionIds,
91
+ liveStorageIds.bucketDefinitionIds
92
+ );
93
+ const unusedParameterIndexIds = difference(stoppedStorageIds.parameterIndexIds, liveStorageIds.parameterIndexIds);
94
+
95
+ const result: storage.CleanupStoppedSyncConfigsResult = {
96
+ ...EMPTY_RESULT
97
+ };
98
+
99
+ if (unusedBucketDefinitionIds.length > 0 || unusedParameterIndexIds.length > 0) {
100
+ await this.cleanupSourceTableMemberships(
101
+ unusedBucketDefinitionIds,
102
+ unusedParameterIndexIds,
103
+ liveConfigDocs,
104
+ result
105
+ );
106
+ result.bucketDataCollectionsDropped = await this.dropBucketDataCollections(unusedBucketDefinitionIds);
107
+ result.bucketStateDocumentsDeleted = await this.deleteBucketStateDocuments(unusedBucketDefinitionIds);
108
+ result.parameterIndexCollectionsDropped = await this.dropParameterIndexCollections(unusedParameterIndexIds);
109
+ }
110
+
111
+ // Event-only source tables carry empty membership arrays, so they are never selected by the
112
+ // membership filter above. Clean them up separately, regardless of whether any bucket or
113
+ // parameter ids became unused (a stopped config may have contributed only an event trigger).
114
+ await this.cleanupEventOnlySourceTables(liveConfigDocs, result);
115
+
116
+ result.stoppedSyncConfigsRemoved = await this.pruneStoppedSyncConfigStates(stoppedStates);
117
+
118
+ if (result.stoppedSyncConfigsRemoved > 0) {
119
+ this.logger.info(
120
+ `Cleaned up ${result.stoppedSyncConfigsRemoved} stopped sync config${result.stoppedSyncConfigsRemoved == 1 ? '' : 's'}`,
121
+ result
122
+ );
123
+ }
124
+ return result;
125
+ }
126
+
127
+ private async loadSyncConfigDefinitions(states: SyncConfigState[]): Promise<SyncConfigDefinition[]> {
128
+ const ids = [...new Map(states.map((state) => [state._id.toHexString(), state._id])).values()];
129
+ if (ids.length == 0) {
130
+ return [];
131
+ }
132
+ return this.db.syncConfigDefinitions.find({ _id: { $in: ids } }).toArray();
133
+ }
134
+
135
+ private storageIdsFor(configs: Pick<SyncConfigDefinition, 'rule_mapping'>[]) {
136
+ const mappings = configs.map((config) =>
137
+ SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(config.rule_mapping)
138
+ );
139
+ return {
140
+ bucketDefinitionIds: [...new Set(mappings.flatMap((mapping) => mapping.allBucketDefinitionIds()))],
141
+ parameterIndexIds: [...new Set(mappings.flatMap((mapping) => mapping.allParameterIndexIds()))]
142
+ };
143
+ }
144
+
145
+ private async cleanupSourceTableMemberships(
146
+ unusedBucketDefinitionIds: BucketDefinitionId[],
147
+ unusedParameterIndexIds: ParameterIndexId[],
148
+ liveConfigDocs: SyncConfigDefinition[],
149
+ result: storage.CleanupStoppedSyncConfigsResult
150
+ ) {
151
+ const update: Record<string, unknown> = {};
152
+ if (unusedBucketDefinitionIds.length > 0) {
153
+ update.bucket_data_source_ids = { $in: unusedBucketDefinitionIds };
154
+ }
155
+ if (unusedParameterIndexIds.length > 0) {
156
+ update.parameter_lookup_source_ids = { $in: unusedParameterIndexIds };
157
+ }
158
+
159
+ // Keep obsolete membership ids as the durable cleanup marker until each source table is
160
+ // either deleted or retained. If interrupted after dropping a source_records collection,
161
+ // the next run can still rediscover the source table from these obsolete ids and retry.
162
+ const filter = this.sourceTableMembershipFilter(unusedBucketDefinitionIds, unusedParameterIndexIds);
163
+ const candidateSourceTables = await this.db
164
+ .sourceTables(this.replicationStreamId)
165
+ .find(filter, {
166
+ projection: {
167
+ _id: 1,
168
+ bucket_data_source_ids: 1,
169
+ parameter_lookup_source_ids: 1,
170
+ schema_name: 1,
171
+ table_name: 1
172
+ }
173
+ })
174
+ .toArray();
175
+ if (candidateSourceTables.length == 0) {
176
+ return;
177
+ }
178
+ const liveSyncConfigs = this.parseSyncConfigs(liveConfigDocs);
179
+
180
+ const deletableSourceTables = candidateSourceTables.filter(
181
+ (sourceTable) =>
182
+ this.membershipsBecomeEmpty(sourceTable, unusedBucketDefinitionIds, unusedParameterIndexIds) &&
183
+ !this.triggersLiveEvent(sourceTable, liveSyncConfigs)
184
+ );
185
+ const retainedSourceTables = candidateSourceTables.filter(
186
+ (sourceTable) => !deletableSourceTables.some((deletable) => deletable._id.equals(sourceTable._id))
187
+ );
188
+ const retainedSourceTableIds = retainedSourceTables.map((sourceTable) => sourceTable._id);
189
+
190
+ await this.deleteSourceTables(
191
+ deletableSourceTables.map((table) => table._id),
192
+ (ids) => this.deletableSourceTableFilter(ids, unusedBucketDefinitionIds, unusedParameterIndexIds),
193
+ result
194
+ );
195
+
196
+ // A retained source table whose memberships become empty is kept alive only by a live event
197
+ // (otherwise it would be deletable). It becomes event-only, and event-only save() never reads
198
+ // or writes current_data, so its source_records collection is now dead weight. Drop it before
199
+ // the $pull below, so the obsolete membership ids remain as a recovery marker if interrupted.
200
+ // Existing source-table docs only ever shrink their memberships, so this table cannot resume
201
+ // data sync on the same doc and need current_data again.
202
+ const becomingEventOnlySourceTableIds = retainedSourceTables
203
+ .filter((sourceTable) =>
204
+ this.membershipsBecomeEmpty(sourceTable, unusedBucketDefinitionIds, unusedParameterIndexIds)
205
+ )
206
+ .map((sourceTable) => sourceTable._id);
207
+ for (const sourceTableId of becomingEventOnlySourceTableIds) {
208
+ this.throwIfAborted();
209
+ await this.dropCollection(this.db.sourceRecords(this.replicationStreamId, sourceTableId));
210
+ result.sourceRecordCollectionsDropped += 1;
211
+ }
212
+
213
+ if (retainedSourceTableIds.length > 0) {
214
+ this.throwIfAborted();
215
+ const updateResult = await this.db.sourceTables(this.replicationStreamId).updateMany(
216
+ { _id: { $in: retainedSourceTableIds } },
217
+ {
218
+ $pull: update
219
+ },
220
+ { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
221
+ );
222
+ result.sourceTablesUpdated += updateResult.modifiedCount;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Clean up event-only source tables that no live sync config still triggers events for.
228
+ *
229
+ * Event-only source tables carry empty membership arrays (see source-table-utils:
230
+ * "Empty memberships indicate an event-only table"), so the membership filter never selects
231
+ * them. Without this, the source_tables row and its source_records collection leak when the
232
+ * config that contributed the event trigger is stopped.
233
+ */
234
+ private async cleanupEventOnlySourceTables(
235
+ liveConfigDocs: SyncConfigDefinition[],
236
+ result: storage.CleanupStoppedSyncConfigsResult
237
+ ) {
238
+ const candidateSourceTables = await this.db
239
+ .sourceTables(this.replicationStreamId)
240
+ .find(this.eventOnlySourceTableFilter(), {
241
+ projection: {
242
+ _id: 1,
243
+ bucket_data_source_ids: 1,
244
+ parameter_lookup_source_ids: 1,
245
+ schema_name: 1,
246
+ table_name: 1
247
+ }
248
+ })
249
+ .toArray();
250
+ if (candidateSourceTables.length == 0) {
251
+ return;
252
+ }
253
+
254
+ const liveSyncConfigs = this.parseSyncConfigs(liveConfigDocs);
255
+ const deletableSourceTableIds = candidateSourceTables
256
+ .filter((sourceTable) => !this.triggersLiveEvent(sourceTable, liveSyncConfigs))
257
+ .map((sourceTable) => sourceTable._id);
258
+
259
+ await this.deleteSourceTables(deletableSourceTableIds, (ids) => this.eventOnlyDeletableFilter(ids), result);
260
+ }
261
+
262
+ /**
263
+ * Drop the given source tables and their source_records collections.
264
+ *
265
+ * `refetchFilter` re-applies the deletability predicate against the latest persisted state, so
266
+ * a source table that became live through another writer after planning is not deleted. The
267
+ * sourceRecords collection is dropped before the related sourceTables row, so an interruption
268
+ * can be recovered on the next run.
269
+ */
270
+ private async deleteSourceTables(
271
+ deletableSourceTableIds: bson.ObjectId[],
272
+ refetchFilter: (ids: bson.ObjectId[]) => Record<string, unknown>,
273
+ result: storage.CleanupStoppedSyncConfigsResult
274
+ ) {
275
+ if (deletableSourceTableIds.length == 0) {
276
+ return;
277
+ }
278
+ const sourceTablesToDelete = await this.db
279
+ .sourceTables(this.replicationStreamId)
280
+ .find(refetchFilter(deletableSourceTableIds), { projection: { _id: 1 } })
281
+ .toArray();
282
+
283
+ for (const sourceTable of sourceTablesToDelete) {
284
+ this.throwIfAborted();
285
+ await this.dropCollection(this.db.sourceRecords(this.replicationStreamId, sourceTable._id));
286
+ result.sourceRecordCollectionsDropped += 1;
287
+ }
288
+
289
+ if (sourceTablesToDelete.length > 0) {
290
+ this.throwIfAborted();
291
+ const deleteResult = await this.db
292
+ .sourceTables(this.replicationStreamId)
293
+ .deleteMany(refetchFilter(sourceTablesToDelete.map((table) => table._id)), {
294
+ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS
295
+ });
296
+ result.sourceTablesDeleted += deleteResult.deletedCount;
297
+ }
298
+ }
299
+
300
+ private membershipsBecomeEmpty(
301
+ sourceTable: Pick<SourceTableDocumentV3, 'bucket_data_source_ids' | 'parameter_lookup_source_ids'>,
302
+ unusedBucketDefinitionIds: BucketDefinitionId[],
303
+ unusedParameterIndexIds: ParameterIndexId[]
304
+ ): boolean {
305
+ const unusedBucketDefinitionIdSet = new Set(unusedBucketDefinitionIds);
306
+ const unusedParameterIndexIdSet = new Set(unusedParameterIndexIds);
307
+ return (
308
+ sourceTable.bucket_data_source_ids.every((id) => unusedBucketDefinitionIdSet.has(id)) &&
309
+ sourceTable.parameter_lookup_source_ids.every((id) => unusedParameterIndexIdSet.has(id))
310
+ );
311
+ }
312
+
313
+ private deletableSourceTableFilter(
314
+ ids: bson.ObjectId[],
315
+ unusedBucketDefinitionIds: BucketDefinitionId[],
316
+ unusedParameterIndexIds: ParameterIndexId[]
317
+ ): Record<string, unknown> {
318
+ return {
319
+ _id: { $in: ids },
320
+ bucket_data_source_ids: { $not: { $elemMatch: { $nin: unusedBucketDefinitionIds } } },
321
+ parameter_lookup_source_ids: { $not: { $elemMatch: { $nin: unusedParameterIndexIds } } }
322
+ };
323
+ }
324
+
325
+ private eventOnlySourceTableFilter(): Record<string, unknown> {
326
+ return {
327
+ bucket_data_source_ids: { $size: 0 },
328
+ parameter_lookup_source_ids: { $size: 0 }
329
+ };
330
+ }
331
+
332
+ private eventOnlyDeletableFilter(ids: bson.ObjectId[]): Record<string, unknown> {
333
+ return {
334
+ _id: { $in: ids },
335
+ ...this.eventOnlySourceTableFilter()
336
+ };
337
+ }
338
+
339
+ private parseSyncConfigs(configDocs: SyncConfigDefinition[]): SyncConfig[] {
340
+ // This is ugly - we should not need to re-parse to achieve this.
341
+ // Revisit persistence for this later.
342
+ return configDocs.map((config) => {
343
+ return storage.parsePersistedSyncConfigContent({
344
+ content: config.content,
345
+ compiledPlan: config.serialized_plan ?? null,
346
+ storageVersion: config.storage_version,
347
+ parseOptions: {
348
+ defaultSchema: this.defaultSchema
349
+ }
350
+ }).config;
351
+ });
352
+ }
353
+
354
+ private triggersLiveEvent(sourceTable: SourceTableDocumentV3, liveSyncConfigs: SyncConfig[]): boolean {
355
+ return liveSyncConfigs.some((syncConfig) =>
356
+ syncConfig.tableTriggersEvent({
357
+ connectionTag: this.sourceConnectionTag,
358
+ schema: sourceTable.schema_name,
359
+ name: sourceTable.table_name
360
+ })
361
+ );
362
+ }
363
+
364
+ private sourceTableMembershipFilter(
365
+ bucketDefinitionIds: BucketDefinitionId[],
366
+ parameterIndexIds: ParameterIndexId[]
367
+ ): Partial<SourceTableDocumentV3> | Record<string, unknown> {
368
+ const clauses: Record<string, unknown>[] = [];
369
+ if (bucketDefinitionIds.length > 0) {
370
+ clauses.push({ bucket_data_source_ids: { $in: bucketDefinitionIds } });
371
+ }
372
+ if (parameterIndexIds.length > 0) {
373
+ clauses.push({ parameter_lookup_source_ids: { $in: parameterIndexIds } });
374
+ }
375
+ if (clauses.length == 0) {
376
+ return { _id: { $exists: false } };
377
+ }
378
+ return { $or: clauses };
379
+ }
380
+
381
+ private async dropBucketDataCollections(definitionIds: BucketDefinitionId[]): Promise<number> {
382
+ const lifecycle = this.objectStorage
383
+ ? new ObjectStorageLifecycle(this.db, this.replicationStreamId, this.objectStorage)
384
+ : null;
385
+ for (const definitionId of definitionIds) {
386
+ this.throwIfAborted();
387
+ await this.dropCollection(this.db.bucketData(this.replicationStreamId, definitionId));
388
+ await lifecycle?.deletePrefix(lifecycle.definitionPrefix(definitionId), { signal: this.signal });
389
+ }
390
+ return definitionIds.length;
391
+ }
392
+
393
+ /**
394
+ * Delete bucket_state documents for the unused bucket definitions.
395
+ *
396
+ * The bucket_state collection is shared across all bucket definitions of the
397
+ * stream (keyed by `_id`, an ordered `{ d, b }` compound), so we delete the individual documents.
398
+ *
399
+ * There can be hundreds of millions of buckets, so we delete per definition using a range filter
400
+ * on `_id` (which uses the `_id` index) rather than a slow `_id.d` field scan, and retry on
401
+ * MaxTimeMSExpired so each call makes progress in batches.
402
+ */
403
+ private async deleteBucketStateDocuments(definitionIds: BucketDefinitionId[]): Promise<number> {
404
+ const collection = this.db.bucketState(this.replicationStreamId);
405
+ let deletedCount = 0;
406
+ for (const definitionId of definitionIds) {
407
+ this.throwIfAborted();
408
+ deletedCount += await clearCollectionInIdRanges(
409
+ this.logger,
410
+ 'bucket state',
411
+ collection,
412
+ {
413
+ _id: idPrefixFilter<BucketStateDocumentV3['_id']>({ d: definitionId }, ['b'])
414
+ },
415
+ this.signal,
416
+ this.clearBatchThrottleRate
417
+ );
418
+ }
419
+ return deletedCount;
420
+ }
421
+
422
+ private async dropParameterIndexCollections(indexIds: ParameterIndexId[]): Promise<number> {
423
+ for (const indexId of indexIds) {
424
+ this.throwIfAborted();
425
+ await this.dropCollection(this.db.parameterIndex(this.replicationStreamId, indexId));
426
+ }
427
+ return indexIds.length;
428
+ }
429
+
430
+ private async dropCollection<T extends lib_mongo.mongo.Document>(
431
+ collection: lib_mongo.mongo.Collection<T>
432
+ ): Promise<void> {
433
+ await collection.drop({ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }).catch((error) => {
434
+ if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
435
+ return;
436
+ }
437
+ throw error;
438
+ });
439
+ }
440
+
441
+ private async pruneStoppedSyncConfigStates(stoppedStates: SyncConfigState[]): Promise<number> {
442
+ this.throwIfAborted();
443
+ const stoppedIds = stoppedStates.map((state) => state._id);
444
+ const result = await this.db.sync_rules.updateOne(
445
+ {
446
+ _id: this.replicationStreamId,
447
+ sync_configs: {
448
+ $not: {
449
+ $elemMatch: {
450
+ _id: { $in: stoppedIds },
451
+ state: { $ne: storage.SyncRuleState.STOP }
452
+ }
453
+ }
454
+ }
455
+ },
456
+ {
457
+ $pull: {
458
+ sync_configs: {
459
+ _id: { $in: stoppedIds },
460
+ state: storage.SyncRuleState.STOP
461
+ }
462
+ }
463
+ } as bson.Document
464
+ );
465
+ if (result.modifiedCount == 0 && stoppedIds.length > 0) {
466
+ this.logger.warn(`Skipped pruning stopped sync configs`);
467
+ return 0;
468
+ }
469
+ return stoppedStates.length;
470
+ }
471
+
472
+ private throwIfAborted() {
473
+ if (this.signal?.aborted) {
474
+ throw new ReplicationAbortedError('Aborted stopped sync config cleanup', this.signal.reason);
475
+ }
476
+ }
477
+ }
478
+
479
+ function difference<T>(left: T[], right: T[]): T[] {
480
+ const rightSet = new Set(right);
481
+ return left.filter((value) => !rightSet.has(value));
482
+ }