@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,44 +1,100 @@
1
1
  import * as lib_mongo from '@powersync/lib-service-mongodb';
2
- import { ReplicationAssertionError } from '@powersync/lib-services-framework';
2
+ import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
3
3
  import { storage } from '@powersync/service-core';
4
4
  import * as bson from 'bson';
5
5
  import { mongoTableId } from '../../../utils/util.js';
6
- import { calculateCheckpointState } from '../CheckpointState.js';
6
+ import { canCheckpointState } from '../CheckpointState.js';
7
7
  import { MongoBucketBatch } from '../MongoBucketBatch.js';
8
- import { syncRuleStateUpdatePipeline } from '../SyncRuleStateUpdate.js';
8
+ import { stopReplicationStreamPipeline } from '../SyncRuleStateUpdate.js';
9
9
  import { PersistedBatchV3 } from './PersistedBatchV3.js';
10
10
  import { SourceRecordStoreV3 } from './SourceRecordStoreV3.js';
11
- function sameStringArray(left, right) {
12
- return left.length == right.length && left.every((value, index) => value == right[index]);
13
- }
11
+ import { createNewSourceTable, overlappingSourceTableFilter, planSourceTableReconciliation, sourceTableDesiredResolution, sourceTableFromDocument } from './source-table-utils.js';
14
12
  export class MongoBucketBatchV3 extends MongoBucketBatch {
15
13
  store;
16
- syncConfigId;
14
+ /** Avoid repeating idempotent createIndexes calls for every checkpoint flush. */
15
+ initializedCustomCheckpointEventIds = new Set();
16
+ // Used only to validate supplied checkpoint ids; it does not initialize collections.
17
+ knownEventIds;
18
+ syncConfigIds;
17
19
  needsActivationV3 = true;
18
20
  lastWaitingLogThrottledV3 = 0;
19
21
  constructor(options) {
20
22
  super(options);
21
- if (options.syncConfigId == null) {
23
+ const syncConfigIds = options.syncConfigIds ?? [];
24
+ if (syncConfigIds.length == 0) {
22
25
  throw new ReplicationAssertionError('Missing sync config id for v3 batch');
23
26
  }
24
- this.syncConfigId = options.syncConfigId;
25
- this.store = new SourceRecordStoreV3(this.db, this.group_id, this.mapping);
27
+ this.syncConfigIds = syncConfigIds;
28
+ this.knownEventIds = new Set(this.mapping.allEventDefinitionIds());
29
+ this.store = new SourceRecordStoreV3(this.db, this.replicationStreamId, this.mapping);
26
30
  }
27
31
  createPersistedBatch(writtenSize) {
28
- return new PersistedBatchV3(this.db, this.group_id, this.mapping, writtenSize, {
29
- logger: this.logger
32
+ return new PersistedBatchV3(this.db, this.replicationStreamId, this.mapping, writtenSize, {
33
+ logger: this.logger,
34
+ objectStorage: this.options.objectStorage,
35
+ inlineThresholdBytes: this.options.inlineThresholdBytes,
36
+ signal: this.options.signal,
37
+ objectStorageUsageWriterId: this.objectStorageUsageWriterId
30
38
  });
31
39
  }
32
40
  get sourceRecordStore() {
33
41
  return this.store;
34
42
  }
43
+ async onReplicationTransactionFlush(session, lastOp) {
44
+ // Durably advance the stream-level head of persisted ops within the flush transaction.
45
+ // This ensures a checkpoint created later (even by an empty commit, or by a freshly-appended
46
+ // config that replicates nothing) covers all ops persisted before a potential crash.
47
+ await this.db.sync_rules.updateOne({
48
+ _id: this.replicationStreamId
49
+ }, {
50
+ $max: {
51
+ last_persisted_op: lastOp
52
+ }
53
+ }, { session });
54
+ }
55
+ selectedSyncConfigObjectIds(syncConfigIds) {
56
+ const selectedIds = new Set(this.syncConfigIds.map((id) => id.toHexString()));
57
+ return syncConfigIds
58
+ .filter((id) => selectedIds.has(id) && bson.ObjectId.isValid(id))
59
+ .map((id) => new bson.ObjectId(id));
60
+ }
61
+ relevantSyncConfigIds(table) {
62
+ const bucketDataSourceIds = table.bucketDataSources.map((source) => this.mapping.bucketSourceId(source));
63
+ const parameterLookupSourceIds = table.parameterLookupSources.map((source) => this.mapping.parameterLookupId(source));
64
+ return this.selectedSyncConfigObjectIds(this.mapping.syncConfigIdsForSourceTable(this.syncConfigIds.map((id) => id.toHexString()), table.ref, bucketDataSourceIds, parameterLookupSourceIds, [...table.eventDefinitionIds]));
65
+ }
66
+ relevantSyncConfigIdsForTables(tables) {
67
+ const ids = new Map();
68
+ for (const table of tables) {
69
+ for (const id of this.relevantSyncConfigIds(table)) {
70
+ ids.set(id.toHexString(), id);
71
+ }
72
+ }
73
+ return [...ids.values()];
74
+ }
75
+ snapshotBlockingSourceTablesFilter() {
76
+ const clauses = this.syncConfigIds.flatMap((syncConfigId) => {
77
+ const filter = this.mapping.snapshotBlockingSourceTablesFilter(syncConfigId.toHexString());
78
+ return filter.$or ?? [];
79
+ });
80
+ if (clauses.length == 0) {
81
+ return {
82
+ snapshot_done: false,
83
+ _id: { $exists: false }
84
+ };
85
+ }
86
+ return {
87
+ snapshot_done: false,
88
+ $or: clauses
89
+ };
90
+ }
35
91
  async cleanupDroppedSourceTables(sourceTables) {
36
92
  for (const table of sourceTables) {
37
93
  await this.db
38
- .sourceRecordsV3(this.group_id, mongoTableId(table.id))
94
+ .sourceRecords(this.replicationStreamId, mongoTableId(table.id))
39
95
  .drop()
40
96
  .catch((error) => {
41
- if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
97
+ if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
42
98
  return;
43
99
  }
44
100
  throw error;
@@ -46,179 +102,96 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
46
102
  }
47
103
  }
48
104
  async resolveTables(options) {
49
- const ref = options.source;
50
- const syncRules = options.syncRules ?? this.sync_rules;
51
- const matchingSources = syncRules.getMatchingSources(ref);
105
+ // The test-only override is a whole parsed set, so the sync rules and the mapping
106
+ // used below always come from the same parse.
107
+ const parsedOverride = options.parsedSyncConfig;
108
+ const syncConfig = parsedOverride?.hydratedSyncConfig ?? this.sync_rules;
109
+ const mapping = parsedOverride?.mapping ?? this.mapping;
110
+ const eventById = (parsedOverride ?? this.options.parsedSyncConfig).eventById;
52
111
  const { connection_id, source } = options;
53
- const { schema, name, objectId, replicaIdColumns, connectionTag } = source;
54
- const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
55
- name: column.name,
56
- type: column.type,
57
- type_oid: column.typeId
58
- }));
112
+ const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
113
+ const identity = {
114
+ schema: source.schema,
115
+ name: source.name,
116
+ objectId: source.objectId,
117
+ replicaIdColumns: source.replicaIdColumns.map((column) => ({
118
+ name: column.name,
119
+ type: column.type,
120
+ type_oid: column.typeId
121
+ }))
122
+ };
59
123
  let result = null;
60
- const initializeSourceRecordsFor = [];
61
- await this.db.client.withSession(async (session) => {
62
- const col = this.db.commonSourceTables(this.group_id);
63
- const exactFilter = {
64
- connection_id,
65
- schema_name: schema,
66
- table_name: name,
67
- replica_id_columns2: normalizedReplicaIdColumns
68
- };
69
- if (objectId != null) {
70
- exactFilter.relation_id = objectId;
124
+ const session = this.db.client.startSession();
125
+ await using _ = { [Symbol.asyncDispose]: () => session.endSession() };
126
+ await session.withTransaction(async () => {
127
+ const col = this.db.sourceTables(this.replicationStreamId);
128
+ // Find records that overlap by name or relation id.
129
+ const candidateDocs = await col
130
+ .find(overlappingSourceTableFilter(connection_id, identity), { session })
131
+ .toArray();
132
+ const candidateTables = candidateDocs.map((doc) => sourceTableFromDocument(doc, source.connectionTag, syncConfig, mapping, eventById));
133
+ const candidates = candidateTables.map((table) => table.clone());
134
+ const resolution = await reconcile({ source, candidates });
135
+ storage.validateSourceTableCandidateResolution(candidates, resolution);
136
+ // Persist metadata from the reconciler without mutating the queried documents.
137
+ for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
138
+ await col.updateOne({ _id: mongoTableId(id) }, { $set: { source_metadata: sourceMetadata } }, { session });
71
139
  }
72
- const exactDocs = (await col.find(exactFilter, { session }).toArray());
73
- const bucketSourceById = new Map(matchingSources.bucketDataSources.map((source) => [this.mapping.bucketSourceId(source), source]));
74
- const parameterLookupSourceById = new Map(matchingSources.parameterLookupSources.map((source) => [this.mapping.parameterLookupId(source), source]));
75
- const desiredBucketIds = new Set(bucketSourceById.keys());
76
- const desiredLookupIds = new Set(parameterLookupSourceById.keys());
77
- const desiredHasMembership = desiredBucketIds.size > 0 || desiredLookupIds.size > 0;
78
- const triggersEvent = syncRules.tableTriggersEvent(ref);
79
- const coveredBucketIds = new Set();
80
- const coveredLookupIds = new Set();
81
- const retainedDocIds = [];
82
- const tables = [];
83
- let retainedEventOnlyTable = false;
84
- for (const doc of exactDocs) {
85
- const bucketDataSourceIds = doc.bucket_data_source_ids.filter((id) => desiredBucketIds.has(id));
86
- const parameterLookupSourceIds = doc.parameter_lookup_source_ids.filter((id) => desiredLookupIds.has(id));
87
- const coversDesiredMembership = bucketDataSourceIds.length > 0 || parameterLookupSourceIds.length > 0;
88
- const coversEventOnlyTable = !desiredHasMembership && triggersEvent && !retainedEventOnlyTable;
89
- for (const id of bucketDataSourceIds) {
90
- coveredBucketIds.add(id);
91
- }
92
- for (const id of parameterLookupSourceIds) {
93
- coveredLookupIds.add(id);
94
- }
95
- if (!sameStringArray(doc.bucket_data_source_ids, bucketDataSourceIds) ||
96
- !sameStringArray(doc.parameter_lookup_source_ids, parameterLookupSourceIds)) {
97
- await col.updateOne({ _id: doc._id }, {
98
- $set: {
99
- bucket_data_source_ids: bucketDataSourceIds,
100
- parameter_lookup_source_ids: parameterLookupSourceIds
101
- }
102
- }, { session });
103
- }
104
- if (coversDesiredMembership || coversEventOnlyTable) {
105
- if (coversEventOnlyTable) {
106
- retainedEventOnlyTable = true;
140
+ const context = {
141
+ connectionId: connection_id,
142
+ connectionTag: source.connectionTag,
143
+ identity,
144
+ storeCurrentData: source.sendsCompleteRows !== true,
145
+ syncConfig,
146
+ mapping,
147
+ desired: sourceTableDesiredResolution(syncConfig, source, mapping, eventById),
148
+ sourceCompatibleTables: resolution.compatibleTables,
149
+ newTableSourceMetadata: resolution.newTableValues.sourceMetadata
150
+ };
151
+ // Plan record reuse, membership changes, creation, and removal.
152
+ const plan = planSourceTableReconciliation(candidateDocs, context);
153
+ // Persist narrowing for incomplete snapshots only. Snapshot-complete docs keep stale
154
+ // coverage ids so compatible future configs can reuse already-snapshotted data.
155
+ // Narrowing occurs after removing a sync config, meaning we don't process those
156
+ // definitions anymore.
157
+ for (const update of plan.narrowingUpdates) {
158
+ await col.updateOne({ _id: update.id }, {
159
+ $set: {
160
+ bucket_data_source_ids: update.memberships.bucketDataSourceIds,
161
+ parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds,
162
+ event_definition_ids: update.memberships.eventDefinitionIds
107
163
  }
108
- retainedDocIds.push(doc._id);
109
- tables.push(this.sourceTableFromDocument({
110
- ...doc,
111
- bucket_data_source_ids: bucketDataSourceIds,
112
- parameter_lookup_source_ids: parameterLookupSourceIds
113
- }, connectionTag, syncRules, {
114
- bucketDataSources: bucketDataSourceIds.map((id) => bucketSourceById.get(id)),
115
- parameterLookupSources: parameterLookupSourceIds.map((id) => parameterLookupSourceById.get(id))
116
- }));
117
- }
164
+ }, { session });
118
165
  }
119
- const uncoveredBucketIds = [...desiredBucketIds].filter((id) => !coveredBucketIds.has(id));
120
- const uncoveredLookupIds = [...desiredLookupIds].filter((id) => !coveredLookupIds.has(id));
121
- if (uncoveredBucketIds.length > 0 || uncoveredLookupIds.length > 0 || (triggersEvent && tables.length == 0)) {
166
+ // Any desired membership not covered by an existing doc gets a new source table.
167
+ // That table snapshots only the uncovered memberships.
168
+ if (plan.newTableMemberships != null) {
122
169
  const id = options.idGenerator ? options.idGenerator() : new bson.ObjectId();
123
- const sourceTable = new storage.SourceTable({
124
- id,
125
- ref,
126
- objectId,
127
- replicaIdColumns,
128
- snapshotComplete: false,
129
- bucketDataSources: uncoveredBucketIds.map((id) => bucketSourceById.get(id)),
130
- parameterLookupSources: uncoveredLookupIds.map((id) => parameterLookupSourceById.get(id))
131
- });
132
- sourceTable.syncData = uncoveredBucketIds.length > 0;
133
- sourceTable.syncParameters = uncoveredLookupIds.length > 0;
134
- sourceTable.syncEvent = triggersEvent;
135
- const createDoc = {
136
- _id: id,
137
- connection_id,
138
- relation_id: objectId,
139
- schema_name: schema,
140
- table_name: name,
141
- replica_id_columns: null,
142
- replica_id_columns2: normalizedReplicaIdColumns,
143
- snapshot_done: false,
144
- snapshot_status: undefined,
145
- bucket_data_source_ids: uncoveredBucketIds,
146
- parameter_lookup_source_ids: uncoveredLookupIds
147
- };
148
- await col.insertOne(createDoc, { session });
149
- initializeSourceRecordsFor.push(createDoc._id);
150
- retainedDocIds.push(createDoc._id);
151
- tables.push(sourceTable);
170
+ const { doc, table } = createNewSourceTable(id, plan.newTableMemberships, context);
171
+ await col.insertOne(doc, { session });
172
+ await this.db.initializeSourceRecordsCollection(this.replicationStreamId, doc._id, session);
173
+ plan.tables.push(table);
152
174
  }
153
- const conflictFilter = [{ schema_name: schema, table_name: name }];
154
- if (objectId != null) {
155
- conflictFilter.push({ relation_id: objectId });
156
- }
157
- const dropTables = await col
158
- .find({
159
- connection_id,
160
- _id: { $nin: retainedDocIds },
161
- $or: conflictFilter
162
- }, { session })
163
- .toArray();
164
175
  result = {
165
- tables,
166
- dropTables: dropTables.map((doc) => this.sourceTableFromDocument(doc, connectionTag, syncRules))
176
+ tables: plan.tables,
177
+ dropTables: plan.dropDocs.map((doc) => sourceTableFromDocument(doc, context.connectionTag, syncConfig, mapping, eventById))
167
178
  };
168
179
  });
169
- for (const sourceTableId of initializeSourceRecordsFor) {
170
- await this.db.initializeSourceRecordsCollection(this.group_id, sourceTableId);
171
- }
172
180
  return result;
173
181
  }
174
- sourceTableFromDocument(doc, connectionTag, syncRules, memberships) {
175
- const resolvedMemberships = memberships ?? this.sourceTableMembershipsFromDocument(doc, syncRules);
176
- const table = new storage.SourceTable({
177
- id: doc._id,
178
- ref: {
179
- connectionTag,
180
- schema: doc.schema_name,
181
- name: doc.table_name
182
- },
183
- objectId: doc.relation_id,
184
- replicaIdColumns: doc.replica_id_columns2.map((c) => ({ name: c.name, typeId: c.type_oid, type: c.type })),
185
- snapshotComplete: doc.snapshot_done ?? true,
186
- bucketDataSources: resolvedMemberships.bucketDataSources,
187
- parameterLookupSources: resolvedMemberships.parameterLookupSources
188
- });
189
- table.syncData = table.bucketDataSources.length > 0;
190
- table.syncParameters = table.parameterLookupSources.length > 0;
191
- table.syncEvent = syncRules.tableTriggersEvent(table.ref);
192
- table.snapshotStatus =
193
- doc.snapshot_status == null
194
- ? undefined
195
- : {
196
- lastKey: doc.snapshot_status.last_key?.buffer ?? null,
197
- totalEstimatedCount: doc.snapshot_status.total_estimated_count,
198
- replicatedCount: doc.snapshot_status.replicated_count
199
- };
200
- return table;
201
- }
202
- sourceTableMembershipsFromDocument(doc, syncRules) {
203
- const bucketDataSourceIds = new Set(doc.bucket_data_source_ids);
204
- const parameterLookupSourceIds = new Set(doc.parameter_lookup_source_ids);
205
- return {
206
- bucketDataSources: syncRules.bucketDataSources.filter((source) => bucketDataSourceIds.has(this.mapping.bucketSourceId(source))),
207
- parameterLookupSources: syncRules.bucketParameterLookupSources.filter((source) => parameterLookupSourceIds.has(this.mapping.parameterLookupId(source)))
208
- };
209
- }
210
182
  async getSourceTableStatus(table) {
211
183
  const doc = (await this.db
212
- .commonSourceTables(this.group_id)
184
+ .sourceTables(this.replicationStreamId)
213
185
  .findOne({ _id: mongoTableId(table.id) }, { session: this.session }));
214
186
  if (doc == null) {
215
187
  return null;
216
188
  }
217
- return this.sourceTableFromDocument(doc, table.ref.connectionTag, this.sync_rules);
189
+ return sourceTableFromDocument(doc, table.ref.connectionTag, this.sync_rules, this.mapping, this.options.parsedSyncConfig.eventById);
218
190
  }
219
191
  async commit(lsn, options) {
220
192
  const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
221
193
  await this.flush(options);
194
+ using _ = this.tracer.span('storage', 'commit');
222
195
  const now = new Date();
223
196
  await this.db.write_checkpoints.updateMany({
224
197
  processed_at_lsn: null,
@@ -230,94 +203,145 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
230
203
  }, {
231
204
  session: this.session
232
205
  });
233
- const preUpdateDocument = await this.db.sync_rules.findOne({
234
- _id: this.group_id,
235
- 'sync_configs._id': this.syncConfigId
206
+ const preUpdateDocument = (await this.db.sync_rules.findOne({
207
+ _id: this.replicationStreamId,
208
+ 'sync_configs._id': { $in: this.syncConfigIds }
236
209
  }, {
237
210
  session: this.session,
238
211
  projection: {
239
- snapshot_lsn: 1,
240
- sync_configs: {
241
- $elemMatch: {
242
- _id: this.syncConfigId
243
- }
244
- }
212
+ sync_configs: 1,
213
+ last_persisted_op: 1
245
214
  }
246
- });
247
- const state = preUpdateDocument?.sync_configs?.[0];
248
- if (state == null) {
249
- throw new ReplicationAssertionError(`Failed to update checkpoint - no matching sync_config for _id: ${this.group_id}/${this.syncConfigId.toHexString()}`);
215
+ }));
216
+ const states = preUpdateDocument?.sync_configs?.filter((config) => this.syncConfigIds.some((id) => id.equals(config._id))) ?? [];
217
+ if (states.length == 0) {
218
+ throw new ReplicationAssertionError(`Failed to update checkpoint - no matching sync_config for _id: ${this.replicationStreamId}/${this.syncConfigIds
219
+ .map((id) => id.toHexString())
220
+ .join(',')}`);
250
221
  }
251
- const checkpointState = calculateCheckpointState({
252
- lsn,
253
- snapshotDone: state.snapshot_done === true,
254
- lastCheckpointLsn: state.last_checkpoint_lsn,
255
- noCheckpointBefore: state.no_checkpoint_before,
256
- keepaliveOp: state.keepalive_op == null ? null : BigInt(state.keepalive_op),
257
- lastCheckpoint: state.last_checkpoint,
258
- persistedOp: this.persisted_op,
259
- createEmptyCheckpoints
260
- });
261
- const updateSet = {
262
- last_keepalive_ts: now,
263
- last_fatal_error: null,
264
- last_fatal_error_ts: null,
265
- 'sync_configs.$[config].keepalive_op': checkpointState.newKeepaliveOp,
266
- 'sync_configs.$[config].last_checkpoint': checkpointState.newLastCheckpoint
267
- };
268
- if (checkpointState.checkpointCreated) {
269
- updateSet['sync_configs.$[config].last_checkpoint_lsn'] = lsn;
270
- updateSet['snapshot_lsn'] = null;
271
- updateSet['last_checkpoint_ts'] = now;
222
+ // The replication job / MongoBucketBatch must be constructed with all replicating (PROCESSING / ACTIVE)
223
+ // sync configs in the stream, otherwise we'll get inconsistencies. Configs in other states (e.g. STOP)
224
+ // remain embedded in the document and are ignored here.
225
+ const missingSyncConfig = preUpdateDocument.sync_configs.find((config) => [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE].includes(config.state) &&
226
+ !this.syncConfigIds.some((id) => id.equals(config._id)));
227
+ if (missingSyncConfig != null) {
228
+ throw new ReplicationAssertionError(`Replication job not configured for sync config ${missingSyncConfig._id}`);
272
229
  }
273
- await this.db.sync_rules.updateOne({
274
- _id: this.group_id,
275
- 'sync_configs._id': this.syncConfigId
276
- }, {
277
- $set: updateSet
278
- }, {
279
- session: this.session,
280
- arrayFilters: [{ 'config._id': this.syncConfigId }]
281
- });
282
- if (checkpointState.checkpointBlocked) {
283
- if (Date.now() - this.lastWaitingLogThrottledV3 > 5_000) {
284
- this.logger.info(`Waiting before creating checkpoint, currently at ${lsn} / ${checkpointState.newKeepaliveOp}. Current state: ${JSON.stringify({
230
+ // Effective head of the stream's op sequence.
231
+ // last_persisted_op is $max-advanced durably in the same transaction as every flush, and this
232
+ // read uses the same session, so it covers all ops persisted by this batch.
233
+ const newCheckpoint = preUpdateDocument?.last_persisted_op == null ? 0n : BigInt(preUpdateDocument.last_persisted_op);
234
+ let checkpointBlocked = false;
235
+ let checkpointCreated = false;
236
+ let checkpointLogState = null;
237
+ const unblockedConfigIds = [];
238
+ for (const state of states) {
239
+ if (state.last_checkpoint != null && state.last_checkpoint > newCheckpoint) {
240
+ // last_persisted_op is $max-advanced durably in the same transaction as every flush, and
241
+ // checkpoints are only ever created at that head, so a checkpoint past the head means the
242
+ // op sequence or the stored state is corrupt.
243
+ throw new ReplicationAssertionError(`Invariant violation: sync config ${state._id} has last_checkpoint ${state.last_checkpoint} > stream head ${newCheckpoint}`);
244
+ }
245
+ const canCheckpoint = canCheckpointState(lsn, {
246
+ snapshotDone: state.snapshot_done === true,
247
+ lastCheckpointLsn: state.last_checkpoint_lsn,
248
+ noCheckpointBefore: state.no_checkpoint_before
249
+ });
250
+ if (!canCheckpoint) {
251
+ checkpointBlocked = true;
252
+ // Log the first blocked config's state.
253
+ checkpointLogState ??= {
285
254
  snapshot_done: state.snapshot_done,
286
255
  last_checkpoint_lsn: state.last_checkpoint_lsn,
287
256
  no_checkpoint_before: state.no_checkpoint_before
288
- })}`);
257
+ };
258
+ continue;
259
+ }
260
+ checkpointCreated ||= createEmptyCheckpoints || state.last_checkpoint !== newCheckpoint;
261
+ unblockedConfigIds.push(state._id);
262
+ }
263
+ // Every commit advances the stream's resume position: commit() flushes first, so all
264
+ // source changes up to this lsn have been persisted, even when checkpoints are blocked.
265
+ // In the future we could also advance this on flush, when the connector provides the
266
+ // current position (see setResumeLsn, which connectors may already call after flushing).
267
+ const resumeLsnUpdate = { resume_lsn: lsn };
268
+ if (unblockedConfigIds.length > 0) {
269
+ // All unblocked configs get the SAME new value, so we apply it with a single updateOne
270
+ // (single-document atomicity).
271
+ const updateSet = {
272
+ last_keepalive_ts: now,
273
+ last_fatal_error: null,
274
+ last_fatal_error_ts: null
275
+ };
276
+ // Only advance checkpoint fields when an actual (non-empty) checkpoint is created, matching
277
+ // the previous per-config / v1 behaviour.
278
+ if (checkpointCreated) {
279
+ updateSet['sync_configs.$[config].last_checkpoint'] = newCheckpoint;
280
+ updateSet['sync_configs.$[config].last_checkpoint_lsn'] = lsn;
281
+ updateSet['last_checkpoint_ts'] = now;
282
+ }
283
+ await this.db.sync_rules.updateOne({
284
+ _id: this.replicationStreamId,
285
+ 'sync_configs._id': { $in: unblockedConfigIds }
286
+ }, { $set: updateSet, $max: resumeLsnUpdate }, {
287
+ session: this.session,
288
+ arrayFilters: checkpointCreated ? [{ 'config._id': { $in: unblockedConfigIds } }] : undefined
289
+ });
290
+ }
291
+ else {
292
+ // All selected configs are blocked - only update keepalive/error tracking and the
293
+ // resume position.
294
+ await this.db.sync_rules.updateOne({
295
+ _id: this.replicationStreamId
296
+ }, {
297
+ $set: {
298
+ last_keepalive_ts: now,
299
+ last_fatal_error: null,
300
+ last_fatal_error_ts: null
301
+ },
302
+ $max: resumeLsnUpdate
303
+ }, { session: this.session });
304
+ }
305
+ if (checkpointBlocked) {
306
+ if (Date.now() - this.lastWaitingLogThrottledV3 > 5_000) {
307
+ this.logger.info(`Waiting before creating checkpoint, currently at ${lsn}. Current state: ${JSON.stringify(checkpointLogState)}`);
289
308
  this.lastWaitingLogThrottledV3 = Date.now();
290
309
  }
291
310
  }
292
311
  else {
293
- if (checkpointState.checkpointCreated) {
294
- this.logger.debug(`Created checkpoint at ${lsn} / ${checkpointState.newLastCheckpoint}`);
312
+ if (checkpointCreated) {
313
+ this.logger.debug(`Created checkpoint at ${lsn}`);
295
314
  }
296
315
  await this.autoActivateV3(lsn);
316
+ // All configs are now checkpointed at newCheckpoint (the stream head).
317
+ await this.sourceRecordStore.postCommitCleanup(newCheckpoint, this.logger);
318
+ }
319
+ if (checkpointCreated) {
297
320
  await this.db.notifyCheckpoint();
298
- this.persisted_op = null;
299
- this.last_checkpoint_lsn = lsn;
300
- if (checkpointState.newLastCheckpoint != null) {
301
- await this.sourceRecordStore.postCommitCleanup(checkpointState.newLastCheckpoint, this.logger);
302
- }
303
321
  }
304
322
  return {
305
- checkpointBlocked: checkpointState.checkpointBlocked,
306
- checkpointCreated: checkpointState.checkpointCreated
323
+ checkpointBlocked,
324
+ checkpointCreated
307
325
  };
308
326
  }
309
327
  async keepalive(lsn) {
310
328
  return await this.commit(lsn, { createEmptyCheckpoints: true });
311
329
  }
312
330
  async setResumeLsn(lsn) {
331
+ using _ = this.tracer.span('storage', 'set_resume_lsn');
313
332
  await this.db.sync_rules.updateOne({
314
- _id: this.group_id,
315
- 'sync_configs._id': this.syncConfigId
333
+ _id: this.replicationStreamId
316
334
  }, {
317
335
  $set: {
318
- snapshot_lsn: lsn
336
+ resume_lsn: lsn
319
337
  }
320
- }, { session: this.session });
338
+ }, {
339
+ session: this.session,
340
+ // Losing occasional resume LSN is fine. That may mean reprocessing
341
+ // some source changes in some edge cases, which is not an issue since
342
+ // changes are processed in an idempotent way.
343
+ writeConcern: { w: 1 }
344
+ });
321
345
  }
322
346
  async autoActivateV3(lsn) {
323
347
  if (!this.needsActivationV3) {
@@ -325,30 +349,32 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
325
349
  }
326
350
  const session = this.session;
327
351
  let activated = false;
352
+ let needsFutureActivationCheck = true;
328
353
  await session.withTransaction(async () => {
354
+ // Reset on transaction retries.
355
+ needsFutureActivationCheck = true;
356
+ activated = false;
329
357
  const doc = await this.db.sync_rules.findOne({
330
- _id: this.group_id,
331
- 'sync_configs._id': this.syncConfigId
358
+ _id: this.replicationStreamId,
359
+ 'sync_configs._id': { $in: this.syncConfigIds }
332
360
  }, {
333
361
  session,
334
362
  projection: {
335
363
  state: 1,
336
- sync_configs: {
337
- $elemMatch: {
338
- _id: this.syncConfigId
339
- }
340
- }
364
+ sync_configs: 1
341
365
  }
342
366
  });
343
- const state = doc?.sync_configs?.[0];
344
- if (doc &&
345
- doc.state == storage.SyncRuleState.PROCESSING &&
346
- state?.state == storage.SyncRuleState.PROCESSING &&
347
- state.snapshot_done &&
348
- state.last_checkpoint != null) {
367
+ const states = doc?.sync_configs?.filter((config) => this.syncConfigIds.some((id) => id.equals(config._id))) ?? [];
368
+ if (doc == null || states.length == 0) {
369
+ return;
370
+ }
371
+ const processingStates = states.filter((state) => state.state == storage.SyncRuleState.PROCESSING);
372
+ if (doc.state == storage.SyncRuleState.PROCESSING &&
373
+ processingStates.length == states.length &&
374
+ states.every((state) => state.snapshot_done && state.last_checkpoint != null)) {
349
375
  await this.db.sync_rules.updateOne({
350
- _id: this.group_id,
351
- 'sync_configs._id': this.syncConfigId
376
+ _id: this.replicationStreamId,
377
+ 'sync_configs._id': { $in: this.syncConfigIds }
352
378
  }, {
353
379
  $set: {
354
380
  state: storage.SyncRuleState.ACTIVE,
@@ -356,16 +382,39 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
356
382
  }
357
383
  }, {
358
384
  session,
359
- arrayFilters: [{ 'config._id': this.syncConfigId }]
385
+ arrayFilters: [{ 'config._id': { $in: this.syncConfigIds } }]
360
386
  });
361
387
  await this.db.sync_rules.updateMany({
362
- _id: { $ne: this.group_id },
388
+ _id: { $ne: this.replicationStreamId },
363
389
  state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
364
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP), { session });
390
+ }, stopReplicationStreamPipeline(), { session });
391
+ activated = true;
392
+ }
393
+ else if (doc.state == storage.SyncRuleState.ACTIVE &&
394
+ processingStates.length > 0 &&
395
+ processingStates.every((state) => state.snapshot_done && state.last_checkpoint != null)) {
396
+ await this.db.sync_rules.updateOne({
397
+ _id: this.replicationStreamId,
398
+ 'sync_configs._id': { $in: processingStates.map((state) => state._id) }
399
+ }, {
400
+ $set: {
401
+ 'sync_configs.$[activeConfig].state': storage.SyncRuleState.STOP,
402
+ 'sync_configs.$[processingConfig].state': storage.SyncRuleState.ACTIVE
403
+ }
404
+ }, {
405
+ session,
406
+ arrayFilters: [
407
+ { 'activeConfig.state': storage.SyncRuleState.ACTIVE },
408
+ { 'processingConfig._id': { $in: processingStates.map((state) => state._id) } }
409
+ ]
410
+ });
365
411
  activated = true;
366
412
  }
367
- else if (doc?.state != storage.SyncRuleState.PROCESSING) {
368
- this.needsActivationV3 = false;
413
+ else if (doc.state != storage.SyncRuleState.PROCESSING && doc.state != storage.SyncRuleState.ACTIVE) {
414
+ needsFutureActivationCheck = false;
415
+ }
416
+ else if (doc.state == storage.SyncRuleState.ACTIVE && processingStates.length == 0) {
417
+ needsFutureActivationCheck = false;
369
418
  }
370
419
  });
371
420
  if (activated) {
@@ -373,11 +422,14 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
373
422
  await this.db.notifyCheckpoint();
374
423
  this.needsActivationV3 = false;
375
424
  }
425
+ else if (!needsFutureActivationCheck) {
426
+ this.needsActivationV3 = false;
427
+ }
376
428
  }
377
429
  async markAllSnapshotDone(no_checkpoint_before_lsn) {
378
430
  await this.db.sync_rules.updateOne({
379
- _id: this.group_id,
380
- 'sync_configs._id': this.syncConfigId
431
+ _id: this.replicationStreamId,
432
+ 'sync_configs._id': { $in: this.syncConfigIds }
381
433
  }, {
382
434
  $set: {
383
435
  'sync_configs.$[config].snapshot_done': true,
@@ -388,18 +440,22 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
388
440
  }
389
441
  }, {
390
442
  session: this.session,
391
- arrayFilters: [{ 'config._id': this.syncConfigId }]
443
+ arrayFilters: [{ 'config._id': { $in: this.syncConfigIds } }]
392
444
  });
393
445
  }
394
446
  async markSnapshotDone(no_checkpoint_before_lsn, options) {
395
447
  await this.withTransaction(async () => {
396
448
  // Protect against race conditions
397
- const count = await this.db.sourceTablesV3(this.group_id).countDocuments({
398
- snapshot_done: false
399
- }, { session: this.session });
400
- if (count > 0) {
449
+ const blockingTables = await this.db
450
+ .sourceTables(this.replicationStreamId)
451
+ .find(this.snapshotBlockingSourceTablesFilter(), {
452
+ session: this.session,
453
+ projection: { schema_name: 1, table_name: 1 }
454
+ })
455
+ .toArray();
456
+ if (blockingTables.length > 0) {
401
457
  if (options?.throwOnConflict ?? true) {
402
- throw new ReplicationAssertionError(`Cannot mark snapshot done while ${count} source table${count == 1 ? '' : 's'} still require snapshotting`);
458
+ throw new ReplicationAssertionError(`Cannot mark snapshot done while source tables still require snapshotting. Tables: ${blockingTables.map((t) => `${t.schema_name}.${t.table_name}`).join(', ')}`);
403
459
  }
404
460
  else {
405
461
  return;
@@ -408,24 +464,29 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
408
464
  await this.markAllSnapshotDone(no_checkpoint_before_lsn);
409
465
  });
410
466
  }
411
- async markTableSnapshotRequired(_table) {
467
+ async markTableSnapshotRequired(table) {
468
+ const syncConfigIds = this.relevantSyncConfigIds(table);
469
+ if (syncConfigIds.length == 0) {
470
+ return;
471
+ }
412
472
  await this.db.sync_rules.updateOne({
413
- _id: this.group_id,
414
- 'sync_configs._id': this.syncConfigId
473
+ _id: this.replicationStreamId,
474
+ 'sync_configs._id': { $in: syncConfigIds }
415
475
  }, {
416
476
  $set: {
417
477
  'sync_configs.$[config].snapshot_done': false
418
478
  }
419
479
  }, {
420
480
  session: this.session,
421
- arrayFilters: [{ 'config._id': this.syncConfigId }]
481
+ arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
422
482
  });
423
483
  }
424
484
  async markTableSnapshotDone(tables, no_checkpoint_before_lsn) {
425
485
  const session = this.session;
426
486
  const ids = tables.map((table) => mongoTableId(table.id));
487
+ const syncConfigIds = this.relevantSyncConfigIdsForTables(tables);
427
488
  await this.withTransaction(async () => {
428
- await this.db.commonSourceTables(this.group_id).updateMany({ _id: { $in: ids } }, {
489
+ await this.db.sourceTables(this.replicationStreamId).updateMany({ _id: { $in: ids } }, {
429
490
  $set: {
430
491
  snapshot_done: true
431
492
  },
@@ -433,10 +494,10 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
433
494
  snapshot_status: 1
434
495
  }
435
496
  }, { session });
436
- if (no_checkpoint_before_lsn != null) {
497
+ if (no_checkpoint_before_lsn != null && syncConfigIds.length > 0) {
437
498
  await this.db.sync_rules.updateOne({
438
- _id: this.group_id,
439
- 'sync_configs._id': this.syncConfigId
499
+ _id: this.replicationStreamId,
500
+ 'sync_configs._id': { $in: syncConfigIds }
440
501
  }, {
441
502
  $set: {
442
503
  last_keepalive_ts: new Date()
@@ -446,7 +507,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
446
507
  }
447
508
  }, {
448
509
  session: this.session,
449
- arrayFilters: [{ 'config._id': this.syncConfigId }]
510
+ // Only set for sync configs that use this table
511
+ arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
450
512
  });
451
513
  }
452
514
  });
@@ -456,5 +518,74 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
456
518
  return copy;
457
519
  });
458
520
  }
521
+ async batchCreateCustomWriteCheckpoints(session, opId) {
522
+ if (this.write_checkpoint_batch.length == 0) {
523
+ return;
524
+ }
525
+ const checkpointsByEvent = Map.groupBy(this.write_checkpoint_batch, (checkpoint) => this.validateCustomCheckpointEventId(checkpoint.event_id));
526
+ for (const [eventId, checkpoints] of checkpointsByEvent) {
527
+ await this.batchCreateEventCustomWriteCheckpoints(session, opId, eventId, checkpoints);
528
+ }
529
+ }
530
+ async prepareCustomWriteCheckpoints() {
531
+ // Most events never produce custom checkpoints, so create collections lazily
532
+ // only for the event ids present in this batch. This hook runs before the
533
+ // replication transaction because MongoDB cannot create indexes within it.
534
+ const eventIds = new Set(this.write_checkpoint_batch.map((checkpoint) => this.validateCustomCheckpointEventId(checkpoint.event_id)));
535
+ for (const eventId of eventIds) {
536
+ if (this.initializedCustomCheckpointEventIds.has(eventId)) {
537
+ continue;
538
+ }
539
+ await this.db.initializeCustomCheckpointRequestsCollection({
540
+ replicationStreamId: this.replicationStreamId,
541
+ eventId
542
+ });
543
+ this.initializedCustomCheckpointEventIds.add(eventId);
544
+ }
545
+ }
546
+ validateCustomCheckpointEventId(eventId) {
547
+ if (eventId == null) {
548
+ throw new ServiceAssertionError('V3 custom checkpoints require an event definition id');
549
+ }
550
+ if (!this.knownEventIds.has(eventId)) {
551
+ throw new ServiceAssertionError(`Unknown custom checkpoint event definition ${eventId}`);
552
+ }
553
+ return eventId;
554
+ }
555
+ async batchCreateEventCustomWriteCheckpoints(session, opId, eventId, checkpoints) {
556
+ await this.db
557
+ .customCheckpointRequests({
558
+ eventId,
559
+ replicationStreamId: this.replicationStreamId
560
+ })
561
+ .bulkWrite(checkpoints.map((checkpointOptions) => {
562
+ const set = {
563
+ user_id: checkpointOptions.user_id,
564
+ checkpoint: checkpointOptions.checkpoint,
565
+ op_id: opId
566
+ };
567
+ if (checkpointOptions.checkpoint_requested_at != null) {
568
+ set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
569
+ }
570
+ return {
571
+ updateOne: {
572
+ filter: {
573
+ user_id: checkpointOptions.user_id
574
+ },
575
+ update: {
576
+ $set: set,
577
+ ...(checkpointOptions.checkpoint_requested_at == null
578
+ ? {
579
+ $unset: {
580
+ checkpoint_requested_at: 1
581
+ }
582
+ }
583
+ : {})
584
+ },
585
+ upsert: true
586
+ }
587
+ };
588
+ }), { session });
589
+ }
459
590
  }
460
591
  //# sourceMappingURL=MongoBucketBatchV3.js.map