@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,698 @@
1
+ import { MongoSyncBucketStorageV3 } from '@module/storage/implementation/v3/MongoSyncBucketStorageV3.js';
2
+ import { ObjectStorageLifecycle } from '@module/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js';
3
+ import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
4
+ import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
5
+ import { test_utils } from '@powersync/service-core-tests';
6
+ import * as bson from 'bson';
7
+ import { describe, expect, test } from 'vitest';
8
+ import { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
9
+ import { ReplicationStreamDocumentV3 } from '../../src/storage/implementation/v3/models.js';
10
+ import { env } from './env.js';
11
+ import { MemoryObjectStorage } from './helpers/MemoryObjectStorage.js';
12
+ import { INITIALIZED_MONGO_STORAGE_FACTORY } from './util.js';
13
+
14
+ function sourceDescriptor(name: string, options: { objectId?: string } = {}): storage.SourceEntityDescriptor {
15
+ return {
16
+ connectionTag: storage.SourceTable.DEFAULT_TAG,
17
+ objectId: options.objectId ?? name,
18
+ schema: 'public',
19
+ name,
20
+ replicaIdColumns: [
21
+ {
22
+ name: 'id',
23
+ type: 'VARCHAR',
24
+ typeId: 25
25
+ }
26
+ ]
27
+ };
28
+ }
29
+
30
+ function objectIdGenerator(id: string) {
31
+ let used = false;
32
+ return () => {
33
+ if (used) {
34
+ throw new Error(`Can only generate a single id using ${id}`);
35
+ }
36
+ used = true;
37
+ return new bson.ObjectId(id);
38
+ };
39
+ }
40
+
41
+ async function collectionExists(db: VersionedPowerSyncMongoV3, name: string) {
42
+ return await db.db.listCollections({ name }, { nameOnly: true }).hasNext();
43
+ }
44
+
45
+ async function cleanupStoppedSyncConfigs(bucketStorage: MongoSyncBucketStorageV3) {
46
+ return bucketStorage.cleanupStoppedSyncConfigs({
47
+ defaultSchema: 'public',
48
+ sourceConnectionTag: storage.SourceTable.DEFAULT_TAG
49
+ });
50
+ }
51
+
52
+ describe('cleanupStoppedSyncConfigs - mongodb', () => {
53
+ test('cleans up unused stopped sync config storage', async () => {
54
+ const objectStorage = new MemoryObjectStorage();
55
+ const storageFactory = mongoTestStorageFactoryGenerator({
56
+ url: env.MONGO_TEST_URL,
57
+ isCI: env.CI,
58
+ supportsMultipleSyncConfigs: true,
59
+ objectStorage
60
+ });
61
+ await using factory = await storageFactory.factory();
62
+
63
+ const first = await factory.updateSyncRules(
64
+ updateSyncRulesFromYaml(
65
+ `
66
+ config:
67
+ edition: 3
68
+
69
+ streams:
70
+ by_project:
71
+ query: |
72
+ SELECT id, project FROM "Scene"
73
+ WHERE project IN (
74
+ SELECT project FROM "ProjectInvitation"
75
+ WHERE project = subscription.parameter('project')
76
+ )
77
+ `,
78
+ { storageVersion: 3 }
79
+ )
80
+ );
81
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorageV3;
82
+ const db = firstStorage.db as VersionedPowerSyncMongoV3;
83
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
84
+
85
+ const sceneTable = (
86
+ await firstWriter.resolveTables({
87
+ connection_id: 1,
88
+ source: sourceDescriptor('Scene', { objectId: 'scene-relation' }),
89
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38350')
90
+ })
91
+ ).tables[0];
92
+ const invitationTable = (
93
+ await firstWriter.resolveTables({
94
+ connection_id: 1,
95
+ source: sourceDescriptor('ProjectInvitation', { objectId: 'project-invitation-relation' }),
96
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38351')
97
+ })
98
+ ).tables[0];
99
+ await firstWriter.save({
100
+ sourceTable: sceneTable,
101
+ tag: storage.SaveOperationTag.INSERT,
102
+ after: {
103
+ id: 'scene-1',
104
+ project: 'project-1'
105
+ },
106
+ afterReplicaId: test_utils.rid('scene-1')
107
+ });
108
+ await firstWriter.save({
109
+ sourceTable: invitationTable,
110
+ tag: storage.SaveOperationTag.INSERT,
111
+ after: {
112
+ id: 'invitation-1',
113
+ project: 'project-1'
114
+ },
115
+ afterReplicaId: test_utils.rid('invitation-1')
116
+ });
117
+ await firstWriter.markAllSnapshotDone('1/1');
118
+ await firstWriter.commit('1/1');
119
+
120
+ const stoppedMapping = first.syncConfigContent[0].mapping;
121
+ const stoppedBucketDefinitionId = stoppedMapping.allBucketDefinitionIds()[0];
122
+ const stoppedParameterIndexId = stoppedMapping.allParameterIndexIds()[0];
123
+ expect(stoppedBucketDefinitionId).toBeDefined();
124
+ expect(stoppedParameterIndexId).toBeDefined();
125
+ const bucketDataCollection = db.bucketData(first.replicationStreamId, stoppedBucketDefinitionId!).collectionName;
126
+ const parameterIndexCollection = db.parameterIndex(
127
+ first.replicationStreamId,
128
+ stoppedParameterIndexId!
129
+ ).collectionName;
130
+ const sceneRecordsCollection = db.sourceRecords(
131
+ first.replicationStreamId,
132
+ sceneTable.id as bson.ObjectId
133
+ ).collectionName;
134
+ const invitationRecordsCollection = db.sourceRecords(
135
+ first.replicationStreamId,
136
+ invitationTable.id as bson.ObjectId
137
+ ).collectionName;
138
+ expect(await collectionExists(db, bucketDataCollection)).toBe(true);
139
+ expect(await collectionExists(db, parameterIndexCollection)).toBe(true);
140
+ expect(await collectionExists(db, sceneRecordsCollection)).toBe(true);
141
+ expect(await collectionExists(db, invitationRecordsCollection)).toBe(true);
142
+
143
+ const second = await factory.updateSyncRules(
144
+ updateSyncRulesFromYaml(
145
+ `
146
+ config:
147
+ edition: 3
148
+
149
+ streams:
150
+ by_owner:
151
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
152
+ `,
153
+ { storageVersion: 3 }
154
+ )
155
+ );
156
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
157
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
158
+ const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorageV3;
159
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
160
+ await secondWriter.markAllSnapshotDone('2/1');
161
+ await secondWriter.commit('2/1');
162
+
163
+ const lifecycle = new ObjectStorageLifecycle(db, first.replicationStreamId, objectStorage);
164
+ const stoppedObject = `${lifecycle.definitionPrefix(stoppedBucketDefinitionId!)}stopped.bson`;
165
+ const similarDefinitionObject = `${lifecycle.definitionPrefix(`${stoppedBucketDefinitionId!}0`)}live.bson`;
166
+ for (const path of [stoppedObject, similarDefinitionObject]) {
167
+ await objectStorage.put(path, new Uint8Array(), {
168
+ contentType: 'application/bson',
169
+ contentEncoding: null
170
+ });
171
+ }
172
+
173
+ const result = await cleanupStoppedSyncConfigs(
174
+ (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3
175
+ );
176
+
177
+ expect(result).toEqual({
178
+ stoppedSyncConfigsRemoved: 1,
179
+ bucketDataCollectionsDropped: 1,
180
+ parameterIndexCollectionsDropped: 1,
181
+ bucketStateDocumentsDeleted: 1,
182
+ sourceRecordCollectionsDropped: 2,
183
+ sourceTablesUpdated: 0,
184
+ sourceTablesDeleted: 2
185
+ });
186
+ expect(await collectionExists(db, bucketDataCollection)).toBe(false);
187
+ expect(await collectionExists(db, parameterIndexCollection)).toBe(false);
188
+ expect(await collectionExists(db, sceneRecordsCollection)).toBe(false);
189
+ expect(await collectionExists(db, invitationRecordsCollection)).toBe(false);
190
+ expect(objectStorage.store.has(stoppedObject)).toBe(false);
191
+ expect(objectStorage.store.has(similarDefinitionObject)).toBe(true);
192
+ expect(
193
+ await db.sourceTables(first.replicationStreamId).countDocuments({ _id: sceneTable.id as bson.ObjectId })
194
+ ).toBe(0);
195
+ expect(
196
+ await db.sourceTables(first.replicationStreamId).countDocuments({ _id: invitationTable.id as bson.ObjectId })
197
+ ).toBe(0);
198
+
199
+ const streamDoc = (await db.sync_rules.findOne({ _id: first.replicationStreamId })) as ReplicationStreamDocumentV3;
200
+ expect(streamDoc.sync_configs.map((config) => config.state)).toEqual([storage.SyncRuleState.ACTIVE]);
201
+ });
202
+
203
+ test('keeps source table membership still used by a live sync config', async () => {
204
+ await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
205
+
206
+ const first = await factory.updateSyncRules(
207
+ updateSyncRulesFromYaml(
208
+ `
209
+ config:
210
+ edition: 3
211
+
212
+ streams:
213
+ by_owner:
214
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
215
+ by_project:
216
+ query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
217
+ `,
218
+ { storageVersion: 3 }
219
+ )
220
+ );
221
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorageV3;
222
+ const db = firstStorage.db as VersionedPowerSyncMongoV3;
223
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
224
+ const resolved = await firstWriter.resolveTables({
225
+ connection_id: 1,
226
+ source: sourceDescriptor('todos', { objectId: 'todos-relation' }),
227
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38352')
228
+ });
229
+ await firstWriter.save({
230
+ sourceTable: resolved.tables[0],
231
+ tag: storage.SaveOperationTag.INSERT,
232
+ after: {
233
+ id: 'todo-1',
234
+ owner_id: 'user-1',
235
+ project_id: 'project-1'
236
+ },
237
+ afterReplicaId: test_utils.rid('todo-1')
238
+ });
239
+ await firstWriter.markAllSnapshotDone('1/1');
240
+ await firstWriter.commit('1/1');
241
+
242
+ const sourceTableId = resolved.tables[0].id as bson.ObjectId;
243
+ const sourceRecordsCollection = db.sourceRecords(first.replicationStreamId, sourceTableId).collectionName;
244
+ const [ownerDefinitionId, projectDefinitionId] = first.syncConfigContent[0].mapping.allBucketDefinitionIds();
245
+ const ownerBucketDataCollection = db.bucketData(first.replicationStreamId, ownerDefinitionId).collectionName;
246
+ expect(await collectionExists(db, ownerBucketDataCollection)).toBe(true);
247
+ expect(
248
+ (await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId }))?.bucket_data_source_ids
249
+ ).toEqual([ownerDefinitionId, projectDefinitionId]);
250
+
251
+ const second = await factory.updateSyncRules(
252
+ updateSyncRulesFromYaml(
253
+ `
254
+ config:
255
+ edition: 3
256
+
257
+ streams:
258
+ by_project:
259
+ query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
260
+ `,
261
+ { storageVersion: 3 }
262
+ )
263
+ );
264
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
265
+ const secondDefinitionId = second.syncConfigContent[1].mapping.allBucketDefinitionIds()[0];
266
+ expect(secondDefinitionId).toBe(projectDefinitionId);
267
+
268
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
269
+ const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorageV3;
270
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
271
+ await secondWriter.markAllSnapshotDone('2/1');
272
+ await secondWriter.commit('2/1');
273
+
274
+ const result = await cleanupStoppedSyncConfigs(
275
+ (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3
276
+ );
277
+
278
+ expect(result).toEqual({
279
+ stoppedSyncConfigsRemoved: 1,
280
+ bucketDataCollectionsDropped: 1,
281
+ parameterIndexCollectionsDropped: 0,
282
+ bucketStateDocumentsDeleted: 1,
283
+ sourceRecordCollectionsDropped: 0,
284
+ sourceTablesUpdated: 1,
285
+ sourceTablesDeleted: 0
286
+ });
287
+ const sourceTable = await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId });
288
+ expect(sourceTable?.bucket_data_source_ids).toEqual([secondDefinitionId]);
289
+ expect(await collectionExists(db, ownerBucketDataCollection)).toBe(false);
290
+ expect(await collectionExists(db, sourceRecordsCollection)).toBe(true);
291
+
292
+ const activeStorage = (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3;
293
+ await using activeWriter = await activeStorage.createWriter(test_utils.BATCH_OPTIONS);
294
+ const activeTable = (
295
+ await activeWriter.resolveTables({
296
+ connection_id: 1,
297
+ source: sourceDescriptor('todos', { objectId: 'todos-relation' }),
298
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38354')
299
+ })
300
+ ).tables[0];
301
+ await activeWriter.save({
302
+ sourceTable: activeTable,
303
+ tag: storage.SaveOperationTag.UPDATE,
304
+ before: {
305
+ id: 'todo-1'
306
+ },
307
+ after: {
308
+ id: 'todo-1',
309
+ owner_id: 'user-1',
310
+ project_id: 'project-2'
311
+ },
312
+ beforeReplicaId: test_utils.rid('todo-1'),
313
+ afterReplicaId: test_utils.rid('todo-1')
314
+ });
315
+ await activeWriter.commit('3/1');
316
+
317
+ expect(await collectionExists(db, ownerBucketDataCollection)).toBe(false);
318
+ const sourceRecord = await db.sourceRecords(first.replicationStreamId, sourceTableId).findOne({
319
+ _id: test_utils.rid('todo-1') as any
320
+ });
321
+ expect(sourceRecord?.buckets.map((bucket) => bucket.def)).toEqual([secondDefinitionId]);
322
+ });
323
+
324
+ test('drops current_data when a table becomes event-only but is kept by a live event', async () => {
325
+ await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
326
+
327
+ // The first config syncs `todos` data and also evaluates the audit event for its rows.
328
+ const first = await factory.updateSyncRules(
329
+ updateSyncRulesFromYaml(
330
+ `
331
+ config:
332
+ edition: 3
333
+
334
+ streams:
335
+ by_owner:
336
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
337
+
338
+ event_definitions:
339
+ audit:
340
+ payloads:
341
+ - SELECT id, owner_id FROM todos
342
+ `,
343
+ { storageVersion: 3 }
344
+ )
345
+ );
346
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorageV3;
347
+ const db = firstStorage.db as VersionedPowerSyncMongoV3;
348
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
349
+ const resolved = await firstWriter.resolveTables({
350
+ connection_id: 1,
351
+ source: sourceDescriptor('todos', { objectId: 'todos-relation' }),
352
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38370')
353
+ });
354
+ await firstWriter.save({
355
+ sourceTable: resolved.tables[0],
356
+ tag: storage.SaveOperationTag.INSERT,
357
+ after: {
358
+ id: 'todo-1',
359
+ owner_id: 'user-1'
360
+ },
361
+ afterReplicaId: test_utils.rid('todo-1')
362
+ });
363
+ const eventDefinitionId = [...resolved.tables[0].eventDefinitionIds!][0];
364
+ firstWriter.addCustomWriteCheckpoint({
365
+ user_id: 'user-1',
366
+ checkpoint: 1n,
367
+ event_id: eventDefinitionId
368
+ });
369
+ await firstWriter.markAllSnapshotDone('1/1');
370
+ await firstWriter.commit('1/1');
371
+
372
+ const sourceTableId = resolved.tables[0].id as bson.ObjectId;
373
+ const sourceRecordsCollection = db.sourceRecords(first.replicationStreamId, sourceTableId).collectionName;
374
+ const ownerDefinitionId = first.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
375
+ const initialSourceTable = await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId });
376
+ expect(initialSourceTable?.bucket_data_source_ids).toEqual([ownerDefinitionId]);
377
+ expect(initialSourceTable?.event_definition_ids).toHaveLength(1);
378
+ const eventDefinitionIds = initialSourceTable!.event_definition_ids;
379
+ // The prior snapshot is persisted in current_data.
380
+ expect(await collectionExists(db, sourceRecordsCollection)).toBe(true);
381
+
382
+ // The second (active) config drops the `todos` data stream but keeps the audit event, so the
383
+ // `todos` source table becomes event-only while staying alive for the live event.
384
+ const second = await factory.updateSyncRules(
385
+ updateSyncRulesFromYaml(
386
+ `
387
+ config:
388
+ edition: 3
389
+
390
+ streams:
391
+ by_id:
392
+ query: SELECT * FROM other WHERE id = subscription.parameter('id')
393
+
394
+ event_definitions:
395
+ audit:
396
+ payloads:
397
+ - SELECT id, owner_id FROM todos
398
+ `,
399
+ { storageVersion: 3 }
400
+ )
401
+ );
402
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
403
+
404
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
405
+ const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorageV3;
406
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
407
+ await secondWriter.markAllSnapshotDone('2/1');
408
+ await secondWriter.commit('2/1');
409
+
410
+ const result = await cleanupStoppedSyncConfigs(
411
+ (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3
412
+ );
413
+
414
+ expect(result).toEqual({
415
+ stoppedSyncConfigsRemoved: 1,
416
+ bucketDataCollectionsDropped: 1,
417
+ parameterIndexCollectionsDropped: 0,
418
+ bucketStateDocumentsDeleted: 1,
419
+ sourceRecordCollectionsDropped: 1,
420
+ sourceTablesUpdated: 1,
421
+ sourceTablesDeleted: 0
422
+ });
423
+
424
+ // The source table row survives (the live event still needs it), narrowed to empty data and
425
+ // parameter memberships, but its event membership remains and current_data is dropped.
426
+ const sourceTable = await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId });
427
+ expect(sourceTable).not.toBeNull();
428
+ expect(sourceTable?.bucket_data_source_ids).toEqual([]);
429
+ expect(sourceTable?.parameter_lookup_source_ids).toEqual([]);
430
+ expect(sourceTable?.event_definition_ids).toEqual(eventDefinitionIds);
431
+ expect(await collectionExists(db, sourceRecordsCollection)).toBe(false);
432
+ await expect(
433
+ db
434
+ .customCheckpointRequests({
435
+ replicationStreamId: first.replicationStreamId,
436
+ eventId: eventDefinitionId
437
+ })
438
+ .findOne({ user_id: 'user-1' })
439
+ ).resolves.not.toBeNull();
440
+
441
+ const streamDoc = (await db.sync_rules.findOne({ _id: first.replicationStreamId })) as ReplicationStreamDocumentV3;
442
+ expect(streamDoc.sync_configs.map((config) => config.state)).toEqual([storage.SyncRuleState.ACTIVE]);
443
+ });
444
+
445
+ test('cleans up event-only source tables no longer triggered by a live sync config', async () => {
446
+ await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
447
+
448
+ // The first config syncs `todos` and additionally evaluates the audit event for `audit_log`. The
449
+ // `audit_log` table is referenced only by the event trigger, so its source table has only an
450
+ // event membership (an event-only table).
451
+ const first = await factory.updateSyncRules(
452
+ updateSyncRulesFromYaml(
453
+ `
454
+ config:
455
+ edition: 3
456
+
457
+ streams:
458
+ by_owner:
459
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
460
+
461
+ event_definitions:
462
+ audit:
463
+ payloads:
464
+ - SELECT id FROM audit_log
465
+ `,
466
+ { storageVersion: 3 }
467
+ )
468
+ );
469
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorageV3;
470
+ const db = firstStorage.db as VersionedPowerSyncMongoV3;
471
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
472
+
473
+ const todosTable = (
474
+ await firstWriter.resolveTables({
475
+ connection_id: 1,
476
+ source: sourceDescriptor('todos', { objectId: 'todos-relation' }),
477
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38360')
478
+ })
479
+ ).tables[0];
480
+ const auditTable = (
481
+ await firstWriter.resolveTables({
482
+ connection_id: 1,
483
+ source: sourceDescriptor('audit_log', { objectId: 'audit-log-relation' }),
484
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38361')
485
+ })
486
+ ).tables[0];
487
+ await firstWriter.save({
488
+ sourceTable: todosTable,
489
+ tag: storage.SaveOperationTag.INSERT,
490
+ after: {
491
+ id: 'todo-1',
492
+ owner_id: 'user-1'
493
+ },
494
+ afterReplicaId: test_utils.rid('todo-1')
495
+ });
496
+ await firstWriter.save({
497
+ sourceTable: auditTable,
498
+ tag: storage.SaveOperationTag.INSERT,
499
+ after: {
500
+ id: 'audit-1'
501
+ },
502
+ afterReplicaId: test_utils.rid('audit-1')
503
+ });
504
+ const auditEventDefinitionId = [...auditTable.eventDefinitionIds!][0];
505
+ firstWriter.addCustomWriteCheckpoint({
506
+ user_id: 'audit-user',
507
+ checkpoint: 1n,
508
+ event_id: auditEventDefinitionId
509
+ });
510
+ await firstWriter.markAllSnapshotDone('1/1');
511
+ await firstWriter.commit('1/1');
512
+
513
+ const todosTableId = todosTable.id as bson.ObjectId;
514
+ const auditTableId = auditTable.id as bson.ObjectId;
515
+ // The event-only table has empty bucket and parameter memberships, plus its event id.
516
+ const auditSourceTable = await db.sourceTables(first.replicationStreamId).findOne({ _id: auditTableId });
517
+ expect(auditSourceTable?.bucket_data_source_ids).toEqual([]);
518
+ expect(auditSourceTable?.parameter_lookup_source_ids).toEqual([]);
519
+ expect(auditSourceTable?.event_definition_ids).toHaveLength(1);
520
+ const auditRecordsCollection = db.sourceRecords(first.replicationStreamId, auditTableId).collectionName;
521
+
522
+ // The second (active) config keeps the same `by_owner` stream but drops the audit event.
523
+ // The shared bucket definition stays in use, while the unused event id selects this table for
524
+ // membership cleanup.
525
+ const second = await factory.updateSyncRules(
526
+ updateSyncRulesFromYaml(
527
+ `
528
+ config:
529
+ edition: 3
530
+
531
+ streams:
532
+ by_owner:
533
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
534
+ `,
535
+ { storageVersion: 3 }
536
+ )
537
+ );
538
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
539
+
540
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
541
+ const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorageV3;
542
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
543
+ await secondWriter.markAllSnapshotDone('2/1');
544
+ await secondWriter.commit('2/1');
545
+
546
+ const result = await cleanupStoppedSyncConfigs(
547
+ (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3
548
+ );
549
+
550
+ expect(result).toEqual({
551
+ stoppedSyncConfigsRemoved: 1,
552
+ bucketDataCollectionsDropped: 0,
553
+ parameterIndexCollectionsDropped: 0,
554
+ bucketStateDocumentsDeleted: 0,
555
+ sourceRecordCollectionsDropped: 1,
556
+ sourceTablesUpdated: 0,
557
+ sourceTablesDeleted: 1
558
+ });
559
+ // The orphaned event-only table and its source records are removed...
560
+ expect(await db.sourceTables(first.replicationStreamId).countDocuments({ _id: auditTableId })).toBe(0);
561
+ expect(await collectionExists(db, auditRecordsCollection)).toBe(false);
562
+ const auditCheckpointCollection = db.customCheckpointRequests({
563
+ replicationStreamId: first.replicationStreamId,
564
+ eventId: auditEventDefinitionId
565
+ }).collectionName;
566
+ expect(await collectionExists(db, auditCheckpointCollection)).toBe(false);
567
+ // ...while the data table still used by the live config is retained.
568
+ expect(await db.sourceTables(first.replicationStreamId).countDocuments({ _id: todosTableId })).toBe(1);
569
+
570
+ const streamDoc = (await db.sync_rules.findOne({ _id: first.replicationStreamId })) as ReplicationStreamDocumentV3;
571
+ expect(streamDoc.sync_configs.map((config) => config.state)).toEqual([storage.SyncRuleState.ACTIVE]);
572
+ });
573
+
574
+ test('does not recreate dropped parameter indexes from stale source record lookups', async () => {
575
+ await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
576
+
577
+ const first = await factory.updateSyncRules(
578
+ updateSyncRulesFromYaml(
579
+ `
580
+ config:
581
+ edition: 3
582
+
583
+ streams:
584
+ by_role:
585
+ query: |
586
+ SELECT * FROM todos
587
+ WHERE role_id IN (
588
+ SELECT role_id FROM memberships
589
+ WHERE role_id = subscription.parameter('role_id')
590
+ )
591
+ by_org:
592
+ query: |
593
+ SELECT * FROM todos
594
+ WHERE org_id IN (
595
+ SELECT org_id FROM memberships
596
+ WHERE org_id = subscription.parameter('org_id')
597
+ )
598
+ `,
599
+ { storageVersion: 3 }
600
+ )
601
+ );
602
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorageV3;
603
+ const db = firstStorage.db as VersionedPowerSyncMongoV3;
604
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
605
+ const resolved = await firstWriter.resolveTables({
606
+ connection_id: 1,
607
+ source: sourceDescriptor('memberships', { objectId: 'memberships-relation' }),
608
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38355')
609
+ });
610
+ await firstWriter.save({
611
+ sourceTable: resolved.tables[0],
612
+ tag: storage.SaveOperationTag.INSERT,
613
+ after: {
614
+ id: 'membership-1',
615
+ org_id: 'org-1',
616
+ role_id: 'role-1'
617
+ },
618
+ afterReplicaId: test_utils.rid('membership-1')
619
+ });
620
+ await firstWriter.markAllSnapshotDone('1/1');
621
+ await firstWriter.commit('1/1');
622
+
623
+ const sourceTableId = resolved.tables[0].id as bson.ObjectId;
624
+ const sourceRecordsCollection = db.sourceRecords(first.replicationStreamId, sourceTableId).collectionName;
625
+ const [roleIndexId, orgIndexId] = first.syncConfigContent[0].mapping.allParameterIndexIds();
626
+ const orgParameterIndexCollection = db.parameterIndex(first.replicationStreamId, orgIndexId).collectionName;
627
+ expect(await collectionExists(db, orgParameterIndexCollection)).toBe(true);
628
+ expect(
629
+ (await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId }))?.parameter_lookup_source_ids
630
+ ).toEqual([roleIndexId, orgIndexId]);
631
+
632
+ const second = await factory.updateSyncRules(
633
+ updateSyncRulesFromYaml(
634
+ `
635
+ config:
636
+ edition: 3
637
+
638
+ streams:
639
+ by_role:
640
+ query: |
641
+ SELECT * FROM todos
642
+ WHERE role_id IN (
643
+ SELECT role_id FROM memberships
644
+ WHERE role_id = subscription.parameter('role_id')
645
+ )
646
+ `,
647
+ { storageVersion: 3 }
648
+ )
649
+ );
650
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
651
+ const secondIndexId = second.syncConfigContent[1].mapping.allParameterIndexIds()[0];
652
+ expect(secondIndexId).toBe(roleIndexId);
653
+
654
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
655
+ const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorageV3;
656
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
657
+ await secondWriter.markAllSnapshotDone('2/1');
658
+ await secondWriter.commit('2/1');
659
+
660
+ const result = await cleanupStoppedSyncConfigs(
661
+ (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3
662
+ );
663
+ expect(result.parameterIndexCollectionsDropped).toBe(1);
664
+ expect(await collectionExists(db, orgParameterIndexCollection)).toBe(false);
665
+ expect(await collectionExists(db, sourceRecordsCollection)).toBe(true);
666
+
667
+ const activeStorage = (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3;
668
+ await using activeWriter = await activeStorage.createWriter(test_utils.BATCH_OPTIONS);
669
+ const activeTable = (
670
+ await activeWriter.resolveTables({
671
+ connection_id: 1,
672
+ source: sourceDescriptor('memberships', { objectId: 'memberships-relation' }),
673
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38357')
674
+ })
675
+ ).tables[0];
676
+ await activeWriter.save({
677
+ sourceTable: activeTable,
678
+ tag: storage.SaveOperationTag.UPDATE,
679
+ before: {
680
+ id: 'membership-1'
681
+ },
682
+ after: {
683
+ id: 'membership-1',
684
+ org_id: 'org-1',
685
+ role_id: 'role-2'
686
+ },
687
+ beforeReplicaId: test_utils.rid('membership-1'),
688
+ afterReplicaId: test_utils.rid('membership-1')
689
+ });
690
+ await activeWriter.commit('3/1');
691
+
692
+ expect(await collectionExists(db, orgParameterIndexCollection)).toBe(false);
693
+ const sourceRecord = await db.sourceRecords(first.replicationStreamId, sourceTableId).findOne({
694
+ _id: test_utils.rid('membership-1') as any
695
+ });
696
+ expect(sourceRecord?.lookups.map((lookup) => lookup.i)).toEqual([secondIndexId]);
697
+ });
698
+ });