@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
@@ -1,39 +1,70 @@
1
- import { GetIntanceOptions, LEGACY_STORAGE_VERSION, storage } from '@powersync/service-core';
1
+ import {
2
+ GetIntanceOptions,
3
+ LEGACY_STORAGE_VERSION,
4
+ SingleSyncConfigBucketDefinitionMapping,
5
+ storage
6
+ } from '@powersync/service-core';
2
7
 
3
- import { DO_NOT_LOG, ErrorCode, ServiceError } from '@powersync/lib-services-framework';
8
+ import {
9
+ DO_NOT_LOG,
10
+ ErrorCode,
11
+ logger,
12
+ ReplicationAssertionError,
13
+ ServiceError
14
+ } from '@powersync/lib-services-framework';
4
15
  import { v4 as uuid } from 'uuid';
5
16
 
6
17
  import * as lib_mongo from '@powersync/lib-service-mongodb';
7
18
  import { mongo } from '@powersync/lib-service-mongodb';
8
19
 
20
+ import {
21
+ describeIncrementalSyncConfigUpdate,
22
+ formatIncrementalSyncConfigUpdateLog,
23
+ isCompatible
24
+ } from '@powersync/service-core';
9
25
  import { ObjectId } from 'bson';
10
- import { generateSlotName } from '../utils/util.js';
11
- import { BucketDefinitionMapping } from './implementation/BucketDefinitionMapping.js';
26
+ import { DEFAULT_CLEAR_BATCH_THROTTLE_RATE } from '../types/types.js';
27
+ import { generateReplicationStreamName } from '../utils/util.js';
12
28
  import type { MongoSyncBucketStorage } from './implementation/createMongoSyncBucketStorage.js';
13
29
  import { createMongoSyncBucketStorage } from './implementation/createMongoSyncBucketStorage.js';
14
30
  import { PowerSyncMongo } from './implementation/db.js';
15
31
  import { getMongoStorageConfig, StorageConfig, SyncRuleDocumentBase } from './implementation/models.js';
16
32
  import { MongoChecksumOptions } from './implementation/MongoChecksums.js';
17
- import {
18
- MongoPersistedSyncRulesContentV1,
19
- MongoPersistedSyncRulesContentV3
20
- } from './implementation/MongoPersistedSyncRulesContent.js';
21
- import { syncRuleStateUpdatePipeline } from './implementation/SyncRuleStateUpdate.js';
33
+ import { MongoPersistedReplicationStream } from './implementation/MongoPersistedReplicationStream.js';
34
+ import { stopReplicationStreamPipeline } from './implementation/SyncRuleStateUpdate.js';
22
35
  import { SyncRuleDocumentV1 } from './implementation/v1/models.js';
36
+ import { ObjectStorage } from './implementation/v3/object-storage/ObjectStorage.js';
37
+ import { ObjectStorageUsage } from './implementation/v3/object-storage/ObjectStorageUsage.js';
23
38
  import { VersionedPowerSyncMongoV3 } from './implementation/v3/VersionedPowerSyncMongoV3.js';
24
- import { ReplicationStreamDocumentV3, SyncConfigDefinition } from './storage-index.js';
39
+ import { ReplicationStreamDocumentV3, SyncConfigDefinition, SyncRuleConfigStateV3 } from './storage-index.js';
25
40
 
26
41
  export interface MongoBucketStorageOptions {
27
- checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig' | 'mapping'>;
42
+ checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig'>;
43
+ objectStorage?: ObjectStorage;
44
+ inlineThresholdBytes?: number;
45
+ /**
46
+ * Prefix for replication stream name and Postgres logical replication slot name.
47
+ */
48
+ replicationStreamNamePrefix: string;
49
+ readPreference?: mongo.ReadPreference;
50
+ checksumCacheTtlMs?: number;
51
+ clearBatchThrottleRate?: number;
52
+ defaultStorageVersion?: number;
53
+ /**
54
+ * Reuse a compatible active replication stream by appending a new sync config.
55
+ *
56
+ * This currently requires source replication support. MongoDB sources can process multiple
57
+ * sync configs in one replication stream, but other source connectors still expect a single
58
+ * sync config per stream.
59
+ */
60
+ supportsMultipleSyncConfigs?: boolean;
28
61
  }
29
62
 
30
63
  export class MongoBucketStorage extends storage.BucketStorageFactory {
31
64
  [DO_NOT_LOG] = true;
32
65
 
33
66
  private readonly client: mongo.MongoClient;
34
- private readonly session: mongo.ClientSession;
35
- // TODO: This is still Postgres specific and needs to be reworked
36
- public readonly slot_name_prefix: string;
67
+ public readonly replicationStreamNamePrefix: string;
37
68
 
38
69
  private activeStorageCache: MongoSyncBucketStorage | undefined;
39
70
 
@@ -41,51 +72,58 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
41
72
 
42
73
  constructor(
43
74
  db: PowerSyncMongo,
44
- options: {
45
- slot_name_prefix: string;
46
- },
47
- private internalOptions?: MongoBucketStorageOptions
75
+ private options: MongoBucketStorageOptions
48
76
  ) {
49
77
  super();
50
78
  this.client = db.client;
51
79
  this.db = db;
52
- this.session = this.client.startSession();
53
- this.slot_name_prefix = options.slot_name_prefix;
80
+ this.replicationStreamNamePrefix = options.replicationStreamNamePrefix;
54
81
  }
55
82
 
56
83
  async [Symbol.asyncDispose]() {
57
84
  // No-op
58
85
  }
59
86
 
60
- getInstance(syncRules: storage.PersistedSyncRulesContent, options?: GetIntanceOptions): MongoSyncBucketStorage {
61
- let { id, slot_name } = syncRules;
62
- if ((typeof id as any) == 'bigint') {
63
- id = Number(id);
87
+ getInstance(
88
+ replicationStream: storage.PersistedReplicationStream,
89
+ options?: GetIntanceOptions
90
+ ): MongoSyncBucketStorage {
91
+ if (!(replicationStream instanceof MongoPersistedReplicationStream)) {
92
+ throw new Error(`Expected MongoPersistedReplicationStream`);
93
+ }
94
+ let { replicationStreamId, replicationStreamName } = replicationStream;
95
+ if ((typeof replicationStreamId as any) == 'bigint') {
96
+ replicationStreamId = Number(replicationStreamId);
64
97
  }
65
- const storageConfig = (syncRules as MongoPersistedSyncRulesContentV1).getStorageConfig();
66
- const storage = createMongoSyncBucketStorage(
98
+ const storageConfig = replicationStream.getStorageConfig();
99
+ const syncRuleStorage = createMongoSyncBucketStorage(
67
100
  this,
68
- id,
69
- syncRules as MongoPersistedSyncRulesContentV1,
70
- slot_name,
101
+ replicationStreamId,
102
+ replicationStream,
103
+ replicationStreamName,
71
104
  undefined,
72
105
  {
73
- ...this.internalOptions,
74
- storageConfig
106
+ checksumOptions: this.options.checksumOptions,
107
+ readPreference: this.options.readPreference,
108
+ checksumCacheTtlMs: this.options.checksumCacheTtlMs,
109
+ storageConfig,
110
+ objectStorage: this.options.objectStorage,
111
+ inlineThresholdBytes: this.options.inlineThresholdBytes,
112
+ clearBatchThrottleRate: this.options.clearBatchThrottleRate ?? DEFAULT_CLEAR_BATCH_THROTTLE_RATE
75
113
  }
76
114
  );
77
115
  if (!options?.skipLifecycleHooks) {
78
- this.iterateListeners((cb) => cb.syncStorageCreated?.(storage));
116
+ this.iterateListeners((cb) => cb.syncStorageCreated?.(syncRuleStorage));
79
117
  }
80
118
 
81
- storage.registerListener({
119
+ syncRuleStorage.registerListener({
82
120
  batchStarted: (batch) => {
83
121
  batch.registerListener({
84
122
  replicationEvent: (payload) => this.iterateListeners((cb) => cb.replicationEvent?.(payload))
85
123
  });
86
124
  }
87
125
  });
88
- return storage;
126
+ return syncRuleStorage;
89
127
  }
90
128
 
91
129
  async getSystemIdentifier(): Promise<storage.BucketStorageSystemIdentifier> {
@@ -105,228 +143,527 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
105
143
  };
106
144
  }
107
145
 
108
- async restartReplication(sync_rules_group_id: number) {
109
- const next = await this.getNextSyncRulesContent();
110
- const active = await this.getActiveSyncRulesContent();
146
+ async restartReplication(replicationStreamId: number) {
147
+ await this.withTransaction(async (session) => {
148
+ const next = await this.getDeployingSyncConfigInternal(session);
149
+ const active = await this.getActiveSyncConfigInternal(session);
150
+
151
+ if (next != null && next.content.replicationStreamId == replicationStreamId) {
152
+ // We need to redo the "next" replication stream.
153
+ // This creates a new stream, and stops the existing PROCESSING one.
154
+ await this.updateSyncRulesInTransaction(
155
+ next.content.asUpdateOptions({ forceNewReplicationStream: true }),
156
+ session
157
+ );
158
+ const sharedActiveStream = active?.content.replicationStreamId == replicationStreamId;
159
+ if (sharedActiveStream) {
160
+ // The same stream was also used for the ACTIVE config. Transition that to ERRORED
161
+ await this.errorActiveStreamForReplacement(active, session);
162
+ }
163
+ } else if (next == null && active?.content.replicationStreamId == replicationStreamId) {
164
+ // Slot removed for "active" replication stream, while there is no "next" one.
165
+ await this.updateSyncRulesInTransaction(
166
+ active.content.asUpdateOptions({ forceNewReplicationStream: true }),
167
+ session
168
+ );
169
+
170
+ // In this case we keep the old one as active for clients, so that that existing clients
171
+ // can still get the latest data while we replicate the new ones.
172
+ // It will however not replicate anymore.
173
+ await this.errorActiveStreamForReplacement(active, session);
174
+ } else if (next != null && active?.content.replicationStreamId == replicationStreamId) {
175
+ // Already have next replication stream, but need to stop replicating the active one.
176
+ await this.errorActiveStreamForReplacement(active, session);
177
+ } else {
178
+ // replicationStreamId does not match the ACTIVE or PROCESSING streams - no-op.
179
+ }
180
+ });
181
+ await this.db.notifyCheckpoint();
182
+ }
111
183
 
112
- if (next != null && next.id == sync_rules_group_id) {
113
- // We need to redo the "next" replication stream
114
- await this.updateSyncRules(next.asUpdateOptions());
115
- // Pro-actively stop replicating
116
- await this.db.sync_rules.updateOne(
184
+ /**
185
+ * Keep an active config readable after its replication stream is invalidated.
186
+ *
187
+ * Incremental V3 streams can retain stopped historical configs and embed a deploying config.
188
+ * Read-side storage requires exactly one ACTIVE or ERRORED config, so only the active config
189
+ * transitions to ERRORED; every other embedded config remains or becomes STOP.
190
+ */
191
+ private async errorActiveStreamForReplacement(active: storage.ResolvedSyncConfig, session: mongo.ClientSession) {
192
+ const activeSyncConfigId = active.content.syncConfigId;
193
+ if (activeSyncConfigId == null) {
194
+ // V1 documents
195
+ const result = await this.db.sync_rules.updateOne(
117
196
  {
118
- _id: next.id,
119
- state: storage.SyncRuleState.PROCESSING
197
+ _id: active.content.replicationStreamId,
198
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
120
199
  },
121
- syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP)
122
- );
123
- await this.db.notifyCheckpoint();
124
- } else if (next == null && active?.id == sync_rules_group_id) {
125
- // Slot removed for "active" replication stream, while there is no "next" one.
126
- await this.updateSyncRules(active.asUpdateOptions());
127
-
128
- // In this case we keep the old one as active for clients, so that that existing clients
129
- // can still get the latest data while we replicate the new ones.
130
- // It will however not replicate anymore.
131
-
132
- await this.db.sync_rules.updateOne(
133
200
  {
134
- _id: active.id,
135
- state: storage.SyncRuleState.ACTIVE
201
+ $set: {
202
+ state: storage.SyncRuleState.ERRORED
203
+ }
136
204
  },
137
- syncRuleStateUpdatePipeline(storage.SyncRuleState.ERRORED)
205
+ { session }
138
206
  );
139
- await this.db.notifyCheckpoint();
140
- } else if (next != null && active?.id == sync_rules_group_id) {
141
- // Already have next replication stream, but need to stop replicating the active one.
207
+ if (result.matchedCount != 1) {
208
+ // This should generally not happen, since the entire process runs in a transaction.
209
+ // The filters just double-check that the state is what we expect, and fails hard otherwise.
210
+ throw new ReplicationAssertionError(
211
+ `Active replication stream ${active.content.replicationStreamId} changed during restart`
212
+ );
213
+ }
214
+ return;
215
+ }
142
216
 
143
- await this.db.sync_rules.updateOne(
144
- {
145
- _id: active.id,
146
- state: storage.SyncRuleState.ACTIVE
147
- },
148
- syncRuleStateUpdatePipeline(storage.SyncRuleState.ERRORED)
217
+ const activeConfigObjectId = new ObjectId(activeSyncConfigId);
218
+ const result = await this.db.sync_rules.updateOne(
219
+ {
220
+ _id: active.content.replicationStreamId,
221
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] },
222
+ sync_configs: {
223
+ // Confirm that this is still the ACTIVE/ERRORED one.
224
+ $elemMatch: {
225
+ _id: activeConfigObjectId,
226
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
227
+ }
228
+ }
229
+ },
230
+ {
231
+ $set: {
232
+ state: storage.SyncRuleState.ERRORED,
233
+ // There must be exactly one config with an ERRORED state: This will be used to serve
234
+ // clients until there is a new ACTIVE one.
235
+ 'sync_configs.$[activeConfig].state': storage.SyncRuleState.ERRORED,
236
+ // PROCESSING configs must stop. Historical STOP configs remain unchanged.
237
+ 'sync_configs.$[processingConfig].state': storage.SyncRuleState.STOP
238
+ }
239
+ },
240
+ {
241
+ session,
242
+ arrayFilters: [
243
+ {
244
+ 'activeConfig._id': activeConfigObjectId,
245
+ 'activeConfig.state': { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
246
+ },
247
+ {
248
+ 'processingConfig._id': { $ne: activeConfigObjectId },
249
+ 'processingConfig.state': storage.SyncRuleState.PROCESSING
250
+ }
251
+ ]
252
+ }
253
+ );
254
+ if (result.matchedCount != 1) {
255
+ // This should generally not happen, since the entire process runs in a transaction.
256
+ // The filters just double-check that the state is what we expect, and fails hard otherwise.
257
+ throw new ReplicationAssertionError(
258
+ `Active replication stream ${active.content.replicationStreamId} changed during restart`
149
259
  );
150
- await this.db.notifyCheckpoint();
151
260
  }
152
261
  }
153
262
 
154
- private async updateSyncRulesV3(
263
+ private async updateSyncRulesInTransaction(options: storage.UpdateSyncRulesOptions, session: mongo.ClientSession) {
264
+ const storageVersion =
265
+ options.storageVersion ??
266
+ options.config.parsed.config.storageVersion ??
267
+ this.options.defaultStorageVersion ??
268
+ storage.CURRENT_STORAGE_VERSION;
269
+ const storageConfig = getMongoStorageConfig(storageVersion);
270
+ if (storageConfig.incrementalReprocessing) {
271
+ return this.updateSyncConfigV3InTransaction(options, storageVersion, storageConfig, session);
272
+ }
273
+ return this.updateSyncConfigV1InTransaction(options, storageVersion, session);
274
+ }
275
+
276
+ /**
277
+ * Update to a V3 sync config.
278
+ *
279
+ * Does support cases where existing replication streams are on V1.
280
+ */
281
+ private async updateSyncConfigV3InTransaction(
155
282
  options: storage.UpdateSyncRulesOptions,
156
283
  storageVersion: number,
157
- storageConfig: StorageConfig
158
- ): Promise<MongoPersistedSyncRulesContentV3> {
159
- let rules: MongoPersistedSyncRulesContentV3 | undefined = undefined;
284
+ storageConfig: StorageConfig,
285
+ session: mongo.ClientSession
286
+ ): Promise<MongoPersistedReplicationStream> {
160
287
  const versioned = this.db.versioned(storageConfig) as VersionedPowerSyncMongoV3;
161
288
 
162
- const session = this.session;
289
+ const active = await this.db.sync_rules.findOne<ReplicationStreamDocumentV3>(
290
+ {
291
+ state: storage.SyncRuleState.ACTIVE,
292
+ storage_version: storageVersion
293
+ },
294
+ { session, sort: { _id: -1 }, limit: 1 }
295
+ );
296
+ if (active != null) {
297
+ const existingConfigDocs = await this.loadSyncConfigDefinitions(versioned, active, session);
298
+
299
+ if (
300
+ !options.forceNewReplicationStream &&
301
+ this.options.supportsMultipleSyncConfigs &&
302
+ isCompatible(
303
+ existingConfigDocs.map((d) => d.serialized_plan ?? null),
304
+ options.config,
305
+ logger
306
+ )
307
+ ) {
308
+ logger.info(`Using incremental reprocessing`);
309
+ await this.stopExistingProcessingWork(session);
310
+ return await this.appendSyncConfigToStream({
311
+ versioned,
312
+ existing: active,
313
+ existingConfigDocs,
314
+ options,
315
+ storageVersion,
316
+ session
317
+ });
318
+ }
319
+ }
320
+
321
+ await this.stopExistingProcessingWork(session);
322
+
323
+ const id_doc = await this.db.op_id_sequence.findOneAndUpdate(
324
+ {
325
+ _id: 'sync_rules'
326
+ },
327
+ {
328
+ $inc: {
329
+ op_id: 1n
330
+ }
331
+ },
332
+ {
333
+ upsert: true,
334
+ returnDocument: 'after',
335
+ session
336
+ }
337
+ );
338
+
339
+ const id = Number(id_doc!.op_id);
340
+ const replicationStreamName = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
341
+
342
+ const mapping =
343
+ options.config.plan == null
344
+ ? // For legacy sync rules and streams, use the parsed config directly to create a mapping
345
+ SingleSyncConfigBucketDefinitionMapping.fromParsedSyncConfig(options.config.parsed)
346
+ : // For new sync streams, always use the serialized version
347
+ SingleSyncConfigBucketDefinitionMapping.constructIncrementalMappingFromSerializedPlans(
348
+ [],
349
+ options.config.plan.plan,
350
+ []
351
+ );
352
+
353
+ const syncConfigDoc: SyncConfigDefinition = {
354
+ _id: new ObjectId(),
355
+ replication_stream_id: id,
356
+ created_at: new Date(),
357
+ storage_version: storageVersion,
358
+ content: options.config.yaml,
359
+ version_label: options.version_label,
360
+ serialized_plan: options.config.plan,
361
+ rule_mapping: mapping.serialize()
362
+ };
363
+ await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
364
+
365
+ const doc: ReplicationStreamDocumentV3 = {
366
+ _id: id,
367
+ storage_version: storageVersion,
368
+ sync_configs: [
369
+ {
370
+ _id: syncConfigDoc._id,
371
+ state: storage.SyncRuleState.PROCESSING,
372
+ last_checkpoint: null,
373
+ last_checkpoint_lsn: null,
374
+ no_checkpoint_before: null,
375
+ snapshot_done: false
376
+ }
377
+ ],
378
+ snapshot_lsn: undefined,
379
+ state: storage.SyncRuleState.PROCESSING,
380
+ slot_name: replicationStreamName,
381
+ last_checkpoint_ts: null,
382
+ last_fatal_error: null,
383
+ last_fatal_error_ts: null,
384
+ last_keepalive_ts: null
385
+ };
386
+
387
+ await this.db.sync_rules.insertOne(doc, { session });
388
+ const rules = new MongoPersistedReplicationStream(this.db, doc, [syncConfigDoc]);
389
+ if (options.lock) {
390
+ // We only lock when creating a new stream - otherwise we'll likely get lock contention
391
+ // from an existing job on the stream, which would fail the entire transaction.
392
+ // The lock is persisted on rules.current_lock
393
+ await rules.lock(session);
394
+ }
395
+ return rules;
396
+ }
397
+
398
+ private async loadSyncConfigDefinitions(
399
+ versioned: VersionedPowerSyncMongoV3,
400
+ existing: ReplicationStreamDocumentV3,
401
+ session: mongo.ClientSession
402
+ ) {
403
+ const activeConfigIds = existing.sync_configs
404
+ .filter((config) => config.state == storage.SyncRuleState.ACTIVE)
405
+ .map((config) => config._id);
163
406
 
164
- await session.withTransaction(async () => {
165
- // Only have a single replication stream with PROCESSING.
166
- await this.db.sync_rules.updateMany(
407
+ return versioned.syncConfigDefinitions
408
+ .find(
167
409
  {
168
- state: storage.SyncRuleState.PROCESSING
410
+ _id: { $in: activeConfigIds }
169
411
  },
170
- syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
171
412
  { session }
172
- );
413
+ )
414
+ .toArray();
415
+ }
173
416
 
174
- const id_doc = await this.db.op_id_sequence.findOneAndUpdate(
417
+ /**
418
+ * Load _all_ definition mappings for a replication stream - used as a base to generate new ids.
419
+ */
420
+ private async loadHistoricalSyncConfigRuleMappings(
421
+ versioned: VersionedPowerSyncMongoV3,
422
+ replicationStreamId: number,
423
+ session: mongo.ClientSession
424
+ ) {
425
+ return versioned.syncConfigDefinitions
426
+ .find(
175
427
  {
176
- _id: 'sync_rules'
428
+ replication_stream_id: replicationStreamId
177
429
  },
178
430
  {
179
- $inc: {
180
- op_id: 1n
431
+ session,
432
+ projection: {
433
+ rule_mapping: 1
181
434
  }
435
+ }
436
+ )
437
+ .toArray();
438
+ }
439
+
440
+ private logIncrementalDefinitionChanges(changes: ReturnType<typeof describeIncrementalSyncConfigUpdate>) {
441
+ logger.info(`Incremental reprocessing sync config update:\n${formatIncrementalSyncConfigUpdateLog(changes)}`);
442
+ }
443
+
444
+ private async appendSyncConfigToStream(options: {
445
+ versioned: VersionedPowerSyncMongoV3;
446
+ existing: ReplicationStreamDocumentV3;
447
+ existingConfigDocs: SyncConfigDefinition[];
448
+ options: storage.UpdateSyncRulesOptions;
449
+ storageVersion: number;
450
+ session: mongo.ClientSession;
451
+ }): Promise<MongoPersistedReplicationStream> {
452
+ const { versioned, existing, existingConfigDocs, options: updateOptions, storageVersion, session } = options;
453
+ const compatibleConfigs = existingConfigDocs.map((doc) => ({
454
+ plan: doc.serialized_plan!.plan,
455
+ mapping: SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(doc.rule_mapping)
456
+ }));
457
+ const historicalRuleMappings = await this.loadHistoricalSyncConfigRuleMappings(versioned, existing._id, session);
458
+ const reservedMappings = historicalRuleMappings.map((doc) =>
459
+ SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(doc.rule_mapping)
460
+ );
461
+ const mappingResult = SingleSyncConfigBucketDefinitionMapping.constructIncrementalMappingWithChanges(
462
+ compatibleConfigs,
463
+ updateOptions.config.plan!.plan,
464
+ reservedMappings
465
+ );
466
+ const mapping = mappingResult.mapping;
467
+ this.logIncrementalDefinitionChanges(
468
+ describeIncrementalSyncConfigUpdate({
469
+ activeMappings: existingConfigDocs.map((doc) =>
470
+ SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(doc.rule_mapping)
471
+ ),
472
+ newMapping: mapping,
473
+ newSyncConfig: updateOptions.config.parsed,
474
+ mappingChanges: mappingResult.changes
475
+ })
476
+ );
477
+
478
+ const syncConfigDoc: SyncConfigDefinition = {
479
+ _id: new ObjectId(),
480
+ replication_stream_id: existing._id,
481
+ created_at: new Date(),
482
+ storage_version: storageVersion,
483
+ content: updateOptions.config.yaml,
484
+ version_label: updateOptions.version_label,
485
+ serialized_plan: updateOptions.config.plan,
486
+ rule_mapping: mapping.serialize()
487
+ };
488
+ await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
489
+ const syncConfigState: SyncRuleConfigStateV3 = {
490
+ _id: syncConfigDoc._id,
491
+ state: storage.SyncRuleState.PROCESSING,
492
+ last_checkpoint: null,
493
+ last_checkpoint_lsn: null,
494
+ no_checkpoint_before: null,
495
+ snapshot_done: false
496
+ };
497
+
498
+ await this.db.sync_rules.updateOne(
499
+ { _id: existing._id },
500
+ {
501
+ $push: {
502
+ sync_configs: syncConfigState
182
503
  },
183
- {
184
- upsert: true,
185
- returnDocument: 'after',
186
- session
504
+ $set: {
505
+ last_fatal_error: null,
506
+ last_fatal_error_ts: null
187
507
  }
188
- );
508
+ },
509
+ { session }
510
+ );
511
+ const syncConfigStates = [
512
+ ...existing.sync_configs.filter((config) => config.state == storage.SyncRuleState.ACTIVE),
513
+ syncConfigState
514
+ ];
515
+ const stream = new MongoPersistedReplicationStream(
516
+ this.db,
517
+ {
518
+ ...existing,
519
+ sync_configs: syncConfigStates
520
+ },
521
+ [...existingConfigDocs, syncConfigDoc]
522
+ );
523
+ // The stream already exists, so an active replication job may already hold the stream lock.
524
+ // Deployment only persists the appended sync config; replication job locking is handled by
525
+ // the replicator.
526
+ return stream;
527
+ }
189
528
 
190
- const id = Number(id_doc!.op_id);
191
- const slot_name = generateSlotName(this.slot_name_prefix, id);
192
-
193
- const mapping = BucketDefinitionMapping.fromParsedSyncRules(options.config.parsed);
194
-
195
- const syncConfigDoc: SyncConfigDefinition = {
196
- _id: new ObjectId(),
197
- replication_stream_id: id,
198
- created_at: new Date(),
199
- storage_version: storageVersion,
200
- content: options.config.yaml,
201
- serialized_plan: options.config.plan,
202
- rule_mapping: mapping.serialize()
203
- };
204
- await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
205
-
206
- const doc: ReplicationStreamDocumentV3 = {
207
- _id: id,
208
- storage_version: storageVersion,
209
- sync_configs: [
210
- {
211
- _id: syncConfigDoc._id,
212
- state: storage.SyncRuleState.PROCESSING,
213
- keepalive_op: null,
214
- last_checkpoint: null,
215
- last_checkpoint_lsn: null,
216
- no_checkpoint_before: null,
217
- snapshot_done: false
218
- }
219
- ],
220
- snapshot_lsn: undefined,
221
- state: storage.SyncRuleState.PROCESSING,
222
- slot_name: slot_name,
223
- last_checkpoint_ts: null,
224
- last_fatal_error: null,
225
- last_fatal_error_ts: null,
226
- last_keepalive_ts: null
227
- };
228
-
229
- await this.db.sync_rules.insertOne(doc, { session });
230
- await this.db.notifyCheckpoint();
231
- rules = new MongoPersistedSyncRulesContentV3(this.db, doc, syncConfigDoc);
232
- if (options.lock) {
233
- // The lock is persisted on rules.current_lock
234
- await rules.lock(session);
235
- }
529
+ async updateSyncRules(options: storage.UpdateSyncRulesOptions): Promise<MongoPersistedReplicationStream> {
530
+ let rules: MongoPersistedReplicationStream | undefined;
531
+ await this.withTransaction(async (session) => {
532
+ rules = await this.updateSyncRulesInTransaction(options, session);
236
533
  });
237
-
534
+ await this.db.notifyCheckpoint();
238
535
  return rules!;
239
536
  }
240
537
 
241
- async updateSyncRules(
242
- options: storage.UpdateSyncRulesOptions
243
- ): Promise<MongoPersistedSyncRulesContentV1 | MongoPersistedSyncRulesContentV3> {
244
- const storageVersion =
245
- options.storageVersion ?? options.config.parsed.config.storageVersion ?? storage.CURRENT_STORAGE_VERSION;
538
+ private async withTransaction<T>(callback: (session: mongo.ClientSession) => Promise<T>): Promise<T> {
539
+ return this.client.withSession((session) => session.withTransaction(callback));
540
+ }
246
541
 
247
- const storageConfig = getMongoStorageConfig(storageVersion);
248
- if (storageConfig.incrementalReprocessing) {
249
- return this.updateSyncRulesV3(options, storageVersion, storageConfig);
250
- }
542
+ /**
543
+ * Update to a V1 sync config.
544
+ *
545
+ * Does support cases where existing replication streams are on V3.
546
+ */
547
+ private async updateSyncConfigV1InTransaction(
548
+ options: storage.UpdateSyncRulesOptions,
549
+ storageVersion: number,
550
+ session: mongo.ClientSession
551
+ ): Promise<MongoPersistedReplicationStream> {
552
+ await this.stopExistingProcessingWork(session);
251
553
 
252
- let rules: MongoPersistedSyncRulesContentV1 | undefined = undefined;
554
+ const id_doc = await this.db.op_id_sequence.findOneAndUpdate(
555
+ {
556
+ _id: 'sync_rules'
557
+ },
558
+ {
559
+ $inc: {
560
+ op_id: 1n
561
+ }
562
+ },
563
+ {
564
+ upsert: true,
565
+ returnDocument: 'after',
566
+ session
567
+ }
568
+ );
253
569
 
254
- const session = this.session;
570
+ const id = Number(id_doc!.op_id);
571
+ const slot_name = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
572
+
573
+ // All V1 replication streams share both the `main` op id sequence and the `bucket_parameters`
574
+ // collection, so every parameter entry this stream writes gets an op id above the current
575
+ // head. Seeding the parameter compaction cursor with that head keeps the stream's first
576
+ // compaction from scanning other streams' history, which would otherwise be repeated for
577
+ // every new deployment. A concurrent replication flush can only advance the head after this
578
+ // read, which makes the seed conservative, never too high.
579
+ const opSequence = await this.db.op_id_sequence.findOne({ _id: 'main' }, { session });
580
+
581
+ const doc: SyncRuleDocumentV1 = {
582
+ _id: id,
583
+ storage_version: storageVersion,
584
+ content: options.config.yaml,
585
+ version_label: options.version_label,
586
+ serialized_plan: options.config.plan,
587
+ last_checkpoint: null,
588
+ last_checkpoint_lsn: null,
589
+ no_checkpoint_before: null,
590
+ keepalive_op: null,
591
+ snapshot_done: false,
592
+ snapshot_lsn: undefined,
593
+ state: storage.SyncRuleState.PROCESSING,
594
+ slot_name: slot_name,
595
+ last_checkpoint_ts: null,
596
+ last_fatal_error: null,
597
+ last_fatal_error_ts: null,
598
+ last_keepalive_ts: null,
599
+ parameter_compaction: { compacted_before: opSequence?.op_id ?? 0n }
600
+ };
255
601
 
256
- await session.withTransaction(async () => {
257
- // Only have a single replication stream with PROCESSING.
258
- await this.db.sync_rules.updateMany(
259
- {
260
- state: storage.SyncRuleState.PROCESSING
261
- },
262
- syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
263
- { session }
264
- );
602
+ await this.db.sync_rules.insertOne(doc, { session });
603
+ const rules = new MongoPersistedReplicationStream(this.db, doc);
604
+ if (options.lock) {
605
+ // The lock is persisted on rules.current_lock
606
+ await rules.lock(session);
607
+ }
608
+ return rules;
609
+ }
265
610
 
266
- const id_doc = await this.db.op_id_sequence.findOneAndUpdate(
267
- {
268
- _id: 'sync_rules'
269
- },
270
- {
271
- $inc: {
272
- op_id: 1n
273
- }
274
- },
275
- {
276
- upsert: true,
277
- returnDocument: 'after',
278
- session
611
+ /**
612
+ * Stop top-level and embedded processing work before creating a replacement stream.
613
+ *
614
+ * Use in the same transaction that the replacement stream is created.
615
+ */
616
+ private async stopExistingProcessingWork(session: mongo.ClientSession) {
617
+ await this.db.sync_rules.updateMany(
618
+ {
619
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] },
620
+ 'sync_configs.state': storage.SyncRuleState.PROCESSING
621
+ },
622
+ {
623
+ $set: {
624
+ 'sync_configs.$[config].state': storage.SyncRuleState.STOP
279
625
  }
280
- );
281
-
282
- const id = Number(id_doc!.op_id);
283
- const slot_name = generateSlotName(this.slot_name_prefix, id);
284
-
285
- const doc: SyncRuleDocumentV1 = {
286
- _id: id,
287
- storage_version: storageVersion,
288
- content: options.config.yaml,
289
- serialized_plan: options.config.plan,
290
- last_checkpoint: null,
291
- last_checkpoint_lsn: null,
292
- no_checkpoint_before: null,
293
- keepalive_op: null,
294
- snapshot_done: false,
295
- snapshot_lsn: undefined,
296
- state: storage.SyncRuleState.PROCESSING,
297
- slot_name: slot_name,
298
- last_checkpoint_ts: null,
299
- last_fatal_error: null,
300
- last_fatal_error_ts: null,
301
- last_keepalive_ts: null
302
- };
303
-
304
- await this.db.sync_rules.insertOne(doc, { session });
305
- await this.db.notifyCheckpoint();
306
- rules = new MongoPersistedSyncRulesContentV1(this.db, doc);
307
- if (options.lock) {
308
- // The lock is persisted on rules.current_lock
309
- await rules.lock(session);
626
+ },
627
+ {
628
+ session,
629
+ arrayFilters: [{ 'config.state': storage.SyncRuleState.PROCESSING }]
310
630
  }
311
- });
631
+ );
312
632
 
313
- return rules!;
633
+ await this.db.sync_rules.updateMany(
634
+ {
635
+ state: storage.SyncRuleState.PROCESSING
636
+ },
637
+ stopReplicationStreamPipeline(),
638
+ { session }
639
+ );
314
640
  }
315
641
 
316
- async getActiveSyncRulesContent(): Promise<
317
- MongoPersistedSyncRulesContentV1 | MongoPersistedSyncRulesContentV3 | null
318
- > {
642
+ async getActiveSyncConfig(): Promise<storage.ResolvedSyncConfig | null> {
643
+ return this.getActiveSyncConfigInternal();
644
+ }
645
+
646
+ private async getActiveSyncConfigInternal(session?: mongo.ClientSession): Promise<storage.ResolvedSyncConfig | null> {
319
647
  const doc = await this.db.sync_rules.findOne(
320
648
  {
321
649
  state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
322
650
  },
323
- { sort: { _id: -1 }, limit: 1 }
651
+ { session, sort: { _id: -1 }, limit: 1 }
324
652
  );
325
653
 
326
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED]);
654
+ return this.resolvedSyncConfigFromDoc(
655
+ doc,
656
+ [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED],
657
+ { cacheActiveStorage: session == null },
658
+ session
659
+ );
327
660
  }
328
661
 
329
- private async getSyncRulesContent(doc: SyncRuleDocumentBase | null, stateFilter: storage.SyncRuleState[]) {
662
+ private async replicationStreamFromDoc(
663
+ doc: SyncRuleDocumentBase | null,
664
+ stateFilter: storage.SyncRuleState[],
665
+ session?: mongo.ClientSession
666
+ ) {
330
667
  if (doc == null) {
331
668
  return null;
332
669
  }
@@ -334,8 +671,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
334
671
 
335
672
  if (storageConfig.incrementalReprocessing) {
336
673
  const v3 = doc as ReplicationStreamDocumentV3;
337
- const active = v3.sync_configs.find((c) => stateFilter.includes(c.state));
338
- if (active == null) {
674
+ const matching = v3.sync_configs.filter((c) => stateFilter.includes(c.state));
675
+ if (matching.length == 0) {
339
676
  return null;
340
677
  }
341
678
 
@@ -343,28 +680,42 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
343
680
  // that checks for active replication streams.
344
681
  // It is not a major bottleneck though, since it only runs once every couple of seconds at most.
345
682
  const db = this.db.versioned(storageConfig) as VersionedPowerSyncMongoV3;
346
- const syncConfigDoc = await db.syncConfigDefinitions.findOne({ _id: active._id });
347
- if (syncConfigDoc == null) {
683
+ const syncConfigDocs = await db.syncConfigDefinitions
684
+ .find(
685
+ {
686
+ _id: { $in: matching.map((config) => config._id) }
687
+ },
688
+ { session }
689
+ )
690
+ .toArray();
691
+
692
+ if (syncConfigDocs.length == 0) {
348
693
  return null;
349
694
  }
350
- return new MongoPersistedSyncRulesContentV3(this.db, v3, syncConfigDoc);
695
+ return new MongoPersistedReplicationStream(this.db, v3, syncConfigDocs);
351
696
  }
352
697
 
353
- return new MongoPersistedSyncRulesContentV1(this.db, doc as SyncRuleDocumentV1);
698
+ return new MongoPersistedReplicationStream(this.db, doc as SyncRuleDocumentV1);
354
699
  }
355
700
 
356
- async getNextSyncRulesContent(): Promise<MongoPersistedSyncRulesContentV1 | MongoPersistedSyncRulesContentV3 | null> {
701
+ async getDeployingSyncConfig(): Promise<storage.ResolvedSyncConfig | null> {
702
+ return this.getDeployingSyncConfigInternal();
703
+ }
704
+
705
+ private async getDeployingSyncConfigInternal(
706
+ session?: mongo.ClientSession
707
+ ): Promise<storage.ResolvedSyncConfig | null> {
357
708
  const doc = await this.db.sync_rules.findOne(
358
709
  {
359
- state: storage.SyncRuleState.PROCESSING
710
+ $or: [{ state: storage.SyncRuleState.PROCESSING }, { 'sync_configs.state': storage.SyncRuleState.PROCESSING }]
360
711
  },
361
- { sort: { _id: -1 }, limit: 1 }
712
+ { session, sort: { _id: -1 }, limit: 1 }
362
713
  );
363
714
 
364
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.PROCESSING]);
715
+ return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.PROCESSING], {}, session);
365
716
  }
366
717
 
367
- async getReplicatingSyncRules(): Promise<storage.PersistedSyncRulesContent[]> {
718
+ async getReplicatingReplicationStreams(): Promise<storage.PersistedReplicationStream[]> {
368
719
  const docs = await this.db.sync_rules
369
720
  .find({
370
721
  state: { $in: [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE] }
@@ -374,13 +725,13 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
374
725
  return (
375
726
  await Promise.all(
376
727
  docs.map((doc) => {
377
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE]);
728
+ return this.replicationStreamFromDoc(doc, [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE]);
378
729
  })
379
730
  )
380
731
  ).filter((r) => r != null);
381
732
  }
382
733
 
383
- async getStoppedSyncRules(): Promise<storage.PersistedSyncRulesContent[]> {
734
+ async getStoppedReplicationStreams(): Promise<storage.PersistedReplicationStream[]> {
384
735
  const docs = await this.db.sync_rules
385
736
  .find({
386
737
  state: storage.SyncRuleState.STOP
@@ -390,33 +741,52 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
390
741
  return (
391
742
  await Promise.all(
392
743
  docs.map((doc) => {
393
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.STOP]);
744
+ return this.replicationStreamFromDoc(doc, [storage.SyncRuleState.STOP]);
394
745
  })
395
746
  )
396
747
  ).filter((d) => d != null);
397
748
  }
398
749
 
399
- async getActiveStorage(): Promise<MongoSyncBucketStorage | null> {
400
- const content = await this.getActiveSyncRulesContent();
401
- if (content == null) {
750
+ private async resolvedSyncConfigFromDoc(
751
+ doc: SyncRuleDocumentBase | null,
752
+ stateFilter: storage.SyncRuleState[],
753
+ options: { cacheActiveStorage?: boolean } = {},
754
+ session?: mongo.ClientSession
755
+ ): Promise<storage.ResolvedSyncConfig | null> {
756
+ const stream = await this.replicationStreamFromDoc(doc, stateFilter, session);
757
+ if (stream == null) {
402
758
  return null;
403
759
  }
404
760
 
405
- // It is important that this instance is cached.
406
- // Not for the instance construction itself, but to ensure that internal caches on the instance
407
- // are re-used properly.
408
- if (this.activeStorageCache?.group_id == content.id) {
409
- return this.activeStorageCache;
410
- } else {
411
- const instance = this.getInstance(content);
412
- this.activeStorageCache = instance;
413
- return instance;
414
- }
761
+ const content = stream.syncConfigContent[0];
762
+ const thisFactory = this;
763
+
764
+ return {
765
+ content,
766
+ replicationStream: stream,
767
+ get storage() {
768
+ // It is important that this instance is cached.
769
+ // Not for the instance construction itself, but to ensure that internal caches on the instance
770
+ // are re-used properly.
771
+ if (
772
+ options.cacheActiveStorage &&
773
+ thisFactory.activeStorageCache?.replicationStream.replicationJobId == stream.replicationJobId
774
+ ) {
775
+ return thisFactory.activeStorageCache;
776
+ }
777
+
778
+ const instance = thisFactory.getInstance(stream);
779
+ if (options.cacheActiveStorage) {
780
+ thisFactory.activeStorageCache = instance;
781
+ }
782
+ return instance;
783
+ }
784
+ };
415
785
  }
416
786
 
417
787
  async getStorageMetrics(): Promise<storage.StorageMetrics> {
418
788
  const ignoreNotExisting = (e: unknown) => {
419
- if (lib_mongo.isMongoServerError(e) && e.codeName == 'NamespaceNotFound') {
789
+ if (lib_mongo.isMongoNamespaceNotFoundError(e)) {
420
790
  // Collection doesn't exist - return 0
421
791
  return [{ storageStats: { size: 0 } }];
422
792
  } else {
@@ -424,9 +794,6 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
424
794
  }
425
795
  };
426
796
 
427
- // For now, we get storage metrics over all v1 and v3 collections.
428
- // In the future, we may split these metrics to report separately for active replication streams versus processing streams.
429
-
430
797
  const aggregateStaticCollection = async <T extends mongo.Document>(collection: mongo.Collection<T>) => {
431
798
  // We check whether the collection exists before getting the statistics. This avoids repeated
432
799
  // errors in the MongoDB logs if the collection hasn't been created yet.
@@ -450,8 +817,9 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
450
817
  };
451
818
 
452
819
  const operations_aggregate = await aggregateStaticCollection(this.db.bucket_data);
820
+ const v3OperationCollections = await this.db.listBucketDataCollectionsV3();
453
821
  const v3_operation_aggregates = await Promise.all(
454
- (await this.db.listBucketDataCollectionsV3()).map((collection) =>
822
+ v3OperationCollections.map((collection) =>
455
823
  collection
456
824
  .aggregate([
457
825
  {
@@ -467,8 +835,9 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
467
835
 
468
836
  const parameters_aggregate = await aggregateStaticCollection(this.db.bucket_parameters);
469
837
 
838
+ const v3ParameterCollections = await this.db.listAllParameterIndexCollectionsV3();
470
839
  const v3_parameter_aggregates = await Promise.all(
471
- (await this.db.listAllParameterIndexCollectionsV3()).map((collection) =>
840
+ v3ParameterCollections.map((collection) =>
472
841
  collection
473
842
  .aggregate([
474
843
  {
@@ -484,8 +853,9 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
484
853
 
485
854
  const v1_source_record_aggregate = await aggregateStaticCollection(this.db.current_data);
486
855
 
856
+ const v3SourceRecordCollections = await this.db.listAllSourceRecordCollectionsV3();
487
857
  const source_record_aggregates = await Promise.all(
488
- (await this.db.listAllSourceRecordCollectionsV3()).map((collection) =>
858
+ v3SourceRecordCollections.map((collection) =>
489
859
  collection
490
860
  .aggregate([
491
861
  {
@@ -498,6 +868,120 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
498
868
  .catch(ignoreNotExisting)
499
869
  )
500
870
  );
871
+
872
+ const v3StorageConfig = getMongoStorageConfig(storage.STORAGE_VERSION_3) as StorageConfig & {
873
+ incrementalReprocessing: true;
874
+ };
875
+ const v3Db = this.db.versioned(v3StorageConfig) as VersionedPowerSyncMongoV3;
876
+ const objectStorageDefinitionUsage = await ObjectStorageUsage.readAllDefinitionUsage(v3Db);
877
+
878
+ const v3StreamDocs = (await this.db.sync_rules
879
+ .find({ storage_version: { $gte: storage.STORAGE_VERSION_3 } }, { projection: { _id: 1, sync_configs: 1 } })
880
+ .toArray()) as unknown as Pick<ReplicationStreamDocumentV3, '_id' | 'sync_configs'>[];
881
+
882
+ const collectionSizes = new Map<string, number>();
883
+ const addCollectionSize = (
884
+ collection: { collectionName: string },
885
+ aggregate: { storageStats?: { size?: number | bigint } }[],
886
+ prefix: string,
887
+ sizes: Map<string, number>
888
+ ) => {
889
+ const match = collection.collectionName.match(new RegExp(`^${prefix}(\\d+)_(.+)$`));
890
+ if (match == null) {
891
+ return;
892
+ }
893
+ sizes.set(`${prefix}${match[1]}:${match[2]}`, Number(aggregate[0]?.storageStats?.size ?? 0));
894
+ };
895
+
896
+ v3OperationCollections.forEach((collection, index) =>
897
+ addCollectionSize(collection, v3_operation_aggregates[index], 'bucket_data_', collectionSizes)
898
+ );
899
+ v3ParameterCollections.forEach((collection, index) =>
900
+ addCollectionSize(collection, v3_parameter_aggregates[index], 'parameter_index_', collectionSizes)
901
+ );
902
+ v3SourceRecordCollections.forEach((collection, index) =>
903
+ addCollectionSize(collection, source_record_aggregates[index], 'source_records_', collectionSizes)
904
+ );
905
+
906
+ const syncConfigIds = v3StreamDocs.flatMap((stream) => (stream.sync_configs ?? []).map((config) => config._id));
907
+ const syncConfigDefinitions =
908
+ syncConfigIds.length == 0
909
+ ? []
910
+ : await v3Db.syncConfigDefinitions
911
+ .find({ _id: { $in: syncConfigIds } }, { projection: { _id: 1, rule_mapping: 1, version_label: 1 } })
912
+ .toArray();
913
+ const syncConfigDefinitionsById = new Map(
914
+ syncConfigDefinitions.map((definition) => [definition._id.toHexString(), definition])
915
+ );
916
+
917
+ const sourceTablesByStream = new Map<
918
+ number,
919
+ { _id: ObjectId; bucket_data_source_ids: string[]; parameter_lookup_source_ids: string[] }[]
920
+ >();
921
+ await Promise.all(
922
+ v3StreamDocs.map(async (stream) => {
923
+ const sourceTables = await v3Db
924
+ .sourceTables(stream._id)
925
+ .find({}, { projection: { _id: 1, bucket_data_source_ids: 1, parameter_lookup_source_ids: 1 } })
926
+ .toArray();
927
+ sourceTablesByStream.set(stream._id, sourceTables);
928
+ })
929
+ );
930
+
931
+ const objectStorageSizeByDefinition = new Map<string, number>();
932
+ for (const usage of objectStorageDefinitionUsage) {
933
+ objectStorageSizeByDefinition.set(
934
+ `${usage.replication_stream_id}:${usage.definition_id}`,
935
+ Number(usage.active_bytes)
936
+ );
937
+ }
938
+
939
+ const sumCollectionSizes = (prefix: string, streamId: number, ids: ReadonlySet<string>) =>
940
+ [...ids].reduce((total, id) => total + (collectionSizes.get(`${prefix}${streamId}:${id}`) ?? 0), 0);
941
+
942
+ const syncConfigMetrics: storage.StorageSyncConfigMetrics[] = [];
943
+ for (const stream of v3StreamDocs) {
944
+ const sourceTables = sourceTablesByStream.get(stream._id) ?? [];
945
+ for (const syncConfig of stream.sync_configs ?? []) {
946
+ const definition = syncConfigDefinitionsById.get(syncConfig._id.toHexString());
947
+ if (definition == null) {
948
+ continue;
949
+ }
950
+
951
+ const mapping = SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(definition.rule_mapping);
952
+ const bucketDefinitionIds = mapping.allBucketDefinitionIds();
953
+ const parameterIndexIds = mapping.allParameterIndexIds();
954
+ const bucketDefinitionIdSet = new Set(bucketDefinitionIds);
955
+ const parameterIndexIdSet = new Set(parameterIndexIds);
956
+ const replicationSize = sourceTables.reduce((total, sourceTable) => {
957
+ if (
958
+ !(sourceTable.bucket_data_source_ids ?? []).some((id) => bucketDefinitionIdSet.has(id)) &&
959
+ !(sourceTable.parameter_lookup_source_ids ?? []).some((id) => parameterIndexIdSet.has(id))
960
+ ) {
961
+ return total;
962
+ }
963
+ return total + (collectionSizes.get(`source_records_${stream._id}:${sourceTable._id.toHexString()}`) ?? 0);
964
+ }, 0);
965
+
966
+ syncConfigMetrics.push({
967
+ sync_config_id: syncConfig._id.toHexString(),
968
+ sync_config_state: String(syncConfig.state),
969
+ version_label: definition.version_label,
970
+ attributed_bucket_data_bytes: sumCollectionSizes('bucket_data_', stream._id, bucketDefinitionIdSet),
971
+ attributed_parameter_indexes_bytes: sumCollectionSizes('parameter_index_', stream._id, parameterIndexIdSet),
972
+ attributed_source_records_bytes: replicationSize,
973
+ attributed_object_storage_bytes: [...bucketDefinitionIdSet].reduce(
974
+ (total, definitionId) => total + (objectStorageSizeByDefinition.get(`${stream._id}:${definitionId}`) ?? 0),
975
+ 0
976
+ )
977
+ });
978
+ }
979
+ }
980
+
981
+ const totalObjectStorageSize = objectStorageDefinitionUsage.reduce(
982
+ (total, usage) => total + usage.active_bytes,
983
+ 0n
984
+ );
501
985
  return {
502
986
  operations_size_bytes:
503
987
  Number(operations_aggregate[0].storageStats.size) +
@@ -507,7 +991,9 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
507
991
  v3_parameter_aggregates.reduce((total, aggregate) => total + Number(aggregate[0].storageStats.size), 0),
508
992
  replication_size_bytes:
509
993
  Number(v1_source_record_aggregate[0]?.storageStats?.size ?? 0) +
510
- source_record_aggregates.reduce((total, aggregate) => total + Number(aggregate[0]?.storageStats?.size ?? 0), 0)
994
+ source_record_aggregates.reduce((total, aggregate) => total + Number(aggregate[0]?.storageStats?.size ?? 0), 0),
995
+ object_storage_size_bytes: Number(totalObjectStorageSize),
996
+ sync_config_metrics: syncConfigMetrics
511
997
  };
512
998
  }
513
999