@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
@@ -1,29 +1,34 @@
1
1
  import { LEGACY_STORAGE_VERSION, SingleSyncConfigBucketDefinitionMapping, storage } from '@powersync/service-core';
2
- import { DO_NOT_LOG, ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
2
+ import { DO_NOT_LOG, ErrorCode, logger, ReplicationAssertionError, ServiceError } from '@powersync/lib-services-framework';
3
3
  import { v4 as uuid } from 'uuid';
4
4
  import * as lib_mongo from '@powersync/lib-service-mongodb';
5
5
  import { describeIncrementalSyncConfigUpdate, formatIncrementalSyncConfigUpdateLog, isCompatible } from '@powersync/service-core';
6
+ import { Semaphore } from 'async-mutex';
6
7
  import { ObjectId } from 'bson';
7
- import { DEFAULT_CLEAR_BATCH_THROTTLE_RATE } from '../types/types.js';
8
+ import { DEFAULT_CLEAR_BATCH_THROTTLE_RATE, normalizeChunkCompactionConcurrency } from '../types/types.js';
8
9
  import { generateReplicationStreamName } from '../utils/util.js';
9
10
  import { createMongoSyncBucketStorage } from './implementation/createMongoSyncBucketStorage.js';
10
11
  import { getMongoStorageConfig } from './implementation/models.js';
11
12
  import { MongoPersistedReplicationStream } from './implementation/MongoPersistedReplicationStream.js';
12
- import { syncRuleStateUpdatePipeline } from './implementation/SyncRuleStateUpdate.js';
13
+ import { stopReplicationStreamPipeline } from './implementation/SyncRuleStateUpdate.js';
14
+ import { ObjectStorageUsage } from './implementation/v3/object-storage/ObjectStorageUsage.js';
13
15
  export class MongoBucketStorage extends storage.BucketStorageFactory {
14
16
  options;
15
17
  [DO_NOT_LOG] = true;
16
18
  client;
17
- session;
18
19
  replicationStreamNamePrefix;
19
20
  activeStorageCache;
20
21
  db;
22
+ chunkCompactionConcurrency;
23
+ chunkCompactionSlots;
21
24
  constructor(db, options) {
22
25
  super();
23
26
  this.options = options;
27
+ this.chunkCompactionConcurrency = normalizeChunkCompactionConcurrency(options.chunkCompactionConcurrency, options.objectStorage != null);
28
+ // All replication streams created by this factory share the configured limit.
29
+ this.chunkCompactionSlots = new Semaphore(this.chunkCompactionConcurrency);
24
30
  this.client = db.client;
25
31
  this.db = db;
26
- this.session = this.client.startSession();
27
32
  this.replicationStreamNamePrefix = options.replicationStreamNamePrefix;
28
33
  }
29
34
  async [Symbol.asyncDispose]() {
@@ -72,131 +77,200 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
72
77
  };
73
78
  }
74
79
  async restartReplication(replicationStreamId) {
75
- const next = await this.getDeployingSyncConfig();
76
- const active = await this.getActiveSyncConfig();
77
- if (next != null && next.content.replicationStreamId == replicationStreamId) {
78
- // We need to redo the "next" replication stream
79
- await this.updateSyncRules(next.content.asUpdateOptions());
80
- // Pro-actively stop replicating
81
- await this.db.sync_rules.updateOne({
82
- _id: next.content.replicationStreamId,
83
- state: storage.SyncRuleState.PROCESSING
84
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP));
85
- await this.db.notifyCheckpoint();
86
- }
87
- else if (next == null && active?.content.replicationStreamId == replicationStreamId) {
88
- // Slot removed for "active" replication stream, while there is no "next" one.
89
- await this.updateSyncRules(active.content.asUpdateOptions());
90
- // In this case we keep the old one as active for clients, so that that existing clients
91
- // can still get the latest data while we replicate the new ones.
92
- // It will however not replicate anymore.
93
- await this.db.sync_rules.updateOne({
80
+ await this.withTransaction(async (session) => {
81
+ const next = await this.getDeployingSyncConfigInternal(session);
82
+ const active = await this.getActiveSyncConfigInternal(session);
83
+ if (next != null && next.content.replicationStreamId == replicationStreamId) {
84
+ // We need to redo the "next" replication stream.
85
+ // This creates a new stream, and stops the existing PROCESSING one.
86
+ await this.updateSyncRulesInTransaction(next.content.asUpdateOptions({ forceNewReplicationStream: true }), session);
87
+ const sharedActiveStream = active?.content.replicationStreamId == replicationStreamId;
88
+ if (sharedActiveStream) {
89
+ // The same stream was also used for the ACTIVE config. Transition that to ERRORED
90
+ await this.errorActiveStreamForReplacement(active, session);
91
+ }
92
+ }
93
+ else if (next == null && active?.content.replicationStreamId == replicationStreamId) {
94
+ // Slot removed for "active" replication stream, while there is no "next" one.
95
+ await this.updateSyncRulesInTransaction(active.content.asUpdateOptions({ forceNewReplicationStream: true }), session);
96
+ // In this case we keep the old one as active for clients, so that that existing clients
97
+ // can still get the latest data while we replicate the new ones.
98
+ // It will however not replicate anymore.
99
+ await this.errorActiveStreamForReplacement(active, session);
100
+ }
101
+ else if (next != null && active?.content.replicationStreamId == replicationStreamId) {
102
+ // Already have next replication stream, but need to stop replicating the active one.
103
+ await this.errorActiveStreamForReplacement(active, session);
104
+ }
105
+ else {
106
+ // replicationStreamId does not match the ACTIVE or PROCESSING streams - no-op.
107
+ }
108
+ });
109
+ await this.db.notifyCheckpoint();
110
+ }
111
+ /**
112
+ * Keep an active config readable after its replication stream is invalidated.
113
+ *
114
+ * Incremental V3 streams can retain stopped historical configs and embed a deploying config.
115
+ * Read-side storage requires exactly one ACTIVE or ERRORED config, so only the active config
116
+ * transitions to ERRORED; every other embedded config remains or becomes STOP.
117
+ */
118
+ async errorActiveStreamForReplacement(active, session) {
119
+ const activeSyncConfigId = active.content.syncConfigId;
120
+ if (activeSyncConfigId == null) {
121
+ // V1 documents
122
+ const result = await this.db.sync_rules.updateOne({
94
123
  _id: active.content.replicationStreamId,
95
- state: storage.SyncRuleState.ACTIVE
96
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.ERRORED));
97
- await this.db.notifyCheckpoint();
124
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
125
+ }, {
126
+ $set: {
127
+ state: storage.SyncRuleState.ERRORED
128
+ }
129
+ }, { session });
130
+ if (result.matchedCount != 1) {
131
+ // This should generally not happen, since the entire process runs in a transaction.
132
+ // The filters just double-check that the state is what we expect, and fails hard otherwise.
133
+ throw new ReplicationAssertionError(`Active replication stream ${active.content.replicationStreamId} changed during restart`);
134
+ }
135
+ return;
98
136
  }
99
- else if (next != null && active?.content.replicationStreamId == replicationStreamId) {
100
- // Already have next replication stream, but need to stop replicating the active one.
101
- await this.db.sync_rules.updateOne({
102
- _id: active.content.replicationStreamId,
103
- state: storage.SyncRuleState.ACTIVE
104
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.ERRORED));
105
- await this.db.notifyCheckpoint();
137
+ const activeConfigObjectId = new ObjectId(activeSyncConfigId);
138
+ const result = await this.db.sync_rules.updateOne({
139
+ _id: active.content.replicationStreamId,
140
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] },
141
+ sync_configs: {
142
+ // Confirm that this is still the ACTIVE/ERRORED one.
143
+ $elemMatch: {
144
+ _id: activeConfigObjectId,
145
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
146
+ }
147
+ }
148
+ }, {
149
+ $set: {
150
+ state: storage.SyncRuleState.ERRORED,
151
+ // There must be exactly one config with an ERRORED state: This will be used to serve
152
+ // clients until there is a new ACTIVE one.
153
+ 'sync_configs.$[activeConfig].state': storage.SyncRuleState.ERRORED,
154
+ // PROCESSING configs must stop. Historical STOP configs remain unchanged.
155
+ 'sync_configs.$[processingConfig].state': storage.SyncRuleState.STOP
156
+ }
157
+ }, {
158
+ session,
159
+ arrayFilters: [
160
+ {
161
+ 'activeConfig._id': activeConfigObjectId,
162
+ 'activeConfig.state': { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
163
+ },
164
+ {
165
+ 'processingConfig._id': { $ne: activeConfigObjectId },
166
+ 'processingConfig.state': storage.SyncRuleState.PROCESSING
167
+ }
168
+ ]
169
+ });
170
+ if (result.matchedCount != 1) {
171
+ // This should generally not happen, since the entire process runs in a transaction.
172
+ // The filters just double-check that the state is what we expect, and fails hard otherwise.
173
+ throw new ReplicationAssertionError(`Active replication stream ${active.content.replicationStreamId} changed during restart`);
106
174
  }
107
175
  }
108
- async updateSyncRulesV3(options, storageVersion, storageConfig) {
109
- let rules = undefined;
176
+ async updateSyncRulesInTransaction(options, session) {
177
+ const storageVersion = options.storageVersion ??
178
+ options.config.parsed.config.storageVersion ??
179
+ this.options.defaultStorageVersion ??
180
+ storage.CURRENT_STORAGE_VERSION;
181
+ const storageConfig = getMongoStorageConfig(storageVersion);
182
+ if (storageConfig.incrementalReprocessing) {
183
+ return this.updateSyncConfigV3InTransaction(options, storageVersion, storageConfig, session);
184
+ }
185
+ return this.updateSyncConfigV1InTransaction(options, storageVersion, session);
186
+ }
187
+ /**
188
+ * Update to a V3 sync config.
189
+ *
190
+ * Does support cases where existing replication streams are on V1.
191
+ */
192
+ async updateSyncConfigV3InTransaction(options, storageVersion, storageConfig, session) {
110
193
  const versioned = this.db.versioned(storageConfig);
111
- const session = this.session;
112
- await session.withTransaction(async () => {
113
- const active = await this.db.sync_rules.findOne({
114
- state: storage.SyncRuleState.ACTIVE,
115
- storage_version: storageVersion
116
- }, { session, sort: { _id: -1 }, limit: 1 });
117
- if (active != null) {
118
- const existingConfigDocs = await this.loadSyncConfigDefinitions(versioned, active, session);
119
- if (this.options.supportsMultipleSyncConfigs &&
120
- isCompatible(existingConfigDocs.map((d) => d.serialized_plan ?? null), options.config, logger)) {
121
- logger.info(`Using incremental reprocessing`);
122
- await this.db.sync_rules.updateMany({
123
- state: storage.SyncRuleState.PROCESSING
124
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP), { session });
125
- await this.stopEmbeddedDeployingConfigs(active, session);
126
- rules = await this.appendSyncConfigToStream({
127
- versioned,
128
- existing: active,
129
- existingConfigDocs,
130
- options,
131
- storageVersion,
132
- session
133
- });
134
- return;
135
- }
136
- await this.stopEmbeddedDeployingConfigs(active, session);
194
+ const active = await this.db.sync_rules.findOne({
195
+ state: storage.SyncRuleState.ACTIVE,
196
+ storage_version: storageVersion
197
+ }, { session, sort: { _id: -1 }, limit: 1 });
198
+ if (active != null) {
199
+ const existingConfigDocs = await this.loadSyncConfigDefinitions(versioned, active, session);
200
+ if (!options.forceNewReplicationStream &&
201
+ this.options.supportsMultipleSyncConfigs &&
202
+ isCompatible(existingConfigDocs.map((d) => d.serialized_plan ?? null), options.config, logger)) {
203
+ logger.info(`Using incremental reprocessing`);
204
+ await this.stopExistingProcessingWork(session);
205
+ return await this.appendSyncConfigToStream({
206
+ versioned,
207
+ existing: active,
208
+ existingConfigDocs,
209
+ options,
210
+ storageVersion,
211
+ session
212
+ });
137
213
  }
138
- // Only have a single replication stream with PROCESSING.
139
- await this.db.sync_rules.updateMany({
140
- state: storage.SyncRuleState.PROCESSING
141
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP), { session });
142
- const id_doc = await this.db.op_id_sequence.findOneAndUpdate({
143
- _id: 'sync_rules'
144
- }, {
145
- $inc: {
146
- op_id: 1n
147
- }
148
- }, {
149
- upsert: true,
150
- returnDocument: 'after',
151
- session
152
- });
153
- const id = Number(id_doc.op_id);
154
- const replicationStreamName = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
155
- const mapping = options.config.plan == null
156
- ? // For legacy sync rules and streams, use the parsed config directly to create a mapping
157
- SingleSyncConfigBucketDefinitionMapping.fromParsedSyncConfig(options.config.parsed)
158
- : // For new sync streams, always use the serialized version
159
- SingleSyncConfigBucketDefinitionMapping.constructIncrementalMappingFromSerializedPlans([], options.config.plan.plan, []);
160
- const syncConfigDoc = {
161
- _id: new ObjectId(),
162
- replication_stream_id: id,
163
- created_at: new Date(),
164
- storage_version: storageVersion,
165
- content: options.config.yaml,
166
- serialized_plan: options.config.plan,
167
- rule_mapping: mapping.serialize()
168
- };
169
- await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
170
- const doc = {
171
- _id: id,
172
- storage_version: storageVersion,
173
- sync_configs: [
174
- {
175
- _id: syncConfigDoc._id,
176
- state: storage.SyncRuleState.PROCESSING,
177
- last_checkpoint: null,
178
- last_checkpoint_lsn: null,
179
- no_checkpoint_before: null,
180
- snapshot_done: false
181
- }
182
- ],
183
- snapshot_lsn: undefined,
184
- state: storage.SyncRuleState.PROCESSING,
185
- slot_name: replicationStreamName,
186
- last_checkpoint_ts: null,
187
- last_fatal_error: null,
188
- last_fatal_error_ts: null,
189
- last_keepalive_ts: null
190
- };
191
- await this.db.sync_rules.insertOne(doc, { session });
192
- rules = new MongoPersistedReplicationStream(this.db, doc, [syncConfigDoc]);
193
- if (options.lock) {
194
- // The lock is persisted on rules.current_lock
195
- await rules.lock(session);
214
+ }
215
+ await this.stopExistingProcessingWork(session);
216
+ const id_doc = await this.db.op_id_sequence.findOneAndUpdate({
217
+ _id: 'sync_rules'
218
+ }, {
219
+ $inc: {
220
+ op_id: 1n
196
221
  }
222
+ }, {
223
+ upsert: true,
224
+ returnDocument: 'after',
225
+ session
197
226
  });
198
- // Notify only after the transaction has committed, so listeners cannot observe pre-commit state.
199
- await this.db.notifyCheckpoint();
227
+ const id = Number(id_doc.op_id);
228
+ const replicationStreamName = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
229
+ const mapping = options.config.plan == null
230
+ ? // For legacy sync rules and streams, use the parsed config directly to create a mapping
231
+ SingleSyncConfigBucketDefinitionMapping.fromParsedSyncConfig(options.config.parsed)
232
+ : // For new sync streams, always use the serialized version
233
+ SingleSyncConfigBucketDefinitionMapping.constructIncrementalMappingFromSerializedPlans([], options.config.plan.plan, []);
234
+ const syncConfigDoc = {
235
+ _id: new ObjectId(),
236
+ replication_stream_id: id,
237
+ created_at: new Date(),
238
+ storage_version: storageVersion,
239
+ content: options.config.yaml,
240
+ version_label: options.version_label,
241
+ serialized_plan: options.config.plan,
242
+ rule_mapping: mapping.serialize()
243
+ };
244
+ await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
245
+ const doc = {
246
+ _id: id,
247
+ storage_version: storageVersion,
248
+ sync_configs: [
249
+ {
250
+ _id: syncConfigDoc._id,
251
+ state: storage.SyncRuleState.PROCESSING,
252
+ last_checkpoint: null,
253
+ last_checkpoint_lsn: null,
254
+ no_checkpoint_before: null,
255
+ snapshot_done: false
256
+ }
257
+ ],
258
+ snapshot_lsn: undefined,
259
+ state: storage.SyncRuleState.PROCESSING,
260
+ slot_name: replicationStreamName,
261
+ last_checkpoint_ts: null,
262
+ last_fatal_error: null,
263
+ last_fatal_error_ts: null,
264
+ last_keepalive_ts: null
265
+ };
266
+ await this.db.sync_rules.insertOne(doc, { session });
267
+ const rules = new MongoPersistedReplicationStream(this.db, doc, [syncConfigDoc]);
268
+ if (options.lock) {
269
+ // We only lock when creating a new stream - otherwise we'll likely get lock contention
270
+ // from an existing job on the stream, which would fail the entire transaction.
271
+ // The lock is persisted on rules.current_lock
272
+ await rules.lock(session);
273
+ }
200
274
  return rules;
201
275
  }
202
276
  async loadSyncConfigDefinitions(versioned, existing, session) {
@@ -224,25 +298,6 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
224
298
  })
225
299
  .toArray();
226
300
  }
227
- async stopEmbeddedDeployingConfigs(existing, session) {
228
- const deployingConfigs = existing.sync_configs
229
- .filter((config) => config.state == storage.SyncRuleState.PROCESSING)
230
- .map((config) => config._id);
231
- if (deployingConfigs.length == 0) {
232
- return;
233
- }
234
- await this.db.sync_rules.updateOne({
235
- _id: existing._id,
236
- 'sync_configs._id': { $in: deployingConfigs }
237
- }, {
238
- $set: {
239
- 'sync_configs.$[config].state': storage.SyncRuleState.STOP
240
- }
241
- }, {
242
- session,
243
- arrayFilters: [{ 'config._id': { $in: deployingConfigs } }]
244
- });
245
- }
246
301
  logIncrementalDefinitionChanges(changes) {
247
302
  logger.info(`Incremental reprocessing sync config update:\n${formatIncrementalSyncConfigUpdateLog(changes)}`);
248
303
  }
@@ -268,6 +323,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
268
323
  created_at: new Date(),
269
324
  storage_version: storageVersion,
270
325
  content: updateOptions.config.yaml,
326
+ version_label: updateOptions.version_label,
271
327
  serialized_plan: updateOptions.config.plan,
272
328
  rule_mapping: mapping.serialize()
273
329
  };
@@ -303,68 +359,102 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
303
359
  return stream;
304
360
  }
305
361
  async updateSyncRules(options) {
306
- const storageVersion = options.storageVersion ?? options.config.parsed.config.storageVersion ?? storage.CURRENT_STORAGE_VERSION;
307
- const storageConfig = getMongoStorageConfig(storageVersion);
308
- if (storageConfig.incrementalReprocessing) {
309
- return this.updateSyncRulesV3(options, storageVersion, storageConfig);
310
- }
311
- let rules = undefined;
312
- const session = this.session;
313
- await session.withTransaction(async () => {
314
- // Only have a single replication stream with PROCESSING.
315
- await this.db.sync_rules.updateMany({
316
- state: storage.SyncRuleState.PROCESSING
317
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP), { session });
318
- const id_doc = await this.db.op_id_sequence.findOneAndUpdate({
319
- _id: 'sync_rules'
320
- }, {
321
- $inc: {
322
- op_id: 1n
323
- }
324
- }, {
325
- upsert: true,
326
- returnDocument: 'after',
327
- session
328
- });
329
- const id = Number(id_doc.op_id);
330
- const slot_name = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
331
- const doc = {
332
- _id: id,
333
- storage_version: storageVersion,
334
- content: options.config.yaml,
335
- serialized_plan: options.config.plan,
336
- last_checkpoint: null,
337
- last_checkpoint_lsn: null,
338
- no_checkpoint_before: null,
339
- keepalive_op: null,
340
- snapshot_done: false,
341
- snapshot_lsn: undefined,
342
- state: storage.SyncRuleState.PROCESSING,
343
- slot_name: slot_name,
344
- last_checkpoint_ts: null,
345
- last_fatal_error: null,
346
- last_fatal_error_ts: null,
347
- last_keepalive_ts: null
348
- };
349
- await this.db.sync_rules.insertOne(doc, { session });
350
- await this.db.notifyCheckpoint();
351
- rules = new MongoPersistedReplicationStream(this.db, doc);
352
- if (options.lock) {
353
- // The lock is persisted on rules.current_lock
354
- await rules.lock(session);
362
+ let rules;
363
+ await this.withTransaction(async (session) => {
364
+ rules = await this.updateSyncRulesInTransaction(options, session);
365
+ });
366
+ await this.db.notifyCheckpoint();
367
+ return rules;
368
+ }
369
+ async withTransaction(callback) {
370
+ return this.client.withSession((session) => session.withTransaction(callback));
371
+ }
372
+ /**
373
+ * Update to a V1 sync config.
374
+ *
375
+ * Does support cases where existing replication streams are on V3.
376
+ */
377
+ async updateSyncConfigV1InTransaction(options, storageVersion, session) {
378
+ await this.stopExistingProcessingWork(session);
379
+ const id_doc = await this.db.op_id_sequence.findOneAndUpdate({
380
+ _id: 'sync_rules'
381
+ }, {
382
+ $inc: {
383
+ op_id: 1n
355
384
  }
385
+ }, {
386
+ upsert: true,
387
+ returnDocument: 'after',
388
+ session
356
389
  });
390
+ const id = Number(id_doc.op_id);
391
+ const slot_name = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
392
+ // All V1 replication streams share both the `main` op id sequence and the `bucket_parameters`
393
+ // collection, so every parameter entry this stream writes gets an op id above the current
394
+ // head. Seeding the parameter compaction cursor with that head keeps the stream's first
395
+ // compaction from scanning other streams' history, which would otherwise be repeated for
396
+ // every new deployment. A concurrent replication flush can only advance the head after this
397
+ // read, which makes the seed conservative, never too high.
398
+ const opSequence = await this.db.op_id_sequence.findOne({ _id: 'main' }, { session });
399
+ const doc = {
400
+ _id: id,
401
+ storage_version: storageVersion,
402
+ content: options.config.yaml,
403
+ version_label: options.version_label,
404
+ serialized_plan: options.config.plan,
405
+ last_checkpoint: null,
406
+ last_checkpoint_lsn: null,
407
+ no_checkpoint_before: null,
408
+ keepalive_op: null,
409
+ snapshot_done: false,
410
+ snapshot_lsn: undefined,
411
+ state: storage.SyncRuleState.PROCESSING,
412
+ slot_name: slot_name,
413
+ last_checkpoint_ts: null,
414
+ last_fatal_error: null,
415
+ last_fatal_error_ts: null,
416
+ last_keepalive_ts: null,
417
+ parameter_compaction: { compacted_before: opSequence?.op_id ?? 0n }
418
+ };
419
+ await this.db.sync_rules.insertOne(doc, { session });
420
+ const rules = new MongoPersistedReplicationStream(this.db, doc);
421
+ if (options.lock) {
422
+ // The lock is persisted on rules.current_lock
423
+ await rules.lock(session);
424
+ }
357
425
  return rules;
358
426
  }
427
+ /**
428
+ * Stop top-level and embedded processing work before creating a replacement stream.
429
+ *
430
+ * Use in the same transaction that the replacement stream is created.
431
+ */
432
+ async stopExistingProcessingWork(session) {
433
+ await this.db.sync_rules.updateMany({
434
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] },
435
+ 'sync_configs.state': storage.SyncRuleState.PROCESSING
436
+ }, {
437
+ $set: {
438
+ 'sync_configs.$[config].state': storage.SyncRuleState.STOP
439
+ }
440
+ }, {
441
+ session,
442
+ arrayFilters: [{ 'config.state': storage.SyncRuleState.PROCESSING }]
443
+ });
444
+ await this.db.sync_rules.updateMany({
445
+ state: storage.SyncRuleState.PROCESSING
446
+ }, stopReplicationStreamPipeline(), { session });
447
+ }
359
448
  async getActiveSyncConfig() {
449
+ return this.getActiveSyncConfigInternal();
450
+ }
451
+ async getActiveSyncConfigInternal(session) {
360
452
  const doc = await this.db.sync_rules.findOne({
361
453
  state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
362
- }, { sort: { _id: -1 }, limit: 1 });
363
- return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED], {
364
- cacheActiveStorage: true
365
- });
454
+ }, { session, sort: { _id: -1 }, limit: 1 });
455
+ return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED], { cacheActiveStorage: session == null }, session);
366
456
  }
367
- async replicationStreamFromDoc(doc, stateFilter) {
457
+ async replicationStreamFromDoc(doc, stateFilter, session) {
368
458
  if (doc == null) {
369
459
  return null;
370
460
  }
@@ -382,7 +472,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
382
472
  const syncConfigDocs = await db.syncConfigDefinitions
383
473
  .find({
384
474
  _id: { $in: matching.map((config) => config._id) }
385
- })
475
+ }, { session })
386
476
  .toArray();
387
477
  if (syncConfigDocs.length == 0) {
388
478
  return null;
@@ -392,10 +482,13 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
392
482
  return new MongoPersistedReplicationStream(this.db, doc);
393
483
  }
394
484
  async getDeployingSyncConfig() {
485
+ return this.getDeployingSyncConfigInternal();
486
+ }
487
+ async getDeployingSyncConfigInternal(session) {
395
488
  const doc = await this.db.sync_rules.findOne({
396
489
  $or: [{ state: storage.SyncRuleState.PROCESSING }, { 'sync_configs.state': storage.SyncRuleState.PROCESSING }]
397
- }, { sort: { _id: -1 }, limit: 1 });
398
- return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.PROCESSING]);
490
+ }, { session, sort: { _id: -1 }, limit: 1 });
491
+ return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.PROCESSING], {}, session);
399
492
  }
400
493
  async getReplicatingReplicationStreams() {
401
494
  const docs = await this.db.sync_rules
@@ -417,8 +510,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
417
510
  return this.replicationStreamFromDoc(doc, [storage.SyncRuleState.STOP]);
418
511
  }))).filter((d) => d != null);
419
512
  }
420
- async resolvedSyncConfigFromDoc(doc, stateFilter, options = {}) {
421
- const stream = await this.replicationStreamFromDoc(doc, stateFilter);
513
+ async resolvedSyncConfigFromDoc(doc, stateFilter, options = {}, session) {
514
+ const stream = await this.replicationStreamFromDoc(doc, stateFilter, session);
422
515
  if (stream == null) {
423
516
  return null;
424
517
  }
@@ -445,7 +538,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
445
538
  }
446
539
  async getStorageMetrics() {
447
540
  const ignoreNotExisting = (e) => {
448
- if (lib_mongo.isMongoServerError(e) && e.codeName == 'NamespaceNotFound') {
541
+ if (lib_mongo.isMongoNamespaceNotFoundError(e)) {
449
542
  // Collection doesn't exist - return 0
450
543
  return [{ storageStats: { size: 0 } }];
451
544
  }
@@ -453,8 +546,6 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
453
546
  return Promise.reject(e);
454
547
  }
455
548
  };
456
- // For now, we get storage metrics over all v1 and v3 collections.
457
- // In the future, we may split these metrics to report separately for active replication streams versus processing streams.
458
549
  const aggregateStaticCollection = async (collection) => {
459
550
  // We check whether the collection exists before getting the statistics. This avoids repeated
460
551
  // errors in the MongoDB logs if the collection hasn't been created yet.
@@ -475,7 +566,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
475
566
  .catch(ignoreNotExisting);
476
567
  };
477
568
  const operations_aggregate = await aggregateStaticCollection(this.db.bucket_data);
478
- const v3_operation_aggregates = await Promise.all((await this.db.listBucketDataCollectionsV3()).map((collection) => collection
569
+ const v3OperationCollections = await this.db.listBucketDataCollectionsV3();
570
+ const v3_operation_aggregates = await Promise.all(v3OperationCollections.map((collection) => collection
479
571
  .aggregate([
480
572
  {
481
573
  $collStats: {
@@ -486,7 +578,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
486
578
  .toArray()
487
579
  .catch(ignoreNotExisting)));
488
580
  const parameters_aggregate = await aggregateStaticCollection(this.db.bucket_parameters);
489
- const v3_parameter_aggregates = await Promise.all((await this.db.listAllParameterIndexCollectionsV3()).map((collection) => collection
581
+ const v3ParameterCollections = await this.db.listAllParameterIndexCollectionsV3();
582
+ const v3_parameter_aggregates = await Promise.all(v3ParameterCollections.map((collection) => collection
490
583
  .aggregate([
491
584
  {
492
585
  $collStats: {
@@ -497,7 +590,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
497
590
  .toArray()
498
591
  .catch(ignoreNotExisting)));
499
592
  const v1_source_record_aggregate = await aggregateStaticCollection(this.db.current_data);
500
- const source_record_aggregates = await Promise.all((await this.db.listAllSourceRecordCollectionsV3()).map((collection) => collection
593
+ const v3SourceRecordCollections = await this.db.listAllSourceRecordCollectionsV3();
594
+ const source_record_aggregates = await Promise.all(v3SourceRecordCollections.map((collection) => collection
501
595
  .aggregate([
502
596
  {
503
597
  $collStats: {
@@ -507,13 +601,84 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
507
601
  ])
508
602
  .toArray()
509
603
  .catch(ignoreNotExisting)));
604
+ const v3StorageConfig = getMongoStorageConfig(storage.STORAGE_VERSION_3);
605
+ const v3Db = this.db.versioned(v3StorageConfig);
606
+ const objectStorageDefinitionUsage = await ObjectStorageUsage.readAllDefinitionUsage(v3Db);
607
+ const v3StreamDocs = (await this.db.sync_rules
608
+ .find({ storage_version: { $gte: storage.STORAGE_VERSION_3 } }, { projection: { _id: 1, sync_configs: 1 } })
609
+ .toArray());
610
+ const collectionSizes = new Map();
611
+ const addCollectionSize = (collection, aggregate, prefix, sizes) => {
612
+ const match = collection.collectionName.match(new RegExp(`^${prefix}(\\d+)_(.+)$`));
613
+ if (match == null) {
614
+ return;
615
+ }
616
+ sizes.set(`${prefix}${match[1]}:${match[2]}`, Number(aggregate[0]?.storageStats?.size ?? 0));
617
+ };
618
+ v3OperationCollections.forEach((collection, index) => addCollectionSize(collection, v3_operation_aggregates[index], 'bucket_data_', collectionSizes));
619
+ v3ParameterCollections.forEach((collection, index) => addCollectionSize(collection, v3_parameter_aggregates[index], 'parameter_index_', collectionSizes));
620
+ v3SourceRecordCollections.forEach((collection, index) => addCollectionSize(collection, source_record_aggregates[index], 'source_records_', collectionSizes));
621
+ const syncConfigIds = v3StreamDocs.flatMap((stream) => (stream.sync_configs ?? []).map((config) => config._id));
622
+ const syncConfigDefinitions = syncConfigIds.length == 0
623
+ ? []
624
+ : await v3Db.syncConfigDefinitions
625
+ .find({ _id: { $in: syncConfigIds } }, { projection: { _id: 1, rule_mapping: 1, version_label: 1 } })
626
+ .toArray();
627
+ const syncConfigDefinitionsById = new Map(syncConfigDefinitions.map((definition) => [definition._id.toHexString(), definition]));
628
+ const sourceTablesByStream = new Map();
629
+ await Promise.all(v3StreamDocs.map(async (stream) => {
630
+ const sourceTables = await v3Db
631
+ .sourceTables(stream._id)
632
+ .find({}, { projection: { _id: 1, bucket_data_source_ids: 1, parameter_lookup_source_ids: 1 } })
633
+ .toArray();
634
+ sourceTablesByStream.set(stream._id, sourceTables);
635
+ }));
636
+ const objectStorageSizeByDefinition = new Map();
637
+ for (const usage of objectStorageDefinitionUsage) {
638
+ objectStorageSizeByDefinition.set(`${usage.replication_stream_id}:${usage.definition_id}`, Number(usage.active_bytes));
639
+ }
640
+ const sumCollectionSizes = (prefix, streamId, ids) => [...ids].reduce((total, id) => total + (collectionSizes.get(`${prefix}${streamId}:${id}`) ?? 0), 0);
641
+ const syncConfigMetrics = [];
642
+ for (const stream of v3StreamDocs) {
643
+ const sourceTables = sourceTablesByStream.get(stream._id) ?? [];
644
+ for (const syncConfig of stream.sync_configs ?? []) {
645
+ const definition = syncConfigDefinitionsById.get(syncConfig._id.toHexString());
646
+ if (definition == null) {
647
+ continue;
648
+ }
649
+ const mapping = SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(definition.rule_mapping);
650
+ const bucketDefinitionIds = mapping.allBucketDefinitionIds();
651
+ const parameterIndexIds = mapping.allParameterIndexIds();
652
+ const bucketDefinitionIdSet = new Set(bucketDefinitionIds);
653
+ const parameterIndexIdSet = new Set(parameterIndexIds);
654
+ const replicationSize = sourceTables.reduce((total, sourceTable) => {
655
+ if (!(sourceTable.bucket_data_source_ids ?? []).some((id) => bucketDefinitionIdSet.has(id)) &&
656
+ !(sourceTable.parameter_lookup_source_ids ?? []).some((id) => parameterIndexIdSet.has(id))) {
657
+ return total;
658
+ }
659
+ return total + (collectionSizes.get(`source_records_${stream._id}:${sourceTable._id.toHexString()}`) ?? 0);
660
+ }, 0);
661
+ syncConfigMetrics.push({
662
+ sync_config_id: syncConfig._id.toHexString(),
663
+ sync_config_state: String(syncConfig.state),
664
+ version_label: definition.version_label,
665
+ attributed_bucket_data_bytes: sumCollectionSizes('bucket_data_', stream._id, bucketDefinitionIdSet),
666
+ attributed_parameter_indexes_bytes: sumCollectionSizes('parameter_index_', stream._id, parameterIndexIdSet),
667
+ attributed_source_records_bytes: replicationSize,
668
+ attributed_object_storage_bytes: [...bucketDefinitionIdSet].reduce((total, definitionId) => total + (objectStorageSizeByDefinition.get(`${stream._id}:${definitionId}`) ?? 0), 0)
669
+ });
670
+ }
671
+ }
672
+ const totalObjectStorageSize = objectStorageDefinitionUsage.reduce((total, usage) => total + usage.active_bytes, 0n);
510
673
  return {
511
674
  operations_size_bytes: Number(operations_aggregate[0].storageStats.size) +
512
675
  v3_operation_aggregates.reduce((total, aggregate) => total + Number(aggregate[0].storageStats.size), 0),
513
676
  parameters_size_bytes: Number(parameters_aggregate[0].storageStats.size) +
514
677
  v3_parameter_aggregates.reduce((total, aggregate) => total + Number(aggregate[0].storageStats.size), 0),
515
678
  replication_size_bytes: Number(v1_source_record_aggregate[0]?.storageStats?.size ?? 0) +
516
- source_record_aggregates.reduce((total, aggregate) => total + Number(aggregate[0]?.storageStats?.size ?? 0), 0)
679
+ source_record_aggregates.reduce((total, aggregate) => total + Number(aggregate[0]?.storageStats?.size ?? 0), 0),
680
+ object_storage_size_bytes: Number(totalObjectStorageSize),
681
+ sync_config_metrics: syncConfigMetrics
517
682
  };
518
683
  }
519
684
  async getPowerSyncInstanceId() {