@powersync/service-module-mongodb-storage 0.0.0-dev-20260602073133 → 0.0.0-dev-20260803144812

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 (239) hide show
  1. package/CHANGELOG.md +101 -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 +41 -16
  6. package/dist/storage/MongoBucketStorage.js +226 -71
  7. package/dist/storage/MongoBucketStorage.js.map +1 -1
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +56 -25
  9. package/dist/storage/implementation/MongoBucketBatch.js +110 -62
  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 +17 -19
  15. package/dist/storage/implementation/MongoCompactor.js +80 -263
  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 +7 -6
  21. package/dist/storage/implementation/MongoParameterCompactor.js +25 -2
  22. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  23. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
  24. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -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 +123 -0
  31. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  32. package/dist/storage/implementation/MongoStorageProvider.js +39 -4
  33. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +57 -29
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js +87 -61
  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 +3 -3
  39. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  40. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
  41. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +171 -24
  42. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  43. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +8 -0
  44. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  45. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  46. package/dist/storage/implementation/common/PersistedBatch.d.ts +7 -2
  47. package/dist/storage/implementation/common/PersistedBatch.js +12 -0
  48. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  49. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
  50. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  51. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  52. package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
  53. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  54. package/dist/storage/implementation/db.js +17 -15
  55. package/dist/storage/implementation/db.js.map +1 -1
  56. package/dist/storage/implementation/models.d.ts +20 -13
  57. package/dist/storage/implementation/models.js.map +1 -1
  58. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +9 -1
  59. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +61 -29
  60. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  61. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +4 -4
  62. package/dist/storage/implementation/v1/MongoChecksumsV1.js +119 -7
  63. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  64. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
  65. package/dist/storage/implementation/v1/MongoCompactorV1.js +251 -2
  66. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  67. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -19
  68. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +82 -58
  69. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  70. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
  71. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
  72. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
  73. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  74. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  75. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  76. package/dist/storage/implementation/v1/models.d.ts +10 -1
  77. package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
  78. package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
  79. package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
  80. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -4
  81. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +309 -259
  82. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  83. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +30 -9
  84. package/dist/storage/implementation/v3/MongoChecksumsV3.js +161 -20
  85. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  86. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +40 -2
  87. package/dist/storage/implementation/v3/MongoCompactorV3.js +647 -23
  88. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  89. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +67 -0
  90. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +351 -0
  91. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  92. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +45 -30
  93. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +272 -140
  94. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  95. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +6 -3
  96. package/dist/storage/implementation/v3/PersistedBatchV3.js +84 -21
  97. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  98. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  99. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  100. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  101. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +15 -13
  102. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +32 -27
  103. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  104. package/dist/storage/implementation/v3/bucket-format.d.ts +13 -0
  105. package/dist/storage/implementation/v3/bucket-format.js +77 -0
  106. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  107. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  108. package/dist/storage/implementation/v3/chunking.js +30 -0
  109. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  110. package/dist/storage/implementation/v3/models.d.ts +101 -21
  111. package/dist/storage/implementation/v3/models.js +11 -28
  112. package/dist/storage/implementation/v3/models.js.map +1 -1
  113. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
  114. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
  115. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
  116. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
  117. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
  118. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
  119. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
  120. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
  121. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
  122. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
  123. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
  124. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
  125. package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
  126. package/dist/storage/implementation/v3/source-table-utils.js +214 -0
  127. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  128. package/dist/storage/storage-index.d.ts +4 -4
  129. package/dist/storage/storage-index.js +4 -4
  130. package/dist/storage/storage-index.js.map +1 -1
  131. package/dist/types/types.d.ts +49 -3
  132. package/dist/types/types.js +48 -1
  133. package/dist/types/types.js.map +1 -1
  134. package/dist/utils/test-utils.d.ts +1 -2
  135. package/dist/utils/test-utils.js +7 -1
  136. package/dist/utils/test-utils.js.map +1 -1
  137. package/dist/utils/util.d.ts +9 -2
  138. package/dist/utils/util.js +128 -4
  139. package/dist/utils/util.js.map +1 -1
  140. package/package.json +12 -19
  141. package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
  142. package/src/storage/MongoBucketStorage.ts +347 -88
  143. package/src/storage/implementation/MongoBucketBatch.ts +147 -82
  144. package/src/storage/implementation/MongoChecksums.ts +93 -153
  145. package/src/storage/implementation/MongoCompactor.ts +102 -316
  146. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  147. package/src/storage/implementation/MongoParameterCompactor.ts +25 -6
  148. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
  149. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  150. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +154 -0
  151. package/src/storage/implementation/MongoStorageProvider.ts +46 -4
  152. package/src/storage/implementation/MongoSyncBucketStorage.ts +121 -87
  153. package/src/storage/implementation/MongoSyncRulesLock.ts +7 -4
  154. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +202 -28
  155. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +9 -0
  156. package/src/storage/implementation/common/PersistedBatch.ts +19 -3
  157. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  158. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  159. package/src/storage/implementation/db.ts +19 -20
  160. package/src/storage/implementation/models.ts +20 -14
  161. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +75 -37
  162. package/src/storage/implementation/v1/MongoChecksumsV1.ts +154 -12
  163. package/src/storage/implementation/v1/MongoCompactorV1.ts +294 -9
  164. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +142 -105
  165. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
  166. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  167. package/src/storage/implementation/v1/models.ts +11 -1
  168. package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
  169. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +382 -321
  170. package/src/storage/implementation/v3/MongoChecksumsV3.ts +250 -32
  171. package/src/storage/implementation/v3/MongoCompactorV3.ts +847 -32
  172. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +482 -0
  173. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +359 -187
  174. package/src/storage/implementation/v3/PersistedBatchV3.ts +118 -37
  175. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  176. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +46 -36
  177. package/src/storage/implementation/v3/bucket-format.ts +97 -0
  178. package/src/storage/implementation/v3/chunking.ts +40 -0
  179. package/src/storage/implementation/v3/models.ts +132 -55
  180. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
  181. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
  182. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
  183. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
  184. package/src/storage/implementation/v3/source-table-utils.ts +392 -0
  185. package/src/storage/storage-index.ts +12 -4
  186. package/src/types/types.ts +58 -1
  187. package/src/utils/test-utils.ts +8 -3
  188. package/src/utils/util.ts +185 -6
  189. package/test/src/__snapshots__/storage.test.ts.snap +0 -75
  190. package/test/src/__snapshots__/storage_sync.test.ts.snap +35 -617
  191. package/test/src/cleanup-stopped-sync-configs.test.ts +671 -0
  192. package/test/src/helpers/MemoryObjectStorage.ts +53 -0
  193. package/test/src/helpers/s3TestFactory.ts +69 -0
  194. package/test/src/object_storage_lifecycle.test.ts +59 -0
  195. package/test/src/setup.ts +6 -1
  196. package/test/src/storage.test.ts +227 -13
  197. package/test/src/storage_compacting.test.ts +1951 -12
  198. package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
  199. package/test/src/storage_s3_checksums.test.ts +191 -0
  200. package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
  201. package/test/src/storage_s3_reading.test.ts +526 -0
  202. package/test/src/storage_s3_writing.test.ts +135 -0
  203. package/test/src/storage_sync.test.ts +1180 -73
  204. package/test/src/storeCurrentData.test.ts +227 -0
  205. package/test/src/util.ts +2 -1
  206. package/tsconfig.json +1 -4
  207. package/tsconfig.tsbuildinfo +1 -1
  208. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -15
  209. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  210. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  211. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  212. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -67
  213. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  214. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -29
  215. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -76
  216. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  217. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  218. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  219. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  220. package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
  221. package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
  222. package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
  223. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
  224. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
  225. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
  226. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -4
  227. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  228. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  229. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
  230. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -48
  231. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
  232. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -75
  233. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -82
  234. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -101
  235. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  236. package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
  237. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
  238. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
  239. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -68
@@ -26,30 +26,196 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
26
26
  this._mode = mode;
27
27
  }
28
28
 
29
- async createManagedWriteCheckpoint(checkpoint: storage.ManagedWriteCheckpointOptions): Promise<bigint> {
29
+ async createManagedWriteCheckpoints(
30
+ checkpoints: storage.ManagedWriteCheckpointOptions[]
31
+ ): Promise<storage.CreateManagedWriteCheckpointsResult> {
30
32
  if (this.writeCheckpointMode !== storage.WriteCheckpointMode.MANAGED) {
31
33
  throw new framework.ServiceAssertionError(
32
34
  `Attempting to create a managed Write Checkpoint when the current Write Checkpoint mode is set to "${this.writeCheckpointMode}"`
33
35
  );
34
36
  }
35
37
 
36
- const { user_id, heads: lsns } = checkpoint;
37
- const doc = await this.db.write_checkpoints.findOneAndUpdate(
38
- {
39
- user_id: user_id
40
- },
41
- {
42
- $set: {
43
- lsns,
44
- processed_at_lsn: null
38
+ const uniqueCheckpoints = storage.uniqueManagedWriteCheckpoints(checkpoints);
39
+ if (uniqueCheckpoints.length == 0) {
40
+ return { writeCheckpoints: new Map(), shouldAdvance: false };
41
+ }
42
+
43
+ let shouldAdvance = false;
44
+ const writeCheckpoints = new Map<string, bigint>();
45
+ const generatedCheckpoints = uniqueCheckpoints.filter((checkpoint) => checkpoint.checkpoint_request_id == null);
46
+ const suppliedCheckpoints = uniqueCheckpoints.filter((checkpoint) => checkpoint.checkpoint_request_id != null);
47
+
48
+ if (generatedCheckpoints.length == 1) {
49
+ // For the common case of a single checkpoint, we can do this in a single request.
50
+ const { user_id, heads: lsns } = generatedCheckpoints[0];
51
+ const doc = await this.db.write_checkpoints.findOneAndUpdate(
52
+ {
53
+ user_id
45
54
  },
46
- $inc: {
47
- client_id: 1n
48
- }
49
- },
50
- { upsert: true, returnDocument: 'after' }
55
+ {
56
+ $set: {
57
+ lsns,
58
+ processed_at_lsn: null
59
+ },
60
+ // Unset rather than set to null, to keep the document out of the
61
+ // partial checkpoint_requested_at index used for retention cleanup.
62
+ $unset: {
63
+ checkpoint_requested_at: 1
64
+ },
65
+ $inc: {
66
+ client_id: 1n
67
+ }
68
+ },
69
+ { upsert: true, returnDocument: 'after' }
70
+ );
71
+
72
+ // Generated checkpoints always reset processed_at_lsn, so they are pending
73
+ // and the source marker must be forced.
74
+ shouldAdvance = true;
75
+ writeCheckpoints.set(doc!.user_id, doc!.client_id);
76
+ } else if (generatedCheckpoints.length > 1) {
77
+ // For more than one generated checkpoint, this gives a constant 2 requests.
78
+ await this.db.write_checkpoints.bulkWrite(
79
+ generatedCheckpoints.map(({ user_id, heads: lsns }) => ({
80
+ updateOne: {
81
+ filter: { user_id },
82
+ update: {
83
+ $set: {
84
+ lsns,
85
+ processed_at_lsn: null
86
+ },
87
+ $unset: {
88
+ checkpoint_requested_at: 1
89
+ },
90
+ $inc: {
91
+ client_id: 1n
92
+ }
93
+ },
94
+ upsert: true
95
+ }
96
+ }))
97
+ );
98
+
99
+ const userIds = generatedCheckpoints.map((checkpoint) => checkpoint.user_id);
100
+ const docs = await this.db.write_checkpoints
101
+ .find(
102
+ {
103
+ user_id: { $in: userIds }
104
+ },
105
+ {
106
+ projection: {
107
+ user_id: 1,
108
+ client_id: 1
109
+ }
110
+ }
111
+ )
112
+ .toArray();
113
+
114
+ shouldAdvance = true;
115
+ for (const doc of docs) {
116
+ writeCheckpoints.set(doc.user_id, doc.client_id);
117
+ }
118
+ }
119
+
120
+ if (suppliedCheckpoints.length > 0) {
121
+ const suppliedResult = await this.createSuppliedManagedWriteCheckpoints(suppliedCheckpoints);
122
+ shouldAdvance ||= suppliedResult.shouldAdvance;
123
+ for (const [userId, writeCheckpoint] of suppliedResult.writeCheckpoints) {
124
+ writeCheckpoints.set(userId, writeCheckpoint);
125
+ }
126
+ }
127
+
128
+ return { writeCheckpoints, shouldAdvance };
129
+ }
130
+
131
+ private async createSuppliedManagedWriteCheckpoints(checkpoints: storage.ManagedWriteCheckpointOptions[]) {
132
+ const checkpointRequestedAt = new Date();
133
+
134
+ await this.db.write_checkpoints.bulkWrite(
135
+ checkpoints.map((checkpoint) => {
136
+ const { user_id, heads: lsns } = checkpoint;
137
+ const checkpointRequestId = checkpoint.checkpoint_request_id!;
138
+
139
+ // Supplied request ids are monotonic. updateMany preserves the existing
140
+ // upsert behavior for first checkpoint requests, while also advancing
141
+ // every duplicate row for the user_id that is lower than this request id.
142
+ const shouldApplyRequestId = {
143
+ $or: [
144
+ { $eq: [{ $ifNull: ['$client_id', null] }, null] },
145
+ { $lt: ['$client_id', { $literal: checkpointRequestId }] }
146
+ ]
147
+ };
148
+ // Equal retries keep their original source head but refresh retention.
149
+ const shouldRefreshRequest = {
150
+ $or: [
151
+ { $eq: [{ $ifNull: ['$client_id', null] }, null] },
152
+ { $lte: ['$client_id', { $literal: checkpointRequestId }] }
153
+ ]
154
+ };
155
+
156
+ return {
157
+ updateMany: {
158
+ filter: { user_id },
159
+ update: [
160
+ {
161
+ $set: {
162
+ user_id,
163
+ client_id: {
164
+ $cond: [shouldApplyRequestId, { $literal: checkpointRequestId }, '$client_id']
165
+ },
166
+ lsns: {
167
+ $cond: [shouldApplyRequestId, { $literal: lsns }, '$lsns']
168
+ },
169
+ processed_at_lsn: {
170
+ $cond: [shouldApplyRequestId, null, '$processed_at_lsn']
171
+ },
172
+ checkpoint_requested_at: {
173
+ $cond: [shouldRefreshRequest, { $literal: checkpointRequestedAt }, '$checkpoint_requested_at']
174
+ }
175
+ }
176
+ }
177
+ ],
178
+ upsert: true
179
+ }
180
+ };
181
+ })
51
182
  );
52
- return doc!.client_id;
183
+
184
+ // Fetch the final ids separately so stale requests can still return the
185
+ // checkpoint currently stored. If duplicate rows already exist for a
186
+ // user_id, use the highest stored id instead of whichever document MongoDB
187
+ // happens to return first.
188
+ const userIds = checkpoints.map((checkpoint) => checkpoint.user_id);
189
+ const docs = await this.db.write_checkpoints
190
+ .find(
191
+ {
192
+ user_id: { $in: userIds }
193
+ },
194
+ {
195
+ projection: {
196
+ user_id: 1,
197
+ client_id: 1,
198
+ processed_at_lsn: 1
199
+ }
200
+ }
201
+ )
202
+ .toArray();
203
+ const maxDocsByUser = new Map<string, (typeof docs)[number]>();
204
+ for (const doc of docs) {
205
+ const existing = maxDocsByUser.get(doc.user_id);
206
+ if (
207
+ existing == null ||
208
+ doc.client_id > existing.client_id ||
209
+ (doc.client_id == existing.client_id && existing.processed_at_lsn != null && doc.processed_at_lsn == null)
210
+ ) {
211
+ maxDocsByUser.set(doc.user_id, doc);
212
+ }
213
+ }
214
+
215
+ return {
216
+ writeCheckpoints: new Map([...maxDocsByUser].map(([userId, doc]) => [userId, doc.client_id])),
217
+ shouldAdvance: [...maxDocsByUser.values()].some((doc) => doc.processed_at_lsn == null)
218
+ };
53
219
  }
54
220
 
55
221
  async lastWriteCheckpoint(filters: storage.LastWriteCheckpointFilters): Promise<bigint | null> {
@@ -178,19 +344,27 @@ export async function batchCreateCustomWriteCheckpoints(
178
344
  }
179
345
 
180
346
  await db.custom_write_checkpoints.bulkWrite(
181
- checkpoints.map((checkpointOptions) => ({
182
- updateOne: {
183
- filter: { user_id: checkpointOptions.user_id, sync_rules_id: checkpointOptions.sync_rules_id },
184
- update: {
185
- $set: {
186
- checkpoint: checkpointOptions.checkpoint,
187
- sync_rules_id: checkpointOptions.sync_rules_id,
188
- op_id: opId
189
- }
190
- },
191
- upsert: true
347
+ checkpoints.map((checkpointOptions) => {
348
+ const set: Record<string, unknown> = {
349
+ checkpoint: checkpointOptions.checkpoint,
350
+ sync_rules_id: checkpointOptions.sync_rules_id,
351
+ op_id: opId
352
+ };
353
+ if (checkpointOptions.checkpoint_requested_at != null) {
354
+ set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
192
355
  }
193
- })),
356
+
357
+ return {
358
+ updateOne: {
359
+ filter: { user_id: checkpointOptions.user_id, sync_rules_id: checkpointOptions.sync_rules_id },
360
+ update: {
361
+ $set: set,
362
+ ...(checkpointOptions.checkpoint_requested_at == null ? { $unset: { checkpoint_requested_at: 1 } } : {})
363
+ },
364
+ upsert: true
365
+ }
366
+ };
367
+ }),
194
368
  { session }
195
369
  );
196
370
  }
@@ -0,0 +1,9 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { InternalOpId } from '@powersync/service-core';
3
+ import * as bson from 'bson';
4
+
5
+ export interface MongoSyncBucketStorageCheckpoint {
6
+ checkpoint: InternalOpId;
7
+ snapshotTime: bson.Timestamp;
8
+ clusterTime: mongo.ClusterTime;
9
+ }
@@ -3,14 +3,14 @@ import { BucketDataSource, BucketDefinitionId, EvaluatedParameters, EvaluatedRow
3
3
  import * as bson from 'bson';
4
4
 
5
5
  import { logger as defaultLogger, Logger } from '@powersync/lib-services-framework';
6
- import { InternalOpId, storage, utils } from '@powersync/service-core';
6
+ import { BucketDefinitionMapping, InternalOpId, storage, utils } from '@powersync/service-core';
7
7
  import { JSONBig } from '@powersync/service-jsonbig';
8
8
  import { mongoTableId, replicaIdToSubkey } from '../../../utils/util.js';
9
- import { BucketDefinitionMapping } from '../BucketDefinitionMapping.js';
10
9
  import { currentBucketKey, MAX_ROW_SIZE } from '../MongoBucketBatchShared.js';
11
10
  import { MongoIdSequence } from '../MongoIdSequence.js';
12
11
  import type { VersionedPowerSyncMongo } from '../db.js';
13
12
  import { TaggedBucketParameterDocument } from '../models.js';
13
+ import { ObjectStorage } from '../v3/object-storage/ObjectStorage.js';
14
14
  import { BucketDataDoc, BucketKey } from './BucketDataDoc.js';
15
15
  import { SourceRecordBucketState, SourceRecordLookupState } from './SourceRecordStore.js';
16
16
 
@@ -35,6 +35,8 @@ const MAX_TRANSACTION_BATCH_SIZE = 30_000_000;
35
35
  */
36
36
  const MAX_TRANSACTION_DOC_COUNT = 2_000;
37
37
 
38
+ export const DEFAULT_INLINE_THRESHOLD_BYTES = 16 * 1024;
39
+
38
40
  export interface SaveBucketDataOptions {
39
41
  op_seq: MongoIdSequence;
40
42
  sourceKey: storage.ReplicaId;
@@ -61,6 +63,8 @@ export interface UpsertCurrentDataOptions {
61
63
 
62
64
  export interface PersistedBatchOptions {
63
65
  logger?: Logger;
66
+ objectStorage?: ObjectStorage;
67
+ inlineThresholdBytes?: number;
64
68
  }
65
69
 
66
70
  /**
@@ -75,6 +79,9 @@ export abstract class PersistedBatch {
75
79
  bucketParameters: TaggedBucketParameterDocument[] = [];
76
80
  bucketStates: Map<string, BucketStateUpdate> = new Map();
77
81
 
82
+ protected readonly objectStorage?: ObjectStorage;
83
+ protected readonly inlineThresholdBytes: number = DEFAULT_INLINE_THRESHOLD_BYTES;
84
+
78
85
  /**
79
86
  * For debug logging only.
80
87
  */
@@ -94,10 +101,14 @@ export abstract class PersistedBatch {
94
101
  ) {
95
102
  this.currentSize = writtenSize;
96
103
  this.logger = options?.logger ?? defaultLogger;
104
+ this.objectStorage = options?.objectStorage;
105
+ if (options?.inlineThresholdBytes != null) {
106
+ this.inlineThresholdBytes = options.inlineThresholdBytes;
107
+ }
97
108
  }
98
109
 
99
110
  saveBucketData(options: SaveBucketDataOptions) {
100
- const remaining_buckets = new Map<string, SaveBucketDataOptions['before_buckets'][number]>();
111
+ const remaining_buckets = new Map<string, SourceRecordBucketState>();
101
112
  for (let bucket of options.before_buckets) {
102
113
  const mapped: SourceRecordBucketState = {
103
114
  bucket: bucket.bucket,
@@ -105,6 +116,11 @@ export abstract class PersistedBatch {
105
116
  id: bucket.id,
106
117
  table: bucket.table
107
118
  };
119
+ if (options.table.bucketDataSourceIds != null && !options.table.bucketDataSourceIds.has(mapped.definitionId!)) {
120
+ // The bucket definition is not active anymore.
121
+ // We don't cleanup these references upfront, but do need to ignore them after the definition is removed.
122
+ continue;
123
+ }
108
124
  remaining_buckets.set(currentBucketKey(mapped), mapped);
109
125
  }
110
126
 
@@ -67,7 +67,7 @@ export abstract class BaseVersionedPowerSyncMongo {
67
67
  }
68
68
 
69
69
  protected async listCollectionsByPrefix<T extends mongo.Document>(prefix: string): Promise<mongo.Collection<T>[]> {
70
- const collections = await this.db.listCollections({ name: new RegExp(`^${prefix}`) }, { nameOnly: true }).toArray();
70
+ const collections = await this.db.listCollections({ name: { $regex: `^${prefix}` } }, { nameOnly: true }).toArray();
71
71
 
72
72
  return collections
73
73
  .filter((collection) => collection.name.startsWith(prefix))
@@ -1,6 +1,6 @@
1
1
  import { storage } from '@powersync/service-core';
2
2
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
3
- import { MongoPersistedSyncRulesContentV1 } from './MongoPersistedSyncRulesContent.js';
3
+ import { MongoPersistedReplicationStream } from './MongoPersistedReplicationStream.js';
4
4
  import { MongoSyncBucketStorage, MongoSyncBucketStorageOptions } from './MongoSyncBucketStorage.js';
5
5
  import { MongoSyncBucketStorageV1 } from './v1/MongoSyncBucketStorageV1.js';
6
6
  import { MongoSyncBucketStorageV3 } from './v3/MongoSyncBucketStorageV3.js';
@@ -11,15 +11,29 @@ export type { MongoSyncBucketStorage };
11
11
 
12
12
  export function createMongoSyncBucketStorage(
13
13
  factory: MongoBucketStorage,
14
- group_id: number,
15
- sync_rules: MongoPersistedSyncRulesContentV1,
16
- slot_name: string,
14
+ replicationStreamId: number,
15
+ replicationStream: MongoPersistedReplicationStream,
16
+ replicationStreamName: string,
17
17
  writeCheckpointMode: storage.WriteCheckpointMode | undefined,
18
18
  options: MongoSyncBucketStorageOptions
19
19
  ): MongoSyncBucketStorage {
20
- if (sync_rules.getStorageConfig().incrementalReprocessing) {
21
- return new MongoSyncBucketStorageV3(factory, group_id, sync_rules, slot_name, writeCheckpointMode, options);
20
+ if (replicationStream.getStorageConfig().incrementalReprocessing) {
21
+ return new MongoSyncBucketStorageV3(
22
+ factory,
23
+ replicationStreamId,
24
+ replicationStream,
25
+ replicationStreamName,
26
+ writeCheckpointMode,
27
+ options
28
+ );
22
29
  }
23
30
 
24
- return new MongoSyncBucketStorageV1(factory, group_id, sync_rules, slot_name, writeCheckpointMode, options);
31
+ return new MongoSyncBucketStorageV1(
32
+ factory,
33
+ replicationStreamId,
34
+ replicationStream,
35
+ replicationStreamName,
36
+ writeCheckpointMode,
37
+ options
38
+ );
25
39
  }
@@ -81,7 +81,7 @@ export class PowerSyncMongo {
81
81
  versioned(storageConfig: StorageConfig & { incrementalReprocessing: false }): VersionedPowerSyncMongoV1;
82
82
  versioned(storageConfig: StorageConfig): VersionedPowerSyncMongo;
83
83
  versioned(storageConfig: StorageConfig): VersionedPowerSyncMongo {
84
- if (storageConfig.incrementalReprocessing) {
84
+ if (storageConfig.version >= storage.STORAGE_VERSION_3) {
85
85
  return new VersionedPowerSyncMongoV3(this, storageConfig);
86
86
  }
87
87
 
@@ -93,22 +93,18 @@ export class PowerSyncMongo {
93
93
  */
94
94
  async listBucketDataCollectionsV3(groupId?: number): Promise<mongo.Collection<BucketDataDocumentV3>[]> {
95
95
  const prefix = groupId == null ? 'bucket_data_' : `bucket_data_${groupId}_`;
96
- const collections = await this.db.listCollections({ name: new RegExp(`^${prefix}`) }, { nameOnly: true }).toArray();
97
-
98
- return collections
99
- .filter((collection) => collection.name.startsWith(prefix))
100
- .map((collection) => this.db.collection<BucketDataDocumentV3>(collection.name));
96
+ return this.collectionsByPrefix<BucketDataDocumentV3>(prefix);
101
97
  }
102
98
 
103
99
  /**
104
100
  * Not safe for user-provided prefix - only for hardcoded values.
105
101
  */
106
- private async collectionsByPrefix(prefix: string): Promise<mongo.Collection<never>[]> {
107
- const collections = await this.db.listCollections({ name: new RegExp(`^${prefix}`) }, { nameOnly: true }).toArray();
102
+ private async collectionsByPrefix<T extends mongo.Document = never>(prefix: string): Promise<mongo.Collection<T>[]> {
103
+ const collections = await this.db.listCollections({ name: { $regex: `^${prefix}` } }, { nameOnly: true }).toArray();
108
104
 
109
105
  return collections
110
106
  .filter((collection) => collection.name.startsWith(prefix))
111
- .map((collection) => this.db.collection<never>(collection.name));
107
+ .map((collection) => this.db.collection<T>(collection.name));
112
108
  }
113
109
 
114
110
  /**
@@ -144,16 +140,12 @@ export class PowerSyncMongo {
144
140
  async listSourceTableCollections(
145
141
  replicationStreamId?: number
146
142
  ): Promise<mongo.Collection<CommonSourceTableDocument>[]> {
147
- const filter =
148
- replicationStreamId == null
149
- ? { name: new RegExp('^source_table_') }
150
- : { name: this.sourceTableCollectionName(replicationStreamId) };
151
- const prefix = replicationStreamId == null ? 'source_table_' : this.sourceTableCollectionName(replicationStreamId);
152
- const collections = await this.db.listCollections(filter, { nameOnly: true }).toArray();
153
-
154
- return collections
155
- .filter((collection) => collection.name.startsWith(prefix))
156
- .map((collection) => this.db.collection<CommonSourceTableDocument>(collection.name));
143
+ if (replicationStreamId == null) {
144
+ return this.collectionsByPrefix('source_table_');
145
+ }
146
+ const name = this.sourceTableCollectionName(replicationStreamId);
147
+ const collections = await this.db.listCollections({ name }, { nameOnly: true }).toArray();
148
+ return collections.map((c) => this.db.collection<CommonSourceTableDocument>(c.name));
157
149
  }
158
150
 
159
151
  /**
@@ -203,7 +195,14 @@ export class PowerSyncMongo {
203
195
  * This is used in a similar way to the Postgres NOTIFY functionality.
204
196
  */
205
197
  async notifyCheckpoint() {
206
- await this.checkpoint_events.insertOne({} as any, { forceServerObjectId: true });
198
+ await this.checkpoint_events.insertOne({} as any, {
199
+ forceServerObjectId: true,
200
+ // No correctness issue if this write is missed / rolled back.
201
+ // It could cause some sync delays, that will resolve in at most a minute, but no consistency issues.
202
+ writeConcern: {
203
+ w: 1
204
+ }
205
+ });
207
206
  }
208
207
 
209
208
  /**
@@ -4,7 +4,7 @@ import { ParameterIndexId, SqliteJsonValue } from '@powersync/service-sync-rules
4
4
  import { event_types } from '@powersync/service-types';
5
5
  import * as bson from 'bson';
6
6
  import type { CurrentDataDocument, SourceTableDocumentV1 } from './v1/models.js';
7
- import type { CurrentBucketV3, CurrentDataDocumentV3, RecordedLookupV3, SourceTableDocumentV3 } from './v3/models.js';
7
+ import type { CurrentDataDocumentV3, RecordedLookupV3 } from './v3/models.js';
8
8
 
9
9
  /**
10
10
  * Replica id uniquely identifying a row on the source database.
@@ -78,6 +78,11 @@ export interface BucketDataProperties {
78
78
  row_id?: string;
79
79
  checksum: bigint;
80
80
  data: string | null;
81
+ /**
82
+ * V1-only.
83
+ *
84
+ * V3 stores this on the BucketDataDocumentV3 instead of the individual ops.
85
+ */
81
86
  target_op?: bigint | null;
82
87
  }
83
88
 
@@ -208,7 +213,7 @@ export interface SyncRuleDocumentBase {
208
213
  storage_version?: number;
209
214
  }
210
215
 
211
- export interface SyncRuleCheckpointFields<TKeepaliveOp extends string | bigint | null> {
216
+ export interface SyncRuleCheckpointFields {
212
217
  /**
213
218
  * The last consistent checkpoint.
214
219
  *
@@ -225,16 +230,6 @@ export interface SyncRuleCheckpointFields<TKeepaliveOp extends string | bigint |
225
230
  * If set, no new checkpoints may be created < this value.
226
231
  */
227
232
  no_checkpoint_before: string | null;
228
-
229
- /**
230
- * Goes together with no_checkpoint_before.
231
- *
232
- * If a keepalive is triggered that creates the checkpoint > no_checkpoint_before,
233
- * then the checkpoint must be equal to this keepalive_op.
234
- *
235
- * This is a string in V1, bigint in V3.
236
- */
237
- keepalive_op: TKeepaliveOp;
238
233
  }
239
234
 
240
235
  export interface StorageConfig extends storage.StorageVersionConfig {
@@ -283,6 +278,11 @@ export interface CustomWriteCheckpointDocument {
283
278
  * This is not unique - multiple write checkpoints can have the same op_id.
284
279
  */
285
280
  op_id?: InternalOpId;
281
+ /**
282
+ * Set when this checkpoint was created from a client-supplied checkpoint
283
+ * request rather than a persistent custom write checkpoint.
284
+ */
285
+ checkpoint_requested_at?: Date | null;
286
286
  }
287
287
 
288
288
  export interface WriteCheckpointDocument {
@@ -296,6 +296,12 @@ export interface WriteCheckpointDocument {
296
296
  * between two checkpoints.
297
297
  */
298
298
  processed_at_lsn: string | null;
299
+ /**
300
+ * Set when this checkpoint was created from a client-supplied checkpoint request
301
+ * rather than a server-generated id. A missing/null value means this is not a
302
+ * checkpoint request.
303
+ */
304
+ checkpoint_requested_at?: Date | null;
299
305
  }
300
306
 
301
307
  export interface InstanceDocument {
@@ -306,6 +312,6 @@ export interface InstanceDocument {
306
312
  export interface ClientConnectionDocument extends event_types.ClientConnection {}
307
313
 
308
314
  export type CurrentDataDocumentId = CurrentDataDocument['_id'] | CurrentDataDocumentV3['_id'];
309
- export type CommonCurrentBucket = CurrentBucket | CurrentBucketV3;
315
+ export type CommonCurrentBucket = CurrentBucket;
310
316
  export type CommonCurrentLookup = bson.Binary | RecordedLookupV3;
311
- export type CommonSourceTableDocument = SourceTableDocumentV1 | SourceTableDocumentV3;
317
+ export type CommonSourceTableDocument = Pick<SourceTableDocumentV1, '_id' | 'snapshot_status'>;