@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
@@ -3,12 +3,14 @@ import { mongo } from '@powersync/lib-service-mongodb';
3
3
  import { ServiceAssertionError } from '@powersync/lib-services-framework';
4
4
  import {
5
5
  CheckpointChanges,
6
+ CompactInitialReplicationOptions,
7
+ CompactInitialReplicationResults,
6
8
  GetCheckpointChangesOptions,
7
9
  InternalOpId,
8
10
  internalToExternalOpId,
9
- maxLsn,
10
11
  ParameterSetLimitExceededError,
11
12
  ProtocolOpId,
13
+ SingleSyncConfigBucketDefinitionMapping,
12
14
  storage,
13
15
  utils
14
16
  } from '@powersync/service-core';
@@ -17,57 +19,125 @@ import { ParameterLookupRows, ScopedParameterLookup, SqliteJsonRow } from '@powe
17
19
  import * as bson from 'bson';
18
20
  import { mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '../../../utils/util.js';
19
21
  import { MongoBucketStorage } from '../../MongoBucketStorage.js';
22
+ import { BucketDataDoc } from '../common/BucketDataDoc.js';
20
23
  import {
21
- MongoSyncBucketStorageCheckpoint,
22
- MongoSyncBucketStorageContext
23
- } from '../common/MongoSyncBucketStorageContext.js';
24
- import { MongoBucketBatchOptions } from '../MongoBucketBatch.js';
24
+ MongoGetCheckpointChangesOptions,
25
+ MongoSyncBucketStorageCheckpoint
26
+ } from '../common/MongoSyncBucketStorageCheckpoint.js';
25
27
  import { MongoChecksums } from '../MongoChecksums.js';
26
28
  import { MongoCompactOptions, MongoCompactor } from '../MongoCompactor.js';
27
29
  import { MongoParameterCompactor } from '../MongoParameterCompactor.js';
30
+ import { MongoPersistedReplicationStream } from '../MongoPersistedReplicationStream.js';
28
31
  import {
29
- MongoPersistedSyncRulesContentV1,
30
- MongoPersistedSyncRulesContentV3
31
- } from '../MongoPersistedSyncRulesContent.js';
32
- import { MongoSyncBucketStorage, MongoSyncBucketStorageOptions } from '../MongoSyncBucketStorage.js';
32
+ MongoCheckpointState,
33
+ MongoSyncBucketStorage,
34
+ MongoSyncBucketStorageOptions
35
+ } from '../MongoSyncBucketStorage.js';
36
+ import { MongoCheckpointAPIOptions } from '../MongoWriteCheckpointAPI.js';
37
+ import { loadBucketDataDocument, maxOpId } from './bucket-format.js';
33
38
  import {
34
39
  BucketDataDocumentV3,
35
40
  BucketParameterDocumentV3,
36
- loadBucketDataDocumentV3,
41
+ deserializeParameterLookup,
37
42
  ReplicationStreamDocumentV3,
43
+ serializeParameterLookup,
38
44
  SyncRuleConfigStateV3
39
45
  } from './models.js';
40
46
  import { MongoBucketBatchV3 } from './MongoBucketBatchV3.js';
41
47
  import { MongoChecksumsV3 } from './MongoChecksumsV3.js';
42
48
  import { MongoCompactorV3 } from './MongoCompactorV3.js';
43
49
  import { MongoParameterCompactorV3 } from './MongoParameterCompactorV3.js';
44
- import { deserializeParameterLookupV3, serializeParameterLookupV3 } from './MongoParameterLookupV3.js';
50
+ import { MongoStoppedSyncConfigCleanup } from './MongoStoppedSyncConfigCleanup.js';
51
+ import { MongoWriteCheckpointAPIV3 } from './MongoWriteCheckpointAPIV3.js';
52
+ import { hydrateBucketDataDocuments } from './object-storage/BucketDataObjectStorage.js';
53
+ import { ObjectStorage } from './object-storage/ObjectStorage.js';
54
+ import { ObjectStorageLifecycle } from './object-storage/ObjectStorageLifecycle.js';
55
+ import { ObjectStorageUsage } from './object-storage/ObjectStorageUsage.js';
45
56
  import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
46
57
 
58
+ export interface MongoSyncBucketStorageContextV3 {
59
+ db: VersionedPowerSyncMongoV3;
60
+ replicationStreamId: number;
61
+ readPreference?: mongo.ReadPreference;
62
+ objectStorage: ObjectStorage | undefined;
63
+ /**
64
+ * Persisted mapping of the single sync config that read operations are served from.
65
+ *
66
+ * Implemented as a lazy getter: accessing it on a storage instance with multiple sync
67
+ * configs throws, but operations that don't use it remain unaffected.
68
+ */
69
+ readonly mapping: SingleSyncConfigBucketDefinitionMapping;
70
+ }
71
+
72
+ const BUCKET_DATA_FETCH_BATCH_LIMIT_BYTES = 16 * 1024 * 1024;
73
+
74
+ /**
75
+ * Keep the documents hydrated for one sync response within a bounded payload or slightly higher.
76
+ *
77
+ * This deserializes on-demand, so no deserialization is performed for discarded data.
78
+ */
79
+ function cutBucketDataBatch(rawDocuments: Buffer[]): {
80
+ documents: BucketDataDocumentV3[];
81
+ wasCut: boolean;
82
+ } {
83
+ let cumulativeBytes = 0;
84
+ let documents: BucketDataDocumentV3[] = [];
85
+ for (const raw of rawDocuments) {
86
+ const doc = bson.deserialize(raw, storage.BSON_DESERIALIZE_INTERNAL_OPTIONS) as BucketDataDocumentV3;
87
+ documents.push(doc);
88
+ cumulativeBytes += doc.size;
89
+ if (cumulativeBytes > BUCKET_DATA_FETCH_BATCH_LIMIT_BYTES) {
90
+ return {
91
+ documents,
92
+ wasCut: documents.length < rawDocuments.length
93
+ };
94
+ }
95
+ }
96
+ return { documents, wasCut: false };
97
+ }
98
+
99
+ function extractRowsFromDocument(
100
+ doc: BucketDataDocumentV3,
101
+ context: { replicationStreamId: number; definitionId: string },
102
+ bucketStart: InternalOpId,
103
+ endOpId: InternalOpId
104
+ ): BucketDataDoc[] {
105
+ const rows: BucketDataDoc[] = [];
106
+ for (const row of loadBucketDataDocument(context, doc)) {
107
+ // In theory a binary search could be faster than a linear scan to find the start.
108
+ // In practice, most cases should not filter out anything here.
109
+ if (row.o <= bucketStart) {
110
+ continue;
111
+ }
112
+ if (row.o > endOpId) {
113
+ break;
114
+ }
115
+
116
+ rows.push(row);
117
+ }
118
+ return rows;
119
+ }
120
+
47
121
  export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
48
- // Declare types to be more specific
49
122
  declare readonly db: VersionedPowerSyncMongoV3;
50
123
  declare readonly checksums: MongoChecksumsV3;
51
124
 
52
- private readonly syncRulesV3: MongoPersistedSyncRulesContentV3;
53
-
54
125
  constructor(
55
126
  factory: MongoBucketStorage,
56
- group_id: number,
57
- sync_rules: MongoPersistedSyncRulesContentV1,
58
- slot_name: string,
127
+ replicationStreamId: number,
128
+ replicationStream: MongoPersistedReplicationStream,
129
+ replicationStreamName: string,
59
130
  writeCheckpointMode: storage.WriteCheckpointMode | undefined,
60
131
  options: MongoSyncBucketStorageOptions
61
132
  ) {
62
- super(factory, group_id, sync_rules, slot_name, writeCheckpointMode, options);
63
- if (!(sync_rules instanceof MongoPersistedSyncRulesContentV3)) {
133
+ super(factory, replicationStreamId, replicationStream, replicationStreamName, writeCheckpointMode, options);
134
+ if (replicationStream.syncConfigIds.length == 0) {
64
135
  throw new ServiceAssertionError('Missing sync config id for storage v3');
65
136
  }
66
- this.syncRulesV3 = sync_rules;
67
137
  }
68
138
 
69
- private get syncConfigId(): bson.ObjectId {
70
- return this.syncRulesV3.syncConfigId;
139
+ private get syncConfigIds(): bson.ObjectId[] {
140
+ return this.replicationStream.syncConfigIds;
71
141
  }
72
142
 
73
143
  private get syncRulesCollection(): mongo.Collection<ReplicationStreamDocumentV3> {
@@ -76,10 +146,10 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
76
146
 
77
147
  private syncConfigMatch(extra: mongo.Document = {}): mongo.Filter<ReplicationStreamDocumentV3> {
78
148
  return {
79
- _id: this.group_id,
149
+ _id: this.replicationStreamId,
80
150
  sync_configs: {
81
151
  $elemMatch: {
82
- _id: this.syncConfigId,
152
+ _id: { $in: this.syncConfigIds },
83
153
  ...extra
84
154
  }
85
155
  }
@@ -89,44 +159,27 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
89
159
  private syncConfigProjection(extra: mongo.Document = {}): mongo.Document {
90
160
  return {
91
161
  ...extra,
92
- sync_configs: {
93
- $elemMatch: {
94
- _id: this.syncConfigId
95
- }
96
- }
162
+ sync_configs: 1
97
163
  };
98
164
  }
99
165
 
100
- private syncConfigArrayFilters(): mongo.UpdateOptions['arrayFilters'] {
101
- return [{ 'config._id': this.syncConfigId }];
102
- }
103
-
104
- /**
105
- * For now, we only support a single sync config per replication stream.
106
- *
107
- * In the future we'll add support for multiple.
108
- */
109
- private selectedSyncConfig(
110
- doc: Pick<ReplicationStreamDocumentV3, 'sync_configs'> | null
111
- ): SyncRuleConfigStateV3 | null {
112
- return doc?.sync_configs?.[0] ?? null;
166
+ private selectedSyncConfigs(doc: Pick<ReplicationStreamDocumentV3, 'sync_configs'> | null): SyncRuleConfigStateV3[] {
167
+ return doc?.sync_configs?.filter((config) => this.syncConfigIds.some((id) => id.equals(config._id))) ?? [];
113
168
  }
114
169
 
115
170
  protected async initializeVersionStorage(): Promise<void> {
116
- const mapping = this.mapping;
117
- for (let source of mapping.allBucketDefinitionIds()) {
118
- const collection = this.db.bucketDataV3(this.group_id, source).collectionName;
119
- await this.db.db
120
- .createCollection(collection, { clusteredIndex: { name: '_id', unique: true, key: { _id: 1 } } })
121
- .catch((error) => {
122
- if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceExists') {
123
- return;
124
- }
125
- throw error;
126
- });
171
+ const storageIds = this.storageIds;
172
+ for (const source of storageIds.bucketDefinitionIds) {
173
+ const collection = this.db.bucketData(this.replicationStreamId, source).collectionName;
174
+ await this.db.db.createCollection(collection, {}).catch((error) => {
175
+ if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceExists') {
176
+ return;
177
+ }
178
+ throw error;
179
+ });
127
180
  }
128
- for (let indexId of mapping.allParameterIndexIds()) {
129
- await this.db.parameterIndexV3(this.group_id, indexId).createIndex(
181
+ for (const indexId of storageIds.parameterIndexIds) {
182
+ await this.db.parameterIndex(this.replicationStreamId, indexId).createIndex(
130
183
  {
131
184
  lookup: 1,
132
185
  key: 1,
@@ -140,10 +193,19 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
140
193
  }
141
194
 
142
195
  protected createMongoChecksums(options: MongoSyncBucketStorageOptions): MongoChecksums {
143
- return new MongoChecksumsV3(this.db, this.group_id, {
196
+ return new MongoChecksumsV3(this.db, this.replicationStreamId, {
144
197
  ...options.checksumOptions,
198
+ checksumCacheTtlMs: options.checksumCacheTtlMs,
145
199
  storageConfig: options?.storageConfig,
146
- mapping: this.sync_rules.mapping
200
+ syncConfigMapping: () => this.singleSyncConfigMapping()
201
+ });
202
+ }
203
+
204
+ protected override createWriteCheckpointAPI(options: MongoCheckpointAPIOptions): MongoWriteCheckpointAPIV3 {
205
+ return new MongoWriteCheckpointAPIV3({
206
+ ...options,
207
+ db: this.db,
208
+ syncConfigMapping: () => this.singleSyncConfigMapping()
147
209
  });
148
210
  }
149
211
 
@@ -151,57 +213,93 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
151
213
  return new MongoCompactorV3(this, this.db, options);
152
214
  }
153
215
 
216
+ override async compactInitialReplication(
217
+ options: CompactInitialReplicationOptions
218
+ ): Promise<CompactInitialReplicationResults> {
219
+ this.logger.info(`Compacting chunks after initial replication...`);
220
+ const start = Date.now();
221
+ const maxOpId = options.maxOpId ?? (await this.fetchPersistedOpHead()) ?? undefined;
222
+ const compactedBuckets = await this.createMongoCompactor({
223
+ ...options,
224
+ maxOpId,
225
+ compactChunksOnly: true,
226
+ logger: this.logger
227
+ }).compact();
228
+ this.logger.info(`Compacted chunks after initial replication in ${(Date.now() - start) / 1000}s`);
229
+ return { buckets: compactedBuckets };
230
+ }
231
+
154
232
  protected createMongoParameterCompactor(
155
233
  checkpoint: InternalOpId,
156
234
  options: storage.CompactOptions
157
235
  ): MongoParameterCompactor {
158
- return new MongoParameterCompactorV3(this.db, this.group_id, checkpoint, options);
236
+ return new MongoParameterCompactorV3(this.db, this.replicationStreamId, checkpoint, options);
159
237
  }
160
238
 
161
- protected createWriterImpl(batchOptions: MongoBucketBatchOptions): storage.BucketStorageBatch {
162
- return new MongoBucketBatchV3(batchOptions);
239
+ protected async fetchPersistedOpHead(): Promise<InternalOpId | null> {
240
+ const doc = await this.syncRulesCollection.findOne(
241
+ { _id: this.replicationStreamId },
242
+ { projection: { last_persisted_op: 1 } }
243
+ );
244
+ return doc?.last_persisted_op == null ? null : BigInt(doc.last_persisted_op);
163
245
  }
164
246
 
165
- protected async fetchCheckpointState(
166
- session: mongo.ClientSession
167
- ): Promise<{ checkpoint: bigint; lsn: string | null } | null> {
247
+ protected async createWriterImpl(options: storage.CreateWriterOptions): Promise<storage.BucketStorageBatch> {
248
+ const batchOptions = this.writerBatchOptions(options);
249
+ const doc = await this.syncRulesCollection.findOne(
250
+ { _id: this.replicationStreamId },
251
+ { projection: { resume_lsn: 1 } }
252
+ );
253
+
254
+ return new MongoBucketBatchV3({
255
+ ...batchOptions,
256
+ // The stream-level replication position - per-config checkpoint LSNs are consistency
257
+ // markers and do not affect where replication resumes.
258
+ resumeFromLsn: doc?.resume_lsn ?? null,
259
+ syncConfigIds: this.syncConfigIds
260
+ });
261
+ }
262
+
263
+ protected async fetchCheckpointState(session: mongo.ClientSession): Promise<MongoCheckpointState | null> {
168
264
  const doc = await this.syncRulesCollection.findOne(
169
265
  this.syncConfigMatch({
170
266
  state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
171
267
  }),
172
268
  {
173
269
  session,
174
- projection: this.syncConfigProjection()
270
+ // The invalidation fence must be read in the same snapshot as the checkpoint.
271
+ projection: this.syncConfigProjection({ 'parameter_compaction.checkpoint_changes_invalid_before': 1 })
175
272
  }
176
273
  );
177
- const syncConfig = this.selectedSyncConfig(doc);
178
- if (!syncConfig?.snapshot_done) {
274
+ // Checkpoints are served from the single active config. A PROCESSING config in the same
275
+ // stream (incremental reprocessing) does not affect checkpoints until it is activated.
276
+ const syncConfigs = this.selectedSyncConfigs(doc).filter((config) =>
277
+ [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED].includes(config.state)
278
+ );
279
+ if (syncConfigs.length > 1) {
280
+ // Activation atomically replaces the previous active config, so this cannot happen unless
281
+ // the stored state is corrupt.
282
+ throw new ServiceAssertionError(
283
+ `Expected a single active sync config, got ${syncConfigs.map((config) => config._id.toHexString()).join(', ')}`
284
+ );
285
+ }
286
+ const syncConfig = syncConfigs[0];
287
+ if (syncConfig == null || !syncConfig.snapshot_done) {
179
288
  return null;
180
289
  }
181
290
  return {
182
291
  checkpoint: syncConfig.last_checkpoint ?? 0n,
183
- lsn: syncConfig.last_checkpoint_lsn ?? null
184
- };
185
- }
186
-
187
- protected async getWriterSyncState() {
188
- const doc = await this.syncRulesCollection.findOne(this.syncConfigMatch(), {
189
- projection: this.syncConfigProjection({ snapshot_lsn: 1 })
190
- });
191
- const syncConfig = this.selectedSyncConfig(doc);
192
- const checkpointLsn = syncConfig?.last_checkpoint_lsn ?? null;
193
- return {
194
- lastCheckpointLsn: checkpointLsn,
195
- resumeFromLsn: maxLsn(checkpointLsn, doc?.snapshot_lsn),
196
- keepaliveOp: syncConfig?.keepalive_op ?? null,
197
- syncConfigId: this.syncConfigId
292
+ lsn: syncConfig.last_checkpoint_lsn ?? null,
293
+ // Stream-level state: shared by all sync configs. Defaults to 0n for streams that have
294
+ // never been compacted.
295
+ parameterChangesInvalidBefore: doc?.parameter_compaction?.checkpoint_changes_invalid_before ?? 0n
198
296
  };
199
297
  }
200
298
 
201
299
  protected async terminateSyncRuleState(): Promise<void> {
202
300
  await this.db.sync_rules.updateOne(
203
301
  {
204
- _id: this.group_id
302
+ _id: this.replicationStreamId
205
303
  },
206
304
  {
207
305
  $set: {
@@ -213,52 +311,83 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
213
311
  );
214
312
  }
215
313
 
216
- protected async getStatusImpl(): Promise<storage.SyncRuleStatus> {
314
+ protected async getStatusImpl(): Promise<storage.ReplicationStreamStatus> {
217
315
  const doc = await this.syncRulesCollection.findOne(this.syncConfigMatch(), {
218
- projection: this.syncConfigProjection({ state: 1, snapshot_lsn: 1, keepalive_op: 1 })
316
+ projection: this.syncConfigProjection({ resume_lsn: 1 })
219
317
  });
220
- const syncConfig = this.selectedSyncConfig(doc);
221
- if (doc == null || syncConfig == null) {
318
+ const syncConfigs = this.selectedSyncConfigs(doc);
319
+ if (doc == null || syncConfigs.length == 0) {
222
320
  throw new ServiceAssertionError('Cannot find replication stream status');
223
321
  }
224
322
 
225
323
  return {
226
- snapshot_done: syncConfig.snapshot_done ?? false,
227
- snapshot_lsn: doc.snapshot_lsn ?? null,
228
- active: doc.state == storage.SyncRuleState.ACTIVE && syncConfig.state == storage.SyncRuleState.ACTIVE,
229
- checkpoint_lsn: syncConfig.last_checkpoint_lsn ?? null,
230
- keepalive_op: syncConfig.keepalive_op ?? null
324
+ snapshotDone:
325
+ syncConfigs.every((config) => config.snapshot_done ?? false) &&
326
+ syncConfigs.every((config) => config.last_checkpoint_lsn != null),
327
+ resumeLsn: doc.resume_lsn ?? null
231
328
  };
232
329
  }
233
330
 
234
331
  protected async clearSyncRuleState(): Promise<void> {
332
+ // Clearing resets the entire replication stream (bucket data and the op sequence), so reset
333
+ // the checkpoint state for _all_ embedded sync configs, not only the ones selected for this
334
+ // storage instance. This maintains the invariant that no config has a last_checkpoint past
335
+ // the stream-level last_persisted_op.
235
336
  await this.syncRulesCollection.updateOne(
236
- this.syncConfigMatch(),
337
+ { _id: this.replicationStreamId },
237
338
  {
238
339
  $set: {
239
340
  persisted_lsn: null,
240
- 'sync_configs.$[config].snapshot_done': false,
241
- 'sync_configs.$[config].last_checkpoint_lsn': null,
242
- 'sync_configs.$[config].last_checkpoint': null,
243
- 'sync_configs.$[config].no_checkpoint_before': null,
244
- 'sync_configs.$[config].keepalive_op': null
341
+ 'sync_configs.$[].snapshot_done': false,
342
+ 'sync_configs.$[].last_checkpoint_lsn': null,
343
+ 'sync_configs.$[].last_checkpoint': null,
344
+ 'sync_configs.$[].no_checkpoint_before': null
245
345
  },
246
346
  $unset: {
247
- snapshot_lsn: 1
347
+ resume_lsn: 1,
348
+ last_persisted_op: 1,
349
+ parameter_compaction: 1
248
350
  }
249
351
  },
250
352
  {
251
- maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS,
252
- arrayFilters: this.syncConfigArrayFilters()
353
+ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS
253
354
  }
254
355
  );
255
356
  }
256
357
 
257
- protected override get versionContext(): MongoSyncBucketStorageContext<VersionedPowerSyncMongoV3> {
358
+ /**
359
+ * The persisted mapping of the single sync config that read operations are served from.
360
+ *
361
+ * Reads always operate on a single sync config: read-path storage instances are
362
+ * constructed for the active sync config only (see MongoBucketStorage.getActiveSyncConfig),
363
+ * and checkpoints are served from the single active config (see fetchCheckpointState).
364
+ * Within a single sync config, unique names are the persistence key of its rule mapping,
365
+ * so its name-keyed {@link SingleSyncConfigBucketDefinitionMapping} resolves sources from
366
+ * any parse of that config unambiguously - no parsed-set identity is required.
367
+ *
368
+ * Throws on storage instances with multiple sync configs (replication-side instances),
369
+ * which must not serve reads.
370
+ */
371
+ private singleSyncConfigMapping(): SingleSyncConfigBucketDefinitionMapping {
372
+ const content = this.replicationStream.syncConfigContent;
373
+ if (content.length != 1) {
374
+ throw new ServiceAssertionError(
375
+ `Read operations require a storage instance with a single sync config, got ${content.length}`
376
+ );
377
+ }
378
+ return content[0].mapping;
379
+ }
380
+
381
+ protected get versionContext(): MongoSyncBucketStorageContextV3 {
382
+ const self = this;
258
383
  return {
259
384
  db: this.db,
260
- group_id: this.group_id,
261
- mapping: this.mapping
385
+ objectStorage: this.objectStorage,
386
+ replicationStreamId: this.replicationStreamId,
387
+ readPreference: this.readPreference,
388
+ get mapping() {
389
+ return self.singleSyncConfigMapping();
390
+ }
262
391
  };
263
392
  }
264
393
 
@@ -271,37 +400,52 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
271
400
  }
272
401
 
273
402
  protected getBucketDataBatchImpl(
274
- checkpoint: utils.InternalOpId,
403
+ checkpoint: MongoSyncBucketStorageCheckpoint,
275
404
  dataBuckets: storage.BucketDataRequest[],
276
405
  options?: storage.BucketDataBatchOptions
277
- ): AsyncIterable<storage.SyncBucketDataChunk> {
406
+ ): AsyncIterable<storage.SyncBucketDataChunk | storage.SyncBucketDataBatchEnd> {
278
407
  return getBucketDataBatchV3(this.versionContext, checkpoint, dataBuckets, options);
279
408
  }
280
409
 
281
- protected async clearBucketData(_signal?: AbortSignal): Promise<void> {
282
- for (const collection of await this.db.listBucketDataCollectionsV3(this.group_id)) {
410
+ protected async clearBucketData(signal?: AbortSignal): Promise<void> {
411
+ for (const collection of await this.db.listBucketDataCollections(this.replicationStreamId)) {
283
412
  await collection.drop();
284
413
  }
414
+ const usage = new ObjectStorageUsage(this.db, this.replicationStreamId);
415
+ await this.db.client.withSession((session) => session.withTransaction(() => usage.removeStream(session)));
416
+ if (this.objectStorage) {
417
+ const lifecycle = new ObjectStorageLifecycle(this.db, this.replicationStreamId, this.objectStorage);
418
+ await lifecycle.deletePrefix(lifecycle.streamPrefix(), { signal });
419
+ }
420
+ await this.db
421
+ .pendingObjectStorageDeletes(this.replicationStreamId)
422
+ .drop({ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS })
423
+ .catch((error) => {
424
+ if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
425
+ return;
426
+ }
427
+ throw error;
428
+ });
285
429
  }
286
430
 
287
431
  protected async clearParameterIndexes(_signal?: AbortSignal): Promise<void> {
288
- for (const collection of await this.db.listParameterIndexCollectionsV3(this.group_id)) {
432
+ for (const collection of await this.db.listParameterIndexCollections(this.replicationStreamId)) {
289
433
  await collection.collection.drop();
290
434
  }
291
435
  }
292
436
 
293
437
  protected async clearSourceRecords(_signal?: AbortSignal): Promise<void> {
294
- for (const collection of await this.db.listSourceRecordCollectionsV3(this.group_id)) {
438
+ for (const collection of await this.db.listSourceRecordCollections(this.replicationStreamId)) {
295
439
  await collection.drop();
296
440
  }
297
441
  }
298
442
 
299
443
  protected async clearBucketState(_signal?: AbortSignal): Promise<void> {
300
444
  await this.db
301
- .bucketStateV3(this.group_id)
445
+ .bucketState(this.replicationStreamId)
302
446
  .drop({ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS })
303
447
  .catch((error) => {
304
- if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
448
+ if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
305
449
  return;
306
450
  }
307
451
  throw error;
@@ -310,14 +454,41 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
310
454
 
311
455
  protected async clearSourceTables(_signal?: AbortSignal): Promise<void> {
312
456
  await this.db
313
- .sourceTablesV3(this.group_id)
457
+ .sourceTables(this.replicationStreamId)
314
458
  .drop({ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS })
315
459
  .catch((error) => {
460
+ if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
461
+ return;
462
+ }
463
+ throw error;
464
+ });
465
+ }
466
+
467
+ protected override async clearCustomCheckpointRequests(signal?: AbortSignal): Promise<void> {
468
+ for (const collection of await this.db.listCustomCheckpointRequestCollections(this.replicationStreamId)) {
469
+ signal?.throwIfAborted();
470
+ await collection.drop({ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }).catch((error) => {
316
471
  if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
317
472
  return;
318
473
  }
319
474
  throw error;
320
475
  });
476
+ }
477
+ }
478
+
479
+ async cleanupStoppedSyncConfigs(
480
+ options: storage.CleanupStoppedSyncConfigsOptions
481
+ ): Promise<storage.CleanupStoppedSyncConfigsResult> {
482
+ return new MongoStoppedSyncConfigCleanup({
483
+ db: this.db,
484
+ replicationStreamId: this.replicationStreamId,
485
+ signal: options.signal,
486
+ logger: options.logger ?? this.logger,
487
+ defaultSchema: options.defaultSchema,
488
+ sourceConnectionTag: options.sourceConnectionTag,
489
+ objectStorage: this.objectStorage,
490
+ clearBatchThrottleRate: this.clearBatchThrottleRate
491
+ }).run();
321
492
  }
322
493
 
323
494
  protected getDataBucketChangesImpl(
@@ -327,14 +498,14 @@ export class MongoSyncBucketStorageV3 extends MongoSyncBucketStorage {
327
498
  }
328
499
 
329
500
  protected getParameterBucketChangesImpl(
330
- options: GetCheckpointChangesOptions
501
+ options: MongoGetCheckpointChangesOptions
331
502
  ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
332
503
  return getParameterBucketChangesV3(this.versionContext, options);
333
504
  }
334
505
  }
335
506
 
336
507
  export async function getParameterSetsV3(
337
- ctx: MongoSyncBucketStorageContext<VersionedPowerSyncMongoV3>,
508
+ ctx: MongoSyncBucketStorageContextV3,
338
509
  checkpoint: MongoSyncBucketStorageCheckpoint,
339
510
  lookups: ScopedParameterLookup[],
340
511
  limit: number
@@ -350,8 +521,8 @@ export async function getParameterSetsV3(
350
521
  pipeline: mongo.Document[];
351
522
  } => {
352
523
  const indexId = lookup.indexId;
353
- const collection = ctx.db.parameterIndexV3(ctx.group_id, indexId);
354
- const lookupFilter = serializeParameterLookupV3(lookup);
524
+ const collection = ctx.db.parameterIndex(ctx.replicationStreamId, indexId);
525
+ const lookupFilter = serializeParameterLookup(lookup);
355
526
 
356
527
  return {
357
528
  collection,
@@ -434,24 +605,38 @@ export async function getParameterSetsV3(
434
605
  }
435
606
 
436
607
  export async function* getBucketDataBatchV3(
437
- ctx: MongoSyncBucketStorageContext<VersionedPowerSyncMongoV3>,
438
- checkpoint: utils.InternalOpId,
608
+ ctx: MongoSyncBucketStorageContextV3,
609
+ checkpoint: MongoSyncBucketStorageCheckpoint,
439
610
  dataBuckets: storage.BucketDataRequest[],
440
611
  options?: storage.BucketDataBatchOptions
441
- ): AsyncIterable<storage.SyncBucketDataChunk> {
612
+ ): AsyncIterable<storage.SyncBucketDataChunk | storage.SyncBucketDataBatchEnd> {
442
613
  if (dataBuckets.length == 0) {
443
614
  return;
444
615
  }
445
616
 
446
- if (checkpoint == null) {
617
+ if (checkpoint.checkpoint == null) {
447
618
  throw new Error('checkpoint is null');
448
619
  }
449
620
 
621
+ const readPreference = options?.requestHint == 'bulk' ? ctx.readPreference : undefined;
622
+ const readConcern = ctx.readPreference == null ? undefined : 'majority';
623
+ const session =
624
+ readPreference == null || checkpoint.snapshotTime == null
625
+ ? undefined
626
+ : ctx.db.client.startSession({ causalConsistency: true });
627
+ await using _ = { [Symbol.asyncDispose]: async () => session?.endSession() };
628
+
629
+ if (session != null) {
630
+ session.advanceOperationTime(checkpoint.snapshotTime);
631
+ session.advanceClusterTime(checkpoint.clusterTime);
632
+ }
633
+
450
634
  const batchLimit = options?.limit ?? storage.DEFAULT_DOCUMENT_BATCH_LIMIT;
451
635
  const chunkSizeLimitBytes = options?.chunkLimitBytes ?? storage.DEFAULT_DOCUMENT_CHUNK_LIMIT_BYTES;
452
- const end = checkpoint;
453
- let remainingLimit = batchLimit;
636
+ const end = checkpoint.checkpoint;
454
637
 
638
+ // Group requests by definition, so that we can query each definition's bucket data in a single query.
639
+ // We only return the results of a single query per batch.
455
640
  const requestsByDefinition = new Map<string, storage.BucketDataRequest[]>();
456
641
  for (const request of dataBuckets) {
457
642
  const definitionId = ctx.mapping.bucketSourceId(request.source);
@@ -461,61 +646,79 @@ export async function* getBucketDataBatchV3(
461
646
  }
462
647
 
463
648
  const definitionGroups = Array.from(requestsByDefinition.entries());
464
- for (let groupIndex = 0; groupIndex < definitionGroups.length && remainingLimit > 0; groupIndex++) {
465
- const [definitionId, requests] = definitionGroups[groupIndex];
649
+ for (const [groupIndex, [definitionId, requests]] of definitionGroups.entries()) {
466
650
  const hasLaterDefinitionGroups = groupIndex < definitionGroups.length - 1;
467
651
  const bucketMap = new Map(requests.map((request) => [request.bucket, request.start]));
468
- const filters: mongo.Filter<BucketDataDocumentV3>[] = Array.from(bucketMap.entries()).map(([bucket, start]) => ({
652
+ const filters = Array.from(bucketMap.entries()).map(([bucket, start]) => ({
469
653
  _id: {
470
- $gt: {
471
- b: bucket,
472
- o: start
473
- },
474
- $lte: {
475
- b: bucket,
476
- o: end as any
477
- }
478
- }
479
- }));
480
-
481
- const cursor = ctx.db.bucketDataV3(ctx.group_id, definitionId).find(
482
- {
483
- $or: filters
654
+ $gt: { b: bucket, o: start },
655
+ $lte: { b: bucket, o: new bson.MaxKey() }
484
656
  },
485
- {
486
- session: undefined,
487
- sort: { _id: 1 },
488
- limit: remainingLimit,
489
- batchSize: remainingLimit + 1,
490
- raw: true,
491
- maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
492
- }
493
- ) as unknown as mongo.FindCursor<Buffer>;
494
-
495
- let { data, hasMore: batchHasMore } = await readSingleBatch(cursor).catch((e) => {
657
+ min_op: { $lte: end }
658
+ // MongoDB Filter<T> doesn't accept compound _id ranges or dotted field paths in its type.
659
+ })) as unknown as mongo.Filter<BucketDataDocumentV3>[];
660
+
661
+ const collection = ctx.db.bucketData(ctx.replicationStreamId, definitionId);
662
+ // MongoDB Filter<T> doesn't accept the $or operator in its type.
663
+ const filter = { $or: filters } as unknown as mongo.Filter<BucketDataDocumentV3>;
664
+ const context = { replicationStreamId: ctx.replicationStreamId, definitionId };
665
+
666
+ const cursorOptions = { limit: batchLimit, batchSize: batchLimit + 1 };
667
+
668
+ // raw: true returns Buffers, but the driver typing doesn't reflect that
669
+ // without an explicit cast to FindCursor<Buffer>.
670
+ const cursor = collection.find(filter, {
671
+ session,
672
+ readPreference,
673
+ readConcern,
674
+ sort: { _id: 1 },
675
+ raw: true,
676
+ maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS,
677
+ ...cursorOptions
678
+ }) as unknown as mongo.FindCursor<Buffer>;
679
+
680
+ let { data: rawData, hasMore } = await readSingleBatch(cursor).catch((e: unknown) => {
496
681
  throw lib_mongo.mapQueryError(e, 'while reading bucket data');
497
682
  });
498
- if (data.length == remainingLimit) {
499
- batchHasMore = true;
683
+
684
+ if (rawData.length >= cursorOptions.limit) {
685
+ hasMore = true;
500
686
  }
501
- if (data.length == 0) {
502
- continue;
687
+
688
+ // Deserialize the raw documents and cut the batch to a bounded size. Any data not
689
+ // making the cut will be read in the next round, using a fresh query.
690
+ const cutBatch = cutBucketDataBatch(rawData);
691
+ const docs = cutBatch.documents;
692
+ if (cutBatch.wasCut) {
693
+ hasMore = true;
503
694
  }
504
695
 
505
- remainingLimit -= data.length;
696
+ // Hydrate any operations from object storage.
697
+ // In the future we can do this in a more pipelined fashion, but for now we hydrate
698
+ // the entire batch at once.
699
+ await hydrateBucketDataDocuments(docs, ctx.objectStorage, { signal: options?.signal, tracer: options?.tracer });
506
700
 
507
- let chunkSizeBytes = 0;
701
+ let currentChunkSizeBytes = 0;
508
702
  let currentChunk: utils.SyncBucketData | null = null;
509
703
  let targetOp: InternalOpId | null = null;
704
+ let seenBuckets = new Set<string>();
705
+ const batchHasMore = hasMore;
706
+
707
+ for (const doc of docs) {
708
+ const bucket = doc._id.b;
709
+ seenBuckets.add(bucket);
710
+ const bucketStart = bucketMap.get(bucket);
711
+ if (bucketStart == null) {
712
+ throw new ServiceAssertionError(`data for unexpected bucket: ${bucket}`);
713
+ }
510
714
 
511
- for (let rawData of data) {
512
- const row = loadBucketDataDocumentV3(
513
- { replicationStreamId: ctx.group_id, definitionId },
514
- bson.deserialize(rawData, storage.BSON_DESERIALIZE_INTERNAL_OPTIONS) as BucketDataDocumentV3
515
- );
516
- const bucket = row.bucketKey.bucket;
517
-
518
- if (currentChunk == null || currentChunk.bucket != bucket || chunkSizeBytes >= chunkSizeLimitBytes) {
715
+ // Reached a new bucket or size limit: yield the current chunk and start a new one.
716
+ if (
717
+ currentChunk == null ||
718
+ currentChunk.bucket != bucket ||
719
+ currentChunkSizeBytes >= chunkSizeLimitBytes ||
720
+ currentChunk.data.length >= batchLimit
721
+ ) {
519
722
  let start: ProtocolOpId | undefined = undefined;
520
723
  if (currentChunk != null) {
521
724
  if (currentChunk.bucket == bucket) {
@@ -525,18 +728,15 @@ export async function* getBucketDataBatchV3(
525
728
 
526
729
  const yieldChunk = currentChunk;
527
730
  currentChunk = null;
528
- chunkSizeBytes = 0;
529
- yield { chunkData: yieldChunk, targetOp: targetOp };
731
+ currentChunkSizeBytes = 0;
732
+ yield { chunkData: yieldChunk, targetOp };
530
733
  targetOp = null;
531
734
  }
532
735
 
533
736
  if (start == null) {
534
- const startOpId = bucketMap.get(bucket);
535
- if (startOpId == null) {
536
- throw new Error(`data for unexpected bucket: ${bucket}`);
537
- }
538
- start = internalToExternalOpId(startOpId);
737
+ start = internalToExternalOpId(bucketStart);
539
738
  }
739
+
540
740
  currentChunk = {
541
741
  bucket,
542
742
  after: start,
@@ -546,35 +746,58 @@ export async function* getBucketDataBatchV3(
546
746
  };
547
747
  }
548
748
 
549
- const entry = mapOpEntry(row);
550
- if (row.target_op != null && (targetOp == null || row.target_op > targetOp)) {
551
- targetOp = row.target_op;
552
- }
553
-
554
- currentChunk.data.push(entry);
555
- currentChunk.next_after = entry.op_id;
556
- chunkSizeBytes += rawData.byteLength;
749
+ const rows = extractRowsFromDocument(doc, context, bucketStart, end);
750
+ currentChunk.data.push(...rows.map(mapOpEntry));
751
+ currentChunk.next_after = currentChunk.data.at(-1)?.op_id ?? internalToExternalOpId(end);
752
+ targetOp = maxOpId(targetOp, doc.target_op);
753
+ currentChunkSizeBytes += doc.size;
557
754
  }
558
755
 
559
756
  if (currentChunk != null) {
560
757
  const yieldChunk = currentChunk;
561
- yieldChunk.has_more = batchHasMore || (remainingLimit <= 0 && hasLaterDefinitionGroups);
562
- yield { chunkData: yieldChunk, targetOp: targetOp };
758
+ // The last chunk may contain more data that was cut in this batch.
759
+ yieldChunk.has_more = batchHasMore;
760
+ yield { chunkData: yieldChunk, targetOp };
563
761
  }
564
762
 
565
- if (batchHasMore || remainingLimit <= 0) {
566
- return;
763
+ if (!batchHasMore) {
764
+ for (const bucket of bucketMap.keys()) {
765
+ if (!seenBuckets.has(bucket)) {
766
+ // We processed everything for this definition group, but this bucket had no data in the batch.
767
+ // Yield an empty chunk to indicate that it is complete.
768
+ // This prevents re-querying the same bucket in the next batch.
769
+ yield {
770
+ chunkData: {
771
+ bucket,
772
+ after: internalToExternalOpId(bucketMap.get(bucket)!),
773
+ has_more: false,
774
+ data: [],
775
+ next_after: internalToExternalOpId(end)
776
+ },
777
+ targetOp
778
+ };
779
+ }
780
+ }
781
+ }
782
+
783
+ if (currentChunk != null) {
784
+ // We yielded data in this group (aside from empty buckets).
785
+ // Return to the caller to allow them to process it before continuing to the next group.
786
+ yield { hasMore: batchHasMore || hasLaterDefinitionGroups };
787
+ break;
788
+ } else {
789
+ // No data in this definition group - continue in the next group.
567
790
  }
568
791
  }
569
792
  }
570
793
 
571
794
  export async function getDataBucketChangesV3(
572
- ctx: MongoSyncBucketStorageContext<VersionedPowerSyncMongoV3>,
795
+ ctx: MongoSyncBucketStorageContextV3,
573
796
  options: GetCheckpointChangesOptions
574
797
  ): Promise<Pick<CheckpointChanges, 'updatedDataBuckets' | 'invalidateDataBuckets'>> {
575
798
  const limit = 1000;
576
799
  const bucketStateUpdates = await ctx.db
577
- .bucketStateV3(ctx.group_id)
800
+ .bucketState(ctx.replicationStreamId)
578
801
  .aggregate<{ _id: string; last_op: bigint }>(
579
802
  [
580
803
  {
@@ -610,15 +833,23 @@ export async function getDataBucketChangesV3(
610
833
  };
611
834
  }
612
835
 
836
+ /**
837
+ * Query the parameter entries changed between the two checkpoints, to determine which parameter
838
+ * lookups need to be re-evaluated.
839
+ *
840
+ * This runs at the next checkpoint's snapshot, so it still sees entries that parameter compaction
841
+ * deleted after that snapshot. Compaction that deleted entries before the snapshot is covered by
842
+ * the invalidation fence, checked before we get here.
843
+ */
613
844
  export async function getParameterBucketChangesV3(
614
- ctx: MongoSyncBucketStorageContext<VersionedPowerSyncMongoV3>,
615
- options: GetCheckpointChangesOptions
845
+ ctx: MongoSyncBucketStorageContextV3,
846
+ options: MongoGetCheckpointChangesOptions
616
847
  ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
617
848
  const limit = 1000;
618
849
  const indexIds = ctx.mapping.allParameterIndexIds();
619
850
  const collections = indexIds.map((indexId) => ({
620
851
  indexId,
621
- collection: ctx.db.parameterIndexV3(ctx.group_id, indexId)
852
+ collection: ctx.db.parameterIndex(ctx.replicationStreamId, indexId)
622
853
  }));
623
854
  if (collections.length == 0) {
624
855
  return {
@@ -642,28 +873,41 @@ export async function getParameterBucketChangesV3(
642
873
  }
643
874
  ];
644
875
  const [firstCollection, ...remainingCollections] = collections;
645
- const parameterUpdates = await firstCollection.collection
646
- .aggregate<{ lookup: bson.Binary; indexId: string }>(
647
- [
648
- ...pipelineForCollection(firstCollection.indexId),
649
- ...remainingCollections.map((collection) => {
650
- return {
651
- $unionWith: {
652
- coll: collection.collection.collectionName,
653
- pipeline: pipelineForCollection(collection.indexId)
654
- }
655
- };
656
- }),
876
+ const parameterUpdates = await ctx.db.client.withSession({ snapshot: true }, async (session) => {
877
+ setSessionSnapshotTime(session, options.nextCheckpoint.snapshotTime);
878
+ return await firstCollection.collection
879
+ .aggregate<{ lookup: bson.Binary; indexId: string }>(
880
+ [
881
+ ...pipelineForCollection(firstCollection.indexId),
882
+ ...remainingCollections.map((collection) => {
883
+ return {
884
+ $unionWith: {
885
+ coll: collection.collection.collectionName,
886
+ pipeline: pipelineForCollection(collection.indexId)
887
+ }
888
+ };
889
+ }),
890
+ {
891
+ $limit: limit + 1
892
+ }
893
+ ],
657
894
  {
658
- $limit: limit + 1
895
+ session,
896
+ readConcern: 'snapshot',
897
+ batchSize: limit + 2,
898
+ maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
659
899
  }
660
- ],
661
- {
662
- batchSize: limit + 2,
663
- maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
664
- }
665
- )
666
- .toArray();
900
+ )
901
+ .toArray()
902
+ .catch((e) => {
903
+ // Includes the case where the checkpoint snapshot has expired. Degrading to
904
+ // invalidateParameterBuckets would be safe in itself - it reads nothing - but it gains
905
+ // nothing: the caller responds to that by re-evaluating the parameter queries at this
906
+ // same snapshot, which fails too. The checkpoint has to be refetched instead, which is
907
+ // what the existing sync retry behavior does.
908
+ throw lib_mongo.mapQueryError(e, 'while querying parameter changes');
909
+ });
910
+ });
667
911
 
668
912
  const invalidateParameterUpdates = parameterUpdates.length > limit;
669
913
 
@@ -671,8 +915,6 @@ export async function getParameterBucketChangesV3(
671
915
  invalidateParameterBuckets: invalidateParameterUpdates,
672
916
  updatedParameterLookups: invalidateParameterUpdates
673
917
  ? new Set<string>()
674
- : new Set<string>(
675
- parameterUpdates.map((p) => JSONBig.stringify(deserializeParameterLookupV3(p.lookup, p.indexId)))
676
- )
918
+ : new Set<string>(parameterUpdates.map((p) => JSONBig.stringify(deserializeParameterLookup(p.lookup, p.indexId))))
677
919
  };
678
920
  }