@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,55 +1,62 @@
1
- import { LEGACY_STORAGE_VERSION, storage } from '@powersync/service-core';
2
- import { DO_NOT_LOG, ErrorCode, ServiceError } from '@powersync/lib-services-framework';
1
+ import { LEGACY_STORAGE_VERSION, SingleSyncConfigBucketDefinitionMapping, storage } from '@powersync/service-core';
2
+ import { DO_NOT_LOG, ErrorCode, logger, ReplicationAssertionError, ServiceError } from '@powersync/lib-services-framework';
3
3
  import { v4 as uuid } from 'uuid';
4
4
  import * as lib_mongo from '@powersync/lib-service-mongodb';
5
+ import { describeIncrementalSyncConfigUpdate, formatIncrementalSyncConfigUpdateLog, isCompatible } from '@powersync/service-core';
5
6
  import { ObjectId } from 'bson';
6
- import { generateSlotName } from '../utils/util.js';
7
- import { BucketDefinitionMapping } from './implementation/BucketDefinitionMapping.js';
7
+ import { DEFAULT_CLEAR_BATCH_THROTTLE_RATE } from '../types/types.js';
8
+ import { generateReplicationStreamName } from '../utils/util.js';
8
9
  import { createMongoSyncBucketStorage } from './implementation/createMongoSyncBucketStorage.js';
9
10
  import { getMongoStorageConfig } from './implementation/models.js';
10
- import { MongoPersistedSyncRulesContentV1, MongoPersistedSyncRulesContentV3 } from './implementation/MongoPersistedSyncRulesContent.js';
11
- import { syncRuleStateUpdatePipeline } from './implementation/SyncRuleStateUpdate.js';
11
+ import { MongoPersistedReplicationStream } from './implementation/MongoPersistedReplicationStream.js';
12
+ import { stopReplicationStreamPipeline } from './implementation/SyncRuleStateUpdate.js';
13
+ import { ObjectStorageUsage } from './implementation/v3/object-storage/ObjectStorageUsage.js';
12
14
  export class MongoBucketStorage extends storage.BucketStorageFactory {
13
- internalOptions;
15
+ options;
14
16
  [DO_NOT_LOG] = true;
15
17
  client;
16
- session;
17
- // TODO: This is still Postgres specific and needs to be reworked
18
- slot_name_prefix;
18
+ replicationStreamNamePrefix;
19
19
  activeStorageCache;
20
20
  db;
21
- constructor(db, options, internalOptions) {
21
+ constructor(db, options) {
22
22
  super();
23
- this.internalOptions = internalOptions;
23
+ this.options = options;
24
24
  this.client = db.client;
25
25
  this.db = db;
26
- this.session = this.client.startSession();
27
- this.slot_name_prefix = options.slot_name_prefix;
26
+ this.replicationStreamNamePrefix = options.replicationStreamNamePrefix;
28
27
  }
29
28
  async [Symbol.asyncDispose]() {
30
29
  // No-op
31
30
  }
32
- getInstance(syncRules, options) {
33
- let { id, slot_name } = syncRules;
34
- if (typeof id == 'bigint') {
35
- id = Number(id);
31
+ getInstance(replicationStream, options) {
32
+ if (!(replicationStream instanceof MongoPersistedReplicationStream)) {
33
+ throw new Error(`Expected MongoPersistedReplicationStream`);
36
34
  }
37
- const storageConfig = syncRules.getStorageConfig();
38
- const storage = createMongoSyncBucketStorage(this, id, syncRules, slot_name, undefined, {
39
- ...this.internalOptions,
40
- storageConfig
35
+ let { replicationStreamId, replicationStreamName } = replicationStream;
36
+ if (typeof replicationStreamId == 'bigint') {
37
+ replicationStreamId = Number(replicationStreamId);
38
+ }
39
+ const storageConfig = replicationStream.getStorageConfig();
40
+ const syncRuleStorage = createMongoSyncBucketStorage(this, replicationStreamId, replicationStream, replicationStreamName, undefined, {
41
+ checksumOptions: this.options.checksumOptions,
42
+ readPreference: this.options.readPreference,
43
+ checksumCacheTtlMs: this.options.checksumCacheTtlMs,
44
+ storageConfig,
45
+ objectStorage: this.options.objectStorage,
46
+ inlineThresholdBytes: this.options.inlineThresholdBytes,
47
+ clearBatchThrottleRate: this.options.clearBatchThrottleRate ?? DEFAULT_CLEAR_BATCH_THROTTLE_RATE
41
48
  });
42
49
  if (!options?.skipLifecycleHooks) {
43
- this.iterateListeners((cb) => cb.syncStorageCreated?.(storage));
50
+ this.iterateListeners((cb) => cb.syncStorageCreated?.(syncRuleStorage));
44
51
  }
45
- storage.registerListener({
52
+ syncRuleStorage.registerListener({
46
53
  batchStarted: (batch) => {
47
54
  batch.registerListener({
48
55
  replicationEvent: (payload) => this.iterateListeners((cb) => cb.replicationEvent?.(payload))
49
56
  });
50
57
  }
51
58
  });
52
- return storage;
59
+ return syncRuleStorage;
53
60
  }
54
61
  async getSystemIdentifier() {
55
62
  const { setName: id } = await this.db.db.command({
@@ -63,234 +70,469 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
63
70
  type: lib_mongo.MONGO_CONNECTION_TYPE
64
71
  };
65
72
  }
66
- async restartReplication(sync_rules_group_id) {
67
- const next = await this.getNextSyncRulesContent();
68
- const active = await this.getActiveSyncRulesContent();
69
- if (next != null && next.id == sync_rules_group_id) {
70
- // We need to redo the "next" replication stream
71
- await this.updateSyncRules(next.asUpdateOptions());
72
- // Pro-actively stop replicating
73
- await this.db.sync_rules.updateOne({
74
- _id: next.id,
75
- state: storage.SyncRuleState.PROCESSING
76
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP));
77
- await this.db.notifyCheckpoint();
78
- }
79
- else if (next == null && active?.id == sync_rules_group_id) {
80
- // Slot removed for "active" replication stream, while there is no "next" one.
81
- await this.updateSyncRules(active.asUpdateOptions());
82
- // In this case we keep the old one as active for clients, so that that existing clients
83
- // can still get the latest data while we replicate the new ones.
84
- // It will however not replicate anymore.
85
- await this.db.sync_rules.updateOne({
86
- _id: active.id,
87
- state: storage.SyncRuleState.ACTIVE
88
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.ERRORED));
89
- await this.db.notifyCheckpoint();
90
- }
91
- else if (next != null && active?.id == sync_rules_group_id) {
92
- // Already have next replication stream, but need to stop replicating the active one.
93
- await this.db.sync_rules.updateOne({
94
- _id: active.id,
95
- state: storage.SyncRuleState.ACTIVE
96
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.ERRORED));
97
- await this.db.notifyCheckpoint();
98
- }
73
+ async restartReplication(replicationStreamId) {
74
+ await this.withTransaction(async (session) => {
75
+ const next = await this.getDeployingSyncConfigInternal(session);
76
+ const active = await this.getActiveSyncConfigInternal(session);
77
+ if (next != null && next.content.replicationStreamId == replicationStreamId) {
78
+ // We need to redo the "next" replication stream.
79
+ // This creates a new stream, and stops the existing PROCESSING one.
80
+ await this.updateSyncRulesInTransaction(next.content.asUpdateOptions({ forceNewReplicationStream: true }), session);
81
+ const sharedActiveStream = active?.content.replicationStreamId == replicationStreamId;
82
+ if (sharedActiveStream) {
83
+ // The same stream was also used for the ACTIVE config. Transition that to ERRORED
84
+ await this.errorActiveStreamForReplacement(active, session);
85
+ }
86
+ }
87
+ else if (next == null && active?.content.replicationStreamId == replicationStreamId) {
88
+ // Slot removed for "active" replication stream, while there is no "next" one.
89
+ await this.updateSyncRulesInTransaction(active.content.asUpdateOptions({ forceNewReplicationStream: true }), session);
90
+ // In this case we keep the old one as active for clients, so that that existing clients
91
+ // can still get the latest data while we replicate the new ones.
92
+ // It will however not replicate anymore.
93
+ await this.errorActiveStreamForReplacement(active, session);
94
+ }
95
+ else if (next != null && active?.content.replicationStreamId == replicationStreamId) {
96
+ // Already have next replication stream, but need to stop replicating the active one.
97
+ await this.errorActiveStreamForReplacement(active, session);
98
+ }
99
+ else {
100
+ // replicationStreamId does not match the ACTIVE or PROCESSING streams - no-op.
101
+ }
102
+ });
103
+ await this.db.notifyCheckpoint();
99
104
  }
100
- async updateSyncRulesV3(options, storageVersion, storageConfig) {
101
- let rules = undefined;
102
- const versioned = this.db.versioned(storageConfig);
103
- const session = this.session;
104
- await session.withTransaction(async () => {
105
- // Only have a single replication stream with PROCESSING.
106
- await this.db.sync_rules.updateMany({
107
- state: storage.SyncRuleState.PROCESSING
108
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP), { session });
109
- const id_doc = await this.db.op_id_sequence.findOneAndUpdate({
110
- _id: 'sync_rules'
105
+ /**
106
+ * Keep an active config readable after its replication stream is invalidated.
107
+ *
108
+ * Incremental V3 streams can retain stopped historical configs and embed a deploying config.
109
+ * Read-side storage requires exactly one ACTIVE or ERRORED config, so only the active config
110
+ * transitions to ERRORED; every other embedded config remains or becomes STOP.
111
+ */
112
+ async errorActiveStreamForReplacement(active, session) {
113
+ const activeSyncConfigId = active.content.syncConfigId;
114
+ if (activeSyncConfigId == null) {
115
+ // V1 documents
116
+ const result = await this.db.sync_rules.updateOne({
117
+ _id: active.content.replicationStreamId,
118
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
111
119
  }, {
112
- $inc: {
113
- op_id: 1n
120
+ $set: {
121
+ state: storage.SyncRuleState.ERRORED
114
122
  }
115
- }, {
116
- upsert: true,
117
- returnDocument: 'after',
118
- session
119
- });
120
- const id = Number(id_doc.op_id);
121
- const slot_name = generateSlotName(this.slot_name_prefix, id);
122
- const mapping = BucketDefinitionMapping.fromParsedSyncRules(options.config.parsed);
123
- const syncConfigDoc = {
124
- _id: new ObjectId(),
125
- replication_stream_id: id,
126
- created_at: new Date(),
127
- storage_version: storageVersion,
128
- content: options.config.yaml,
129
- serialized_plan: options.config.plan,
130
- rule_mapping: mapping.serialize()
131
- };
132
- await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
133
- const doc = {
134
- _id: id,
135
- storage_version: storageVersion,
136
- sync_configs: [
137
- {
138
- _id: syncConfigDoc._id,
139
- state: storage.SyncRuleState.PROCESSING,
140
- keepalive_op: null,
141
- last_checkpoint: null,
142
- last_checkpoint_lsn: null,
143
- no_checkpoint_before: null,
144
- snapshot_done: false
145
- }
146
- ],
147
- snapshot_lsn: undefined,
148
- state: storage.SyncRuleState.PROCESSING,
149
- slot_name: slot_name,
150
- last_checkpoint_ts: null,
151
- last_fatal_error: null,
152
- last_fatal_error_ts: null,
153
- last_keepalive_ts: null
154
- };
155
- await this.db.sync_rules.insertOne(doc, { session });
156
- await this.db.notifyCheckpoint();
157
- rules = new MongoPersistedSyncRulesContentV3(this.db, doc, syncConfigDoc);
158
- if (options.lock) {
159
- // The lock is persisted on rules.current_lock
160
- await rules.lock(session);
123
+ }, { session });
124
+ if (result.matchedCount != 1) {
125
+ // This should generally not happen, since the entire process runs in a transaction.
126
+ // The filters just double-check that the state is what we expect, and fails hard otherwise.
127
+ throw new ReplicationAssertionError(`Active replication stream ${active.content.replicationStreamId} changed during restart`);
161
128
  }
129
+ return;
130
+ }
131
+ const activeConfigObjectId = new ObjectId(activeSyncConfigId);
132
+ const result = await this.db.sync_rules.updateOne({
133
+ _id: active.content.replicationStreamId,
134
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] },
135
+ sync_configs: {
136
+ // Confirm that this is still the ACTIVE/ERRORED one.
137
+ $elemMatch: {
138
+ _id: activeConfigObjectId,
139
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
140
+ }
141
+ }
142
+ }, {
143
+ $set: {
144
+ state: storage.SyncRuleState.ERRORED,
145
+ // There must be exactly one config with an ERRORED state: This will be used to serve
146
+ // clients until there is a new ACTIVE one.
147
+ 'sync_configs.$[activeConfig].state': storage.SyncRuleState.ERRORED,
148
+ // PROCESSING configs must stop. Historical STOP configs remain unchanged.
149
+ 'sync_configs.$[processingConfig].state': storage.SyncRuleState.STOP
150
+ }
151
+ }, {
152
+ session,
153
+ arrayFilters: [
154
+ {
155
+ 'activeConfig._id': activeConfigObjectId,
156
+ 'activeConfig.state': { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
157
+ },
158
+ {
159
+ 'processingConfig._id': { $ne: activeConfigObjectId },
160
+ 'processingConfig.state': storage.SyncRuleState.PROCESSING
161
+ }
162
+ ]
162
163
  });
163
- return rules;
164
+ if (result.matchedCount != 1) {
165
+ // This should generally not happen, since the entire process runs in a transaction.
166
+ // The filters just double-check that the state is what we expect, and fails hard otherwise.
167
+ throw new ReplicationAssertionError(`Active replication stream ${active.content.replicationStreamId} changed during restart`);
168
+ }
164
169
  }
165
- async updateSyncRules(options) {
166
- const storageVersion = options.storageVersion ?? options.config.parsed.config.storageVersion ?? storage.CURRENT_STORAGE_VERSION;
170
+ async updateSyncRulesInTransaction(options, session) {
171
+ const storageVersion = options.storageVersion ??
172
+ options.config.parsed.config.storageVersion ??
173
+ this.options.defaultStorageVersion ??
174
+ storage.CURRENT_STORAGE_VERSION;
167
175
  const storageConfig = getMongoStorageConfig(storageVersion);
168
176
  if (storageConfig.incrementalReprocessing) {
169
- return this.updateSyncRulesV3(options, storageVersion, storageConfig);
177
+ return this.updateSyncConfigV3InTransaction(options, storageVersion, storageConfig, session);
170
178
  }
171
- let rules = undefined;
172
- const session = this.session;
173
- await session.withTransaction(async () => {
174
- // Only have a single replication stream with PROCESSING.
175
- await this.db.sync_rules.updateMany({
176
- state: storage.SyncRuleState.PROCESSING
177
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP), { session });
178
- const id_doc = await this.db.op_id_sequence.findOneAndUpdate({
179
- _id: 'sync_rules'
180
- }, {
181
- $inc: {
182
- op_id: 1n
179
+ return this.updateSyncConfigV1InTransaction(options, storageVersion, session);
180
+ }
181
+ /**
182
+ * Update to a V3 sync config.
183
+ *
184
+ * Does support cases where existing replication streams are on V1.
185
+ */
186
+ async updateSyncConfigV3InTransaction(options, storageVersion, storageConfig, session) {
187
+ const versioned = this.db.versioned(storageConfig);
188
+ const active = await this.db.sync_rules.findOne({
189
+ state: storage.SyncRuleState.ACTIVE,
190
+ storage_version: storageVersion
191
+ }, { session, sort: { _id: -1 }, limit: 1 });
192
+ if (active != null) {
193
+ const existingConfigDocs = await this.loadSyncConfigDefinitions(versioned, active, session);
194
+ if (!options.forceNewReplicationStream &&
195
+ this.options.supportsMultipleSyncConfigs &&
196
+ isCompatible(existingConfigDocs.map((d) => d.serialized_plan ?? null), options.config, logger)) {
197
+ logger.info(`Using incremental reprocessing`);
198
+ await this.stopExistingProcessingWork(session);
199
+ return await this.appendSyncConfigToStream({
200
+ versioned,
201
+ existing: active,
202
+ existingConfigDocs,
203
+ options,
204
+ storageVersion,
205
+ session
206
+ });
207
+ }
208
+ }
209
+ await this.stopExistingProcessingWork(session);
210
+ const id_doc = await this.db.op_id_sequence.findOneAndUpdate({
211
+ _id: 'sync_rules'
212
+ }, {
213
+ $inc: {
214
+ op_id: 1n
215
+ }
216
+ }, {
217
+ upsert: true,
218
+ returnDocument: 'after',
219
+ session
220
+ });
221
+ const id = Number(id_doc.op_id);
222
+ const replicationStreamName = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
223
+ const mapping = options.config.plan == null
224
+ ? // For legacy sync rules and streams, use the parsed config directly to create a mapping
225
+ SingleSyncConfigBucketDefinitionMapping.fromParsedSyncConfig(options.config.parsed)
226
+ : // For new sync streams, always use the serialized version
227
+ SingleSyncConfigBucketDefinitionMapping.constructIncrementalMappingFromSerializedPlans([], options.config.plan.plan, []);
228
+ const syncConfigDoc = {
229
+ _id: new ObjectId(),
230
+ replication_stream_id: id,
231
+ created_at: new Date(),
232
+ storage_version: storageVersion,
233
+ content: options.config.yaml,
234
+ version_label: options.version_label,
235
+ serialized_plan: options.config.plan,
236
+ rule_mapping: mapping.serialize()
237
+ };
238
+ await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
239
+ const doc = {
240
+ _id: id,
241
+ storage_version: storageVersion,
242
+ sync_configs: [
243
+ {
244
+ _id: syncConfigDoc._id,
245
+ state: storage.SyncRuleState.PROCESSING,
246
+ last_checkpoint: null,
247
+ last_checkpoint_lsn: null,
248
+ no_checkpoint_before: null,
249
+ snapshot_done: false
183
250
  }
184
- }, {
185
- upsert: true,
186
- returnDocument: 'after',
187
- session
188
- });
189
- const id = Number(id_doc.op_id);
190
- const slot_name = generateSlotName(this.slot_name_prefix, id);
191
- const doc = {
192
- _id: id,
193
- storage_version: storageVersion,
194
- content: options.config.yaml,
195
- serialized_plan: options.config.plan,
196
- last_checkpoint: null,
197
- last_checkpoint_lsn: null,
198
- no_checkpoint_before: null,
199
- keepalive_op: null,
200
- snapshot_done: false,
201
- snapshot_lsn: undefined,
202
- state: storage.SyncRuleState.PROCESSING,
203
- slot_name: slot_name,
204
- last_checkpoint_ts: null,
251
+ ],
252
+ snapshot_lsn: undefined,
253
+ state: storage.SyncRuleState.PROCESSING,
254
+ slot_name: replicationStreamName,
255
+ last_checkpoint_ts: null,
256
+ last_fatal_error: null,
257
+ last_fatal_error_ts: null,
258
+ last_keepalive_ts: null
259
+ };
260
+ await this.db.sync_rules.insertOne(doc, { session });
261
+ const rules = new MongoPersistedReplicationStream(this.db, doc, [syncConfigDoc]);
262
+ if (options.lock) {
263
+ // We only lock when creating a new stream - otherwise we'll likely get lock contention
264
+ // from an existing job on the stream, which would fail the entire transaction.
265
+ // The lock is persisted on rules.current_lock
266
+ await rules.lock(session);
267
+ }
268
+ return rules;
269
+ }
270
+ async loadSyncConfigDefinitions(versioned, existing, session) {
271
+ const activeConfigIds = existing.sync_configs
272
+ .filter((config) => config.state == storage.SyncRuleState.ACTIVE)
273
+ .map((config) => config._id);
274
+ return versioned.syncConfigDefinitions
275
+ .find({
276
+ _id: { $in: activeConfigIds }
277
+ }, { session })
278
+ .toArray();
279
+ }
280
+ /**
281
+ * Load _all_ definition mappings for a replication stream - used as a base to generate new ids.
282
+ */
283
+ async loadHistoricalSyncConfigRuleMappings(versioned, replicationStreamId, session) {
284
+ return versioned.syncConfigDefinitions
285
+ .find({
286
+ replication_stream_id: replicationStreamId
287
+ }, {
288
+ session,
289
+ projection: {
290
+ rule_mapping: 1
291
+ }
292
+ })
293
+ .toArray();
294
+ }
295
+ logIncrementalDefinitionChanges(changes) {
296
+ logger.info(`Incremental reprocessing sync config update:\n${formatIncrementalSyncConfigUpdateLog(changes)}`);
297
+ }
298
+ async appendSyncConfigToStream(options) {
299
+ const { versioned, existing, existingConfigDocs, options: updateOptions, storageVersion, session } = options;
300
+ const compatibleConfigs = existingConfigDocs.map((doc) => ({
301
+ plan: doc.serialized_plan.plan,
302
+ mapping: SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(doc.rule_mapping)
303
+ }));
304
+ const historicalRuleMappings = await this.loadHistoricalSyncConfigRuleMappings(versioned, existing._id, session);
305
+ const reservedMappings = historicalRuleMappings.map((doc) => SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(doc.rule_mapping));
306
+ const mappingResult = SingleSyncConfigBucketDefinitionMapping.constructIncrementalMappingWithChanges(compatibleConfigs, updateOptions.config.plan.plan, reservedMappings);
307
+ const mapping = mappingResult.mapping;
308
+ this.logIncrementalDefinitionChanges(describeIncrementalSyncConfigUpdate({
309
+ activeMappings: existingConfigDocs.map((doc) => SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(doc.rule_mapping)),
310
+ newMapping: mapping,
311
+ newSyncConfig: updateOptions.config.parsed,
312
+ mappingChanges: mappingResult.changes
313
+ }));
314
+ const syncConfigDoc = {
315
+ _id: new ObjectId(),
316
+ replication_stream_id: existing._id,
317
+ created_at: new Date(),
318
+ storage_version: storageVersion,
319
+ content: updateOptions.config.yaml,
320
+ version_label: updateOptions.version_label,
321
+ serialized_plan: updateOptions.config.plan,
322
+ rule_mapping: mapping.serialize()
323
+ };
324
+ await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
325
+ const syncConfigState = {
326
+ _id: syncConfigDoc._id,
327
+ state: storage.SyncRuleState.PROCESSING,
328
+ last_checkpoint: null,
329
+ last_checkpoint_lsn: null,
330
+ no_checkpoint_before: null,
331
+ snapshot_done: false
332
+ };
333
+ await this.db.sync_rules.updateOne({ _id: existing._id }, {
334
+ $push: {
335
+ sync_configs: syncConfigState
336
+ },
337
+ $set: {
205
338
  last_fatal_error: null,
206
- last_fatal_error_ts: null,
207
- last_keepalive_ts: null
208
- };
209
- await this.db.sync_rules.insertOne(doc, { session });
210
- await this.db.notifyCheckpoint();
211
- rules = new MongoPersistedSyncRulesContentV1(this.db, doc);
212
- if (options.lock) {
213
- // The lock is persisted on rules.current_lock
214
- await rules.lock(session);
339
+ last_fatal_error_ts: null
215
340
  }
341
+ }, { session });
342
+ const syncConfigStates = [
343
+ ...existing.sync_configs.filter((config) => config.state == storage.SyncRuleState.ACTIVE),
344
+ syncConfigState
345
+ ];
346
+ const stream = new MongoPersistedReplicationStream(this.db, {
347
+ ...existing,
348
+ sync_configs: syncConfigStates
349
+ }, [...existingConfigDocs, syncConfigDoc]);
350
+ // The stream already exists, so an active replication job may already hold the stream lock.
351
+ // Deployment only persists the appended sync config; replication job locking is handled by
352
+ // the replicator.
353
+ return stream;
354
+ }
355
+ async updateSyncRules(options) {
356
+ let rules;
357
+ await this.withTransaction(async (session) => {
358
+ rules = await this.updateSyncRulesInTransaction(options, session);
359
+ });
360
+ await this.db.notifyCheckpoint();
361
+ return rules;
362
+ }
363
+ async withTransaction(callback) {
364
+ return this.client.withSession((session) => session.withTransaction(callback));
365
+ }
366
+ /**
367
+ * Update to a V1 sync config.
368
+ *
369
+ * Does support cases where existing replication streams are on V3.
370
+ */
371
+ async updateSyncConfigV1InTransaction(options, storageVersion, session) {
372
+ await this.stopExistingProcessingWork(session);
373
+ const id_doc = await this.db.op_id_sequence.findOneAndUpdate({
374
+ _id: 'sync_rules'
375
+ }, {
376
+ $inc: {
377
+ op_id: 1n
378
+ }
379
+ }, {
380
+ upsert: true,
381
+ returnDocument: 'after',
382
+ session
216
383
  });
384
+ const id = Number(id_doc.op_id);
385
+ const slot_name = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
386
+ // All V1 replication streams share both the `main` op id sequence and the `bucket_parameters`
387
+ // collection, so every parameter entry this stream writes gets an op id above the current
388
+ // head. Seeding the parameter compaction cursor with that head keeps the stream's first
389
+ // compaction from scanning other streams' history, which would otherwise be repeated for
390
+ // every new deployment. A concurrent replication flush can only advance the head after this
391
+ // read, which makes the seed conservative, never too high.
392
+ const opSequence = await this.db.op_id_sequence.findOne({ _id: 'main' }, { session });
393
+ const doc = {
394
+ _id: id,
395
+ storage_version: storageVersion,
396
+ content: options.config.yaml,
397
+ version_label: options.version_label,
398
+ serialized_plan: options.config.plan,
399
+ last_checkpoint: null,
400
+ last_checkpoint_lsn: null,
401
+ no_checkpoint_before: null,
402
+ keepalive_op: null,
403
+ snapshot_done: false,
404
+ snapshot_lsn: undefined,
405
+ state: storage.SyncRuleState.PROCESSING,
406
+ slot_name: slot_name,
407
+ last_checkpoint_ts: null,
408
+ last_fatal_error: null,
409
+ last_fatal_error_ts: null,
410
+ last_keepalive_ts: null,
411
+ parameter_compaction: { compacted_before: opSequence?.op_id ?? 0n }
412
+ };
413
+ await this.db.sync_rules.insertOne(doc, { session });
414
+ const rules = new MongoPersistedReplicationStream(this.db, doc);
415
+ if (options.lock) {
416
+ // The lock is persisted on rules.current_lock
417
+ await rules.lock(session);
418
+ }
217
419
  return rules;
218
420
  }
219
- async getActiveSyncRulesContent() {
421
+ /**
422
+ * Stop top-level and embedded processing work before creating a replacement stream.
423
+ *
424
+ * Use in the same transaction that the replacement stream is created.
425
+ */
426
+ async stopExistingProcessingWork(session) {
427
+ await this.db.sync_rules.updateMany({
428
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] },
429
+ 'sync_configs.state': storage.SyncRuleState.PROCESSING
430
+ }, {
431
+ $set: {
432
+ 'sync_configs.$[config].state': storage.SyncRuleState.STOP
433
+ }
434
+ }, {
435
+ session,
436
+ arrayFilters: [{ 'config.state': storage.SyncRuleState.PROCESSING }]
437
+ });
438
+ await this.db.sync_rules.updateMany({
439
+ state: storage.SyncRuleState.PROCESSING
440
+ }, stopReplicationStreamPipeline(), { session });
441
+ }
442
+ async getActiveSyncConfig() {
443
+ return this.getActiveSyncConfigInternal();
444
+ }
445
+ async getActiveSyncConfigInternal(session) {
220
446
  const doc = await this.db.sync_rules.findOne({
221
447
  state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
222
- }, { sort: { _id: -1 }, limit: 1 });
223
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED]);
448
+ }, { session, sort: { _id: -1 }, limit: 1 });
449
+ return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED], { cacheActiveStorage: session == null }, session);
224
450
  }
225
- async getSyncRulesContent(doc, stateFilter) {
451
+ async replicationStreamFromDoc(doc, stateFilter, session) {
226
452
  if (doc == null) {
227
453
  return null;
228
454
  }
229
455
  const storageConfig = getMongoStorageConfig(doc.storage_version ?? LEGACY_STORAGE_VERSION);
230
456
  if (storageConfig.incrementalReprocessing) {
231
457
  const v3 = doc;
232
- const active = v3.sync_configs.find((c) => stateFilter.includes(c.state));
233
- if (active == null) {
458
+ const matching = v3.sync_configs.filter((c) => stateFilter.includes(c.state));
459
+ if (matching.length == 0) {
234
460
  return null;
235
461
  }
236
462
  // TODO: cache the config. It could specifically help for the main replication loop
237
463
  // that checks for active replication streams.
238
464
  // It is not a major bottleneck though, since it only runs once every couple of seconds at most.
239
465
  const db = this.db.versioned(storageConfig);
240
- const syncConfigDoc = await db.syncConfigDefinitions.findOne({ _id: active._id });
241
- if (syncConfigDoc == null) {
466
+ const syncConfigDocs = await db.syncConfigDefinitions
467
+ .find({
468
+ _id: { $in: matching.map((config) => config._id) }
469
+ }, { session })
470
+ .toArray();
471
+ if (syncConfigDocs.length == 0) {
242
472
  return null;
243
473
  }
244
- return new MongoPersistedSyncRulesContentV3(this.db, v3, syncConfigDoc);
474
+ return new MongoPersistedReplicationStream(this.db, v3, syncConfigDocs);
245
475
  }
246
- return new MongoPersistedSyncRulesContentV1(this.db, doc);
476
+ return new MongoPersistedReplicationStream(this.db, doc);
477
+ }
478
+ async getDeployingSyncConfig() {
479
+ return this.getDeployingSyncConfigInternal();
247
480
  }
248
- async getNextSyncRulesContent() {
481
+ async getDeployingSyncConfigInternal(session) {
249
482
  const doc = await this.db.sync_rules.findOne({
250
- state: storage.SyncRuleState.PROCESSING
251
- }, { sort: { _id: -1 }, limit: 1 });
252
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.PROCESSING]);
483
+ $or: [{ state: storage.SyncRuleState.PROCESSING }, { 'sync_configs.state': storage.SyncRuleState.PROCESSING }]
484
+ }, { session, sort: { _id: -1 }, limit: 1 });
485
+ return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.PROCESSING], {}, session);
253
486
  }
254
- async getReplicatingSyncRules() {
487
+ async getReplicatingReplicationStreams() {
255
488
  const docs = await this.db.sync_rules
256
489
  .find({
257
490
  state: { $in: [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE] }
258
491
  })
259
492
  .toArray();
260
493
  return (await Promise.all(docs.map((doc) => {
261
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE]);
494
+ return this.replicationStreamFromDoc(doc, [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE]);
262
495
  }))).filter((r) => r != null);
263
496
  }
264
- async getStoppedSyncRules() {
497
+ async getStoppedReplicationStreams() {
265
498
  const docs = await this.db.sync_rules
266
499
  .find({
267
500
  state: storage.SyncRuleState.STOP
268
501
  })
269
502
  .toArray();
270
503
  return (await Promise.all(docs.map((doc) => {
271
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.STOP]);
504
+ return this.replicationStreamFromDoc(doc, [storage.SyncRuleState.STOP]);
272
505
  }))).filter((d) => d != null);
273
506
  }
274
- async getActiveStorage() {
275
- const content = await this.getActiveSyncRulesContent();
276
- if (content == null) {
507
+ async resolvedSyncConfigFromDoc(doc, stateFilter, options = {}, session) {
508
+ const stream = await this.replicationStreamFromDoc(doc, stateFilter, session);
509
+ if (stream == null) {
277
510
  return null;
278
511
  }
279
- // It is important that this instance is cached.
280
- // Not for the instance construction itself, but to ensure that internal caches on the instance
281
- // are re-used properly.
282
- if (this.activeStorageCache?.group_id == content.id) {
283
- return this.activeStorageCache;
284
- }
285
- else {
286
- const instance = this.getInstance(content);
287
- this.activeStorageCache = instance;
288
- return instance;
289
- }
512
+ const content = stream.syncConfigContent[0];
513
+ const thisFactory = this;
514
+ return {
515
+ content,
516
+ replicationStream: stream,
517
+ get storage() {
518
+ // It is important that this instance is cached.
519
+ // Not for the instance construction itself, but to ensure that internal caches on the instance
520
+ // are re-used properly.
521
+ if (options.cacheActiveStorage &&
522
+ thisFactory.activeStorageCache?.replicationStream.replicationJobId == stream.replicationJobId) {
523
+ return thisFactory.activeStorageCache;
524
+ }
525
+ const instance = thisFactory.getInstance(stream);
526
+ if (options.cacheActiveStorage) {
527
+ thisFactory.activeStorageCache = instance;
528
+ }
529
+ return instance;
530
+ }
531
+ };
290
532
  }
291
533
  async getStorageMetrics() {
292
534
  const ignoreNotExisting = (e) => {
293
- if (lib_mongo.isMongoServerError(e) && e.codeName == 'NamespaceNotFound') {
535
+ if (lib_mongo.isMongoNamespaceNotFoundError(e)) {
294
536
  // Collection doesn't exist - return 0
295
537
  return [{ storageStats: { size: 0 } }];
296
538
  }
@@ -298,8 +540,6 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
298
540
  return Promise.reject(e);
299
541
  }
300
542
  };
301
- // For now, we get storage metrics over all v1 and v3 collections.
302
- // In the future, we may split these metrics to report separately for active replication streams versus processing streams.
303
543
  const aggregateStaticCollection = async (collection) => {
304
544
  // We check whether the collection exists before getting the statistics. This avoids repeated
305
545
  // errors in the MongoDB logs if the collection hasn't been created yet.
@@ -320,7 +560,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
320
560
  .catch(ignoreNotExisting);
321
561
  };
322
562
  const operations_aggregate = await aggregateStaticCollection(this.db.bucket_data);
323
- const v3_operation_aggregates = await Promise.all((await this.db.listBucketDataCollectionsV3()).map((collection) => collection
563
+ const v3OperationCollections = await this.db.listBucketDataCollectionsV3();
564
+ const v3_operation_aggregates = await Promise.all(v3OperationCollections.map((collection) => collection
324
565
  .aggregate([
325
566
  {
326
567
  $collStats: {
@@ -331,7 +572,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
331
572
  .toArray()
332
573
  .catch(ignoreNotExisting)));
333
574
  const parameters_aggregate = await aggregateStaticCollection(this.db.bucket_parameters);
334
- const v3_parameter_aggregates = await Promise.all((await this.db.listAllParameterIndexCollectionsV3()).map((collection) => collection
575
+ const v3ParameterCollections = await this.db.listAllParameterIndexCollectionsV3();
576
+ const v3_parameter_aggregates = await Promise.all(v3ParameterCollections.map((collection) => collection
335
577
  .aggregate([
336
578
  {
337
579
  $collStats: {
@@ -342,7 +584,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
342
584
  .toArray()
343
585
  .catch(ignoreNotExisting)));
344
586
  const v1_source_record_aggregate = await aggregateStaticCollection(this.db.current_data);
345
- const source_record_aggregates = await Promise.all((await this.db.listAllSourceRecordCollectionsV3()).map((collection) => collection
587
+ const v3SourceRecordCollections = await this.db.listAllSourceRecordCollectionsV3();
588
+ const source_record_aggregates = await Promise.all(v3SourceRecordCollections.map((collection) => collection
346
589
  .aggregate([
347
590
  {
348
591
  $collStats: {
@@ -352,13 +595,84 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
352
595
  ])
353
596
  .toArray()
354
597
  .catch(ignoreNotExisting)));
598
+ const v3StorageConfig = getMongoStorageConfig(storage.STORAGE_VERSION_3);
599
+ const v3Db = this.db.versioned(v3StorageConfig);
600
+ const objectStorageDefinitionUsage = await ObjectStorageUsage.readAllDefinitionUsage(v3Db);
601
+ const v3StreamDocs = (await this.db.sync_rules
602
+ .find({ storage_version: { $gte: storage.STORAGE_VERSION_3 } }, { projection: { _id: 1, sync_configs: 1 } })
603
+ .toArray());
604
+ const collectionSizes = new Map();
605
+ const addCollectionSize = (collection, aggregate, prefix, sizes) => {
606
+ const match = collection.collectionName.match(new RegExp(`^${prefix}(\\d+)_(.+)$`));
607
+ if (match == null) {
608
+ return;
609
+ }
610
+ sizes.set(`${prefix}${match[1]}:${match[2]}`, Number(aggregate[0]?.storageStats?.size ?? 0));
611
+ };
612
+ v3OperationCollections.forEach((collection, index) => addCollectionSize(collection, v3_operation_aggregates[index], 'bucket_data_', collectionSizes));
613
+ v3ParameterCollections.forEach((collection, index) => addCollectionSize(collection, v3_parameter_aggregates[index], 'parameter_index_', collectionSizes));
614
+ v3SourceRecordCollections.forEach((collection, index) => addCollectionSize(collection, source_record_aggregates[index], 'source_records_', collectionSizes));
615
+ const syncConfigIds = v3StreamDocs.flatMap((stream) => (stream.sync_configs ?? []).map((config) => config._id));
616
+ const syncConfigDefinitions = syncConfigIds.length == 0
617
+ ? []
618
+ : await v3Db.syncConfigDefinitions
619
+ .find({ _id: { $in: syncConfigIds } }, { projection: { _id: 1, rule_mapping: 1, version_label: 1 } })
620
+ .toArray();
621
+ const syncConfigDefinitionsById = new Map(syncConfigDefinitions.map((definition) => [definition._id.toHexString(), definition]));
622
+ const sourceTablesByStream = new Map();
623
+ await Promise.all(v3StreamDocs.map(async (stream) => {
624
+ const sourceTables = await v3Db
625
+ .sourceTables(stream._id)
626
+ .find({}, { projection: { _id: 1, bucket_data_source_ids: 1, parameter_lookup_source_ids: 1 } })
627
+ .toArray();
628
+ sourceTablesByStream.set(stream._id, sourceTables);
629
+ }));
630
+ const objectStorageSizeByDefinition = new Map();
631
+ for (const usage of objectStorageDefinitionUsage) {
632
+ objectStorageSizeByDefinition.set(`${usage.replication_stream_id}:${usage.definition_id}`, Number(usage.active_bytes));
633
+ }
634
+ const sumCollectionSizes = (prefix, streamId, ids) => [...ids].reduce((total, id) => total + (collectionSizes.get(`${prefix}${streamId}:${id}`) ?? 0), 0);
635
+ const syncConfigMetrics = [];
636
+ for (const stream of v3StreamDocs) {
637
+ const sourceTables = sourceTablesByStream.get(stream._id) ?? [];
638
+ for (const syncConfig of stream.sync_configs ?? []) {
639
+ const definition = syncConfigDefinitionsById.get(syncConfig._id.toHexString());
640
+ if (definition == null) {
641
+ continue;
642
+ }
643
+ const mapping = SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(definition.rule_mapping);
644
+ const bucketDefinitionIds = mapping.allBucketDefinitionIds();
645
+ const parameterIndexIds = mapping.allParameterIndexIds();
646
+ const bucketDefinitionIdSet = new Set(bucketDefinitionIds);
647
+ const parameterIndexIdSet = new Set(parameterIndexIds);
648
+ const replicationSize = sourceTables.reduce((total, sourceTable) => {
649
+ if (!(sourceTable.bucket_data_source_ids ?? []).some((id) => bucketDefinitionIdSet.has(id)) &&
650
+ !(sourceTable.parameter_lookup_source_ids ?? []).some((id) => parameterIndexIdSet.has(id))) {
651
+ return total;
652
+ }
653
+ return total + (collectionSizes.get(`source_records_${stream._id}:${sourceTable._id.toHexString()}`) ?? 0);
654
+ }, 0);
655
+ syncConfigMetrics.push({
656
+ sync_config_id: syncConfig._id.toHexString(),
657
+ sync_config_state: String(syncConfig.state),
658
+ version_label: definition.version_label,
659
+ attributed_bucket_data_bytes: sumCollectionSizes('bucket_data_', stream._id, bucketDefinitionIdSet),
660
+ attributed_parameter_indexes_bytes: sumCollectionSizes('parameter_index_', stream._id, parameterIndexIdSet),
661
+ attributed_source_records_bytes: replicationSize,
662
+ attributed_object_storage_bytes: [...bucketDefinitionIdSet].reduce((total, definitionId) => total + (objectStorageSizeByDefinition.get(`${stream._id}:${definitionId}`) ?? 0), 0)
663
+ });
664
+ }
665
+ }
666
+ const totalObjectStorageSize = objectStorageDefinitionUsage.reduce((total, usage) => total + usage.active_bytes, 0n);
355
667
  return {
356
668
  operations_size_bytes: Number(operations_aggregate[0].storageStats.size) +
357
669
  v3_operation_aggregates.reduce((total, aggregate) => total + Number(aggregate[0].storageStats.size), 0),
358
670
  parameters_size_bytes: Number(parameters_aggregate[0].storageStats.size) +
359
671
  v3_parameter_aggregates.reduce((total, aggregate) => total + Number(aggregate[0].storageStats.size), 0),
360
672
  replication_size_bytes: Number(v1_source_record_aggregate[0]?.storageStats?.size ?? 0) +
361
- source_record_aggregates.reduce((total, aggregate) => total + Number(aggregate[0]?.storageStats?.size ?? 0), 0)
673
+ source_record_aggregates.reduce((total, aggregate) => total + Number(aggregate[0]?.storageStats?.size ?? 0), 0),
674
+ object_storage_size_bytes: Number(totalObjectStorageSize),
675
+ sync_config_metrics: syncConfigMetrics
362
676
  };
363
677
  }
364
678
  async getPowerSyncInstanceId() {