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