@powersync/service-module-mongodb-storage 0.19.0 → 0.21.0

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