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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +31 -3
  3. package/dist/storage/MongoBucketStorage.js +365 -206
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +15 -4
  6. package/dist/storage/implementation/MongoBucketBatch.js +35 -9
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
  9. package/dist/storage/implementation/MongoCompactor.js +27 -238
  10. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  11. package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
  12. package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
  13. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
  15. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
  16. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  17. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
  18. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  19. package/dist/storage/implementation/MongoStorageProvider.js +3 -1
  20. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  21. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
  22. package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
  23. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  24. package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
  25. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  26. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
  27. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +7 -28
  28. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  29. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  30. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  31. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  32. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
  33. package/dist/storage/implementation/common/PersistedBatch.d.ts +27 -0
  34. package/dist/storage/implementation/common/PersistedBatch.js +43 -2
  35. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  36. package/dist/storage/implementation/db.d.ts +4 -1
  37. package/dist/storage/implementation/db.js +14 -1
  38. package/dist/storage/implementation/db.js.map +1 -1
  39. package/dist/storage/implementation/models.d.ts +31 -0
  40. package/dist/storage/implementation/models.js.map +1 -1
  41. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +2 -0
  42. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
  43. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  44. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +36 -5
  45. package/dist/storage/implementation/v1/MongoCompactorV1.js +229 -8
  46. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  47. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  48. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  49. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  50. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  51. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
  52. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  53. package/dist/storage/implementation/v1/models.js +1 -0
  54. package/dist/storage/implementation/v1/models.js.map +1 -1
  55. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  56. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  57. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  58. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -1
  59. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +90 -19
  60. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  61. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +77 -25
  62. package/dist/storage/implementation/v3/MongoCompactorV3.js +585 -141
  63. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  64. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  65. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  66. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  67. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  68. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  69. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  70. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  71. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
  72. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  73. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  74. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  75. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  76. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -0
  77. package/dist/storage/implementation/v3/PersistedBatchV3.js +52 -11
  78. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  79. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  80. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  81. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  82. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  83. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  84. package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
  85. package/dist/storage/implementation/v3/compact-utils.js +216 -0
  86. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  87. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  88. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  89. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  90. package/dist/storage/implementation/v3/models.d.ts +84 -4
  91. package/dist/storage/implementation/v3/models.js.map +1 -1
  92. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  93. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -4
  94. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  95. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  96. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -5
  97. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +10 -2
  98. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  99. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
  100. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
  101. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  102. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  103. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +177 -23
  104. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  105. package/dist/storage/implementation/v3/source-table-utils.d.ts +6 -6
  106. package/dist/storage/implementation/v3/source-table-utils.js +30 -37
  107. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  108. package/dist/storage/storage-index.d.ts +2 -1
  109. package/dist/storage/storage-index.js +1 -0
  110. package/dist/storage/storage-index.js.map +1 -1
  111. package/dist/types/types.d.ts +23 -1
  112. package/dist/types/types.js +16 -7
  113. package/dist/types/types.js.map +1 -1
  114. package/dist/utils/util.js +3 -1
  115. package/dist/utils/util.js.map +1 -1
  116. package/package.json +10 -9
  117. package/src/storage/MongoBucketStorage.ts +495 -268
  118. package/src/storage/implementation/MongoBucketBatch.ts +54 -14
  119. package/src/storage/implementation/MongoCompactor.ts +35 -346
  120. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  121. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
  122. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
  123. package/src/storage/implementation/MongoStorageProvider.ts +3 -1
  124. package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
  125. package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
  126. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +12 -44
  127. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  128. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  129. package/src/storage/implementation/common/PersistedBatch.ts +54 -2
  130. package/src/storage/implementation/db.ts +21 -2
  131. package/src/storage/implementation/models.ts +34 -0
  132. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +31 -0
  133. package/src/storage/implementation/v1/MongoCompactorV1.ts +303 -19
  134. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  135. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
  136. package/src/storage/implementation/v1/models.ts +1 -0
  137. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  138. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +124 -22
  139. package/src/storage/implementation/v3/MongoCompactorV3.ts +744 -190
  140. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  141. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  142. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
  143. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  144. package/src/storage/implementation/v3/PersistedBatchV3.ts +57 -12
  145. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  146. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  147. package/src/storage/implementation/v3/compact-utils.ts +320 -0
  148. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  149. package/src/storage/implementation/v3/models.ts +91 -3
  150. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +16 -8
  151. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  152. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +15 -4
  153. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
  154. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +257 -27
  155. package/src/storage/implementation/v3/source-table-utils.ts +39 -41
  156. package/src/storage/storage-index.ts +2 -0
  157. package/src/types/types.ts +24 -7
  158. package/src/utils/util.ts +3 -1
  159. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  160. package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
  161. package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
  162. package/test/src/compact-utils.test.ts +226 -0
  163. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  164. package/test/src/object_storage_usage.test.ts +255 -0
  165. package/test/src/parameter_compacting_v1.test.ts +120 -0
  166. package/test/src/parameter_compacting_v3.test.ts +192 -0
  167. package/test/src/parameter_compaction_fence.test.ts +276 -0
  168. package/test/src/storage.test.ts +273 -22
  169. package/test/src/storage_compacting.test.ts +714 -244
  170. package/test/src/storage_s3_checksums.test.ts +3 -4
  171. package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
  172. package/test/src/storage_s3_reading.test.ts +243 -1
  173. package/test/src/storage_s3_writing.test.ts +1 -0
  174. package/test/src/storage_sync.test.ts +336 -41
  175. package/test/src/util.ts +1 -1
  176. package/tsconfig.tsbuildinfo +1 -1
@@ -1,5 +1,5 @@
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';
@@ -9,12 +9,12 @@ import { generateReplicationStreamName } from '../utils/util.js';
9
9
  import { createMongoSyncBucketStorage } from './implementation/createMongoSyncBucketStorage.js';
10
10
  import { getMongoStorageConfig } from './implementation/models.js';
11
11
  import { MongoPersistedReplicationStream } from './implementation/MongoPersistedReplicationStream.js';
12
- import { syncRuleStateUpdatePipeline } from './implementation/SyncRuleStateUpdate.js';
12
+ import { stopReplicationStreamPipeline } from './implementation/SyncRuleStateUpdate.js';
13
+ import { ObjectStorageUsage } from './implementation/v3/object-storage/ObjectStorageUsage.js';
13
14
  export class MongoBucketStorage extends storage.BucketStorageFactory {
14
15
  options;
15
16
  [DO_NOT_LOG] = true;
16
17
  client;
17
- session;
18
18
  replicationStreamNamePrefix;
19
19
  activeStorageCache;
20
20
  db;
@@ -23,7 +23,6 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
23
23
  this.options = options;
24
24
  this.client = db.client;
25
25
  this.db = db;
26
- this.session = this.client.startSession();
27
26
  this.replicationStreamNamePrefix = options.replicationStreamNamePrefix;
28
27
  }
29
28
  async [Symbol.asyncDispose]() {
@@ -72,131 +71,200 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
72
71
  };
73
72
  }
74
73
  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({
74
+ await this.withTransaction(async (session) => {
75
+ const next = await this.getDeployingSyncConfigInternal(session);
76
+ const active = await this.getActiveSyncConfigInternal(session);
77
+ if (next != null && next.content.replicationStreamId == replicationStreamId) {
78
+ // We need to redo the "next" replication stream.
79
+ // This creates a new stream, and stops the existing PROCESSING one.
80
+ await this.updateSyncRulesInTransaction(next.content.asUpdateOptions({ forceNewReplicationStream: true }), session);
81
+ const sharedActiveStream = active?.content.replicationStreamId == replicationStreamId;
82
+ if (sharedActiveStream) {
83
+ // The same stream was also used for the ACTIVE config. Transition that to ERRORED
84
+ await this.errorActiveStreamForReplacement(active, session);
85
+ }
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.updateSyncRulesInTransaction(active.content.asUpdateOptions({ forceNewReplicationStream: true }), session);
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.errorActiveStreamForReplacement(active, session);
94
+ }
95
+ else if (next != null && active?.content.replicationStreamId == replicationStreamId) {
96
+ // Already have next replication stream, but need to stop replicating the active one.
97
+ await this.errorActiveStreamForReplacement(active, session);
98
+ }
99
+ else {
100
+ // replicationStreamId does not match the ACTIVE or PROCESSING streams - no-op.
101
+ }
102
+ });
103
+ await this.db.notifyCheckpoint();
104
+ }
105
+ /**
106
+ * Keep an active config readable after its replication stream is invalidated.
107
+ *
108
+ * Incremental V3 streams can retain stopped historical configs and embed a deploying config.
109
+ * Read-side storage requires exactly one ACTIVE or ERRORED config, so only the active config
110
+ * transitions to ERRORED; every other embedded config remains or becomes STOP.
111
+ */
112
+ async errorActiveStreamForReplacement(active, session) {
113
+ const activeSyncConfigId = active.content.syncConfigId;
114
+ if (activeSyncConfigId == null) {
115
+ // V1 documents
116
+ const result = await this.db.sync_rules.updateOne({
94
117
  _id: active.content.replicationStreamId,
95
- state: storage.SyncRuleState.ACTIVE
96
- }, syncRuleStateUpdatePipeline(storage.SyncRuleState.ERRORED));
97
- await this.db.notifyCheckpoint();
118
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
119
+ }, {
120
+ $set: {
121
+ state: storage.SyncRuleState.ERRORED
122
+ }
123
+ }, { session });
124
+ if (result.matchedCount != 1) {
125
+ // This should generally not happen, since the entire process runs in a transaction.
126
+ // The filters just double-check that the state is what we expect, and fails hard otherwise.
127
+ throw new ReplicationAssertionError(`Active replication stream ${active.content.replicationStreamId} changed during restart`);
128
+ }
129
+ return;
98
130
  }
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();
131
+ const activeConfigObjectId = new ObjectId(activeSyncConfigId);
132
+ const result = await this.db.sync_rules.updateOne({
133
+ _id: active.content.replicationStreamId,
134
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] },
135
+ sync_configs: {
136
+ // Confirm that this is still the ACTIVE/ERRORED one.
137
+ $elemMatch: {
138
+ _id: activeConfigObjectId,
139
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
140
+ }
141
+ }
142
+ }, {
143
+ $set: {
144
+ state: storage.SyncRuleState.ERRORED,
145
+ // There must be exactly one config with an ERRORED state: This will be used to serve
146
+ // clients until there is a new ACTIVE one.
147
+ 'sync_configs.$[activeConfig].state': storage.SyncRuleState.ERRORED,
148
+ // PROCESSING configs must stop. Historical STOP configs remain unchanged.
149
+ 'sync_configs.$[processingConfig].state': storage.SyncRuleState.STOP
150
+ }
151
+ }, {
152
+ session,
153
+ arrayFilters: [
154
+ {
155
+ 'activeConfig._id': activeConfigObjectId,
156
+ 'activeConfig.state': { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
157
+ },
158
+ {
159
+ 'processingConfig._id': { $ne: activeConfigObjectId },
160
+ 'processingConfig.state': storage.SyncRuleState.PROCESSING
161
+ }
162
+ ]
163
+ });
164
+ if (result.matchedCount != 1) {
165
+ // This should generally not happen, since the entire process runs in a transaction.
166
+ // The filters just double-check that the state is what we expect, and fails hard otherwise.
167
+ throw new ReplicationAssertionError(`Active replication stream ${active.content.replicationStreamId} changed during restart`);
106
168
  }
107
169
  }
108
- async updateSyncRulesV3(options, storageVersion, storageConfig) {
109
- let rules = undefined;
170
+ async updateSyncRulesInTransaction(options, session) {
171
+ const storageVersion = options.storageVersion ??
172
+ options.config.parsed.config.storageVersion ??
173
+ this.options.defaultStorageVersion ??
174
+ storage.CURRENT_STORAGE_VERSION;
175
+ const storageConfig = getMongoStorageConfig(storageVersion);
176
+ if (storageConfig.incrementalReprocessing) {
177
+ return this.updateSyncConfigV3InTransaction(options, storageVersion, storageConfig, session);
178
+ }
179
+ return this.updateSyncConfigV1InTransaction(options, storageVersion, session);
180
+ }
181
+ /**
182
+ * Update to a V3 sync config.
183
+ *
184
+ * Does support cases where existing replication streams are on V1.
185
+ */
186
+ async updateSyncConfigV3InTransaction(options, storageVersion, storageConfig, session) {
110
187
  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);
188
+ const active = await this.db.sync_rules.findOne({
189
+ state: storage.SyncRuleState.ACTIVE,
190
+ storage_version: storageVersion
191
+ }, { session, sort: { _id: -1 }, limit: 1 });
192
+ if (active != null) {
193
+ const existingConfigDocs = await this.loadSyncConfigDefinitions(versioned, active, session);
194
+ if (!options.forceNewReplicationStream &&
195
+ this.options.supportsMultipleSyncConfigs &&
196
+ isCompatible(existingConfigDocs.map((d) => d.serialized_plan ?? null), options.config, logger)) {
197
+ logger.info(`Using incremental reprocessing`);
198
+ await this.stopExistingProcessingWork(session);
199
+ return await this.appendSyncConfigToStream({
200
+ versioned,
201
+ existing: active,
202
+ existingConfigDocs,
203
+ options,
204
+ storageVersion,
205
+ session
206
+ });
137
207
  }
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);
208
+ }
209
+ await this.stopExistingProcessingWork(session);
210
+ const id_doc = await this.db.op_id_sequence.findOneAndUpdate({
211
+ _id: 'sync_rules'
212
+ }, {
213
+ $inc: {
214
+ op_id: 1n
196
215
  }
216
+ }, {
217
+ upsert: true,
218
+ returnDocument: 'after',
219
+ session
197
220
  });
198
- // Notify only after the transaction has committed, so listeners cannot observe pre-commit state.
199
- await this.db.notifyCheckpoint();
221
+ const id = Number(id_doc.op_id);
222
+ const replicationStreamName = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
223
+ const mapping = options.config.plan == null
224
+ ? // For legacy sync rules and streams, use the parsed config directly to create a mapping
225
+ SingleSyncConfigBucketDefinitionMapping.fromParsedSyncConfig(options.config.parsed)
226
+ : // For new sync streams, always use the serialized version
227
+ SingleSyncConfigBucketDefinitionMapping.constructIncrementalMappingFromSerializedPlans([], options.config.plan.plan, []);
228
+ const syncConfigDoc = {
229
+ _id: new ObjectId(),
230
+ replication_stream_id: id,
231
+ created_at: new Date(),
232
+ storage_version: storageVersion,
233
+ content: options.config.yaml,
234
+ version_label: options.version_label,
235
+ serialized_plan: options.config.plan,
236
+ rule_mapping: mapping.serialize()
237
+ };
238
+ await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
239
+ const doc = {
240
+ _id: id,
241
+ storage_version: storageVersion,
242
+ sync_configs: [
243
+ {
244
+ _id: syncConfigDoc._id,
245
+ state: storage.SyncRuleState.PROCESSING,
246
+ last_checkpoint: null,
247
+ last_checkpoint_lsn: null,
248
+ no_checkpoint_before: null,
249
+ snapshot_done: false
250
+ }
251
+ ],
252
+ snapshot_lsn: undefined,
253
+ state: storage.SyncRuleState.PROCESSING,
254
+ slot_name: replicationStreamName,
255
+ last_checkpoint_ts: null,
256
+ last_fatal_error: null,
257
+ last_fatal_error_ts: null,
258
+ last_keepalive_ts: null
259
+ };
260
+ await this.db.sync_rules.insertOne(doc, { session });
261
+ const rules = new MongoPersistedReplicationStream(this.db, doc, [syncConfigDoc]);
262
+ if (options.lock) {
263
+ // We only lock when creating a new stream - otherwise we'll likely get lock contention
264
+ // from an existing job on the stream, which would fail the entire transaction.
265
+ // The lock is persisted on rules.current_lock
266
+ await rules.lock(session);
267
+ }
200
268
  return rules;
201
269
  }
202
270
  async loadSyncConfigDefinitions(versioned, existing, session) {
@@ -224,25 +292,6 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
224
292
  })
225
293
  .toArray();
226
294
  }
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
295
  logIncrementalDefinitionChanges(changes) {
247
296
  logger.info(`Incremental reprocessing sync config update:\n${formatIncrementalSyncConfigUpdateLog(changes)}`);
248
297
  }
@@ -268,6 +317,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
268
317
  created_at: new Date(),
269
318
  storage_version: storageVersion,
270
319
  content: updateOptions.config.yaml,
320
+ version_label: updateOptions.version_label,
271
321
  serialized_plan: updateOptions.config.plan,
272
322
  rule_mapping: mapping.serialize()
273
323
  };
@@ -303,68 +353,102 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
303
353
  return stream;
304
354
  }
305
355
  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);
356
+ let rules;
357
+ await this.withTransaction(async (session) => {
358
+ rules = await this.updateSyncRulesInTransaction(options, session);
359
+ });
360
+ await this.db.notifyCheckpoint();
361
+ return rules;
362
+ }
363
+ async withTransaction(callback) {
364
+ return this.client.withSession((session) => session.withTransaction(callback));
365
+ }
366
+ /**
367
+ * Update to a V1 sync config.
368
+ *
369
+ * Does support cases where existing replication streams are on V3.
370
+ */
371
+ async updateSyncConfigV1InTransaction(options, storageVersion, session) {
372
+ await this.stopExistingProcessingWork(session);
373
+ const id_doc = await this.db.op_id_sequence.findOneAndUpdate({
374
+ _id: 'sync_rules'
375
+ }, {
376
+ $inc: {
377
+ op_id: 1n
355
378
  }
379
+ }, {
380
+ upsert: true,
381
+ returnDocument: 'after',
382
+ session
356
383
  });
384
+ const id = Number(id_doc.op_id);
385
+ const slot_name = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
386
+ // All V1 replication streams share both the `main` op id sequence and the `bucket_parameters`
387
+ // collection, so every parameter entry this stream writes gets an op id above the current
388
+ // head. Seeding the parameter compaction cursor with that head keeps the stream's first
389
+ // compaction from scanning other streams' history, which would otherwise be repeated for
390
+ // every new deployment. A concurrent replication flush can only advance the head after this
391
+ // read, which makes the seed conservative, never too high.
392
+ const opSequence = await this.db.op_id_sequence.findOne({ _id: 'main' }, { session });
393
+ const doc = {
394
+ _id: id,
395
+ storage_version: storageVersion,
396
+ content: options.config.yaml,
397
+ version_label: options.version_label,
398
+ serialized_plan: options.config.plan,
399
+ last_checkpoint: null,
400
+ last_checkpoint_lsn: null,
401
+ no_checkpoint_before: null,
402
+ keepalive_op: null,
403
+ snapshot_done: false,
404
+ snapshot_lsn: undefined,
405
+ state: storage.SyncRuleState.PROCESSING,
406
+ slot_name: slot_name,
407
+ last_checkpoint_ts: null,
408
+ last_fatal_error: null,
409
+ last_fatal_error_ts: null,
410
+ last_keepalive_ts: null,
411
+ parameter_compaction: { compacted_before: opSequence?.op_id ?? 0n }
412
+ };
413
+ await this.db.sync_rules.insertOne(doc, { session });
414
+ const rules = new MongoPersistedReplicationStream(this.db, doc);
415
+ if (options.lock) {
416
+ // The lock is persisted on rules.current_lock
417
+ await rules.lock(session);
418
+ }
357
419
  return rules;
358
420
  }
421
+ /**
422
+ * Stop top-level and embedded processing work before creating a replacement stream.
423
+ *
424
+ * Use in the same transaction that the replacement stream is created.
425
+ */
426
+ async stopExistingProcessingWork(session) {
427
+ await this.db.sync_rules.updateMany({
428
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] },
429
+ 'sync_configs.state': storage.SyncRuleState.PROCESSING
430
+ }, {
431
+ $set: {
432
+ 'sync_configs.$[config].state': storage.SyncRuleState.STOP
433
+ }
434
+ }, {
435
+ session,
436
+ arrayFilters: [{ 'config.state': storage.SyncRuleState.PROCESSING }]
437
+ });
438
+ await this.db.sync_rules.updateMany({
439
+ state: storage.SyncRuleState.PROCESSING
440
+ }, stopReplicationStreamPipeline(), { session });
441
+ }
359
442
  async getActiveSyncConfig() {
443
+ return this.getActiveSyncConfigInternal();
444
+ }
445
+ async getActiveSyncConfigInternal(session) {
360
446
  const doc = await this.db.sync_rules.findOne({
361
447
  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
- });
448
+ }, { session, sort: { _id: -1 }, limit: 1 });
449
+ return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED], { cacheActiveStorage: session == null }, session);
366
450
  }
367
- async replicationStreamFromDoc(doc, stateFilter) {
451
+ async replicationStreamFromDoc(doc, stateFilter, session) {
368
452
  if (doc == null) {
369
453
  return null;
370
454
  }
@@ -382,7 +466,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
382
466
  const syncConfigDocs = await db.syncConfigDefinitions
383
467
  .find({
384
468
  _id: { $in: matching.map((config) => config._id) }
385
- })
469
+ }, { session })
386
470
  .toArray();
387
471
  if (syncConfigDocs.length == 0) {
388
472
  return null;
@@ -392,10 +476,13 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
392
476
  return new MongoPersistedReplicationStream(this.db, doc);
393
477
  }
394
478
  async getDeployingSyncConfig() {
479
+ return this.getDeployingSyncConfigInternal();
480
+ }
481
+ async getDeployingSyncConfigInternal(session) {
395
482
  const doc = await this.db.sync_rules.findOne({
396
483
  $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]);
484
+ }, { session, sort: { _id: -1 }, limit: 1 });
485
+ return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.PROCESSING], {}, session);
399
486
  }
400
487
  async getReplicatingReplicationStreams() {
401
488
  const docs = await this.db.sync_rules
@@ -417,8 +504,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
417
504
  return this.replicationStreamFromDoc(doc, [storage.SyncRuleState.STOP]);
418
505
  }))).filter((d) => d != null);
419
506
  }
420
- async resolvedSyncConfigFromDoc(doc, stateFilter, options = {}) {
421
- const stream = await this.replicationStreamFromDoc(doc, stateFilter);
507
+ async resolvedSyncConfigFromDoc(doc, stateFilter, options = {}, session) {
508
+ const stream = await this.replicationStreamFromDoc(doc, stateFilter, session);
422
509
  if (stream == null) {
423
510
  return null;
424
511
  }
@@ -445,7 +532,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
445
532
  }
446
533
  async getStorageMetrics() {
447
534
  const ignoreNotExisting = (e) => {
448
- if (lib_mongo.isMongoServerError(e) && e.codeName == 'NamespaceNotFound') {
535
+ if (lib_mongo.isMongoNamespaceNotFoundError(e)) {
449
536
  // Collection doesn't exist - return 0
450
537
  return [{ storageStats: { size: 0 } }];
451
538
  }
@@ -453,8 +540,6 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
453
540
  return Promise.reject(e);
454
541
  }
455
542
  };
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
543
  const aggregateStaticCollection = async (collection) => {
459
544
  // We check whether the collection exists before getting the statistics. This avoids repeated
460
545
  // errors in the MongoDB logs if the collection hasn't been created yet.
@@ -475,7 +560,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
475
560
  .catch(ignoreNotExisting);
476
561
  };
477
562
  const operations_aggregate = await aggregateStaticCollection(this.db.bucket_data);
478
- const v3_operation_aggregates = await Promise.all((await this.db.listBucketDataCollectionsV3()).map((collection) => collection
563
+ const v3OperationCollections = await this.db.listBucketDataCollectionsV3();
564
+ const v3_operation_aggregates = await Promise.all(v3OperationCollections.map((collection) => collection
479
565
  .aggregate([
480
566
  {
481
567
  $collStats: {
@@ -486,7 +572,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
486
572
  .toArray()
487
573
  .catch(ignoreNotExisting)));
488
574
  const parameters_aggregate = await aggregateStaticCollection(this.db.bucket_parameters);
489
- const v3_parameter_aggregates = await Promise.all((await this.db.listAllParameterIndexCollectionsV3()).map((collection) => collection
575
+ const v3ParameterCollections = await this.db.listAllParameterIndexCollectionsV3();
576
+ const v3_parameter_aggregates = await Promise.all(v3ParameterCollections.map((collection) => collection
490
577
  .aggregate([
491
578
  {
492
579
  $collStats: {
@@ -497,7 +584,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
497
584
  .toArray()
498
585
  .catch(ignoreNotExisting)));
499
586
  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
587
+ const v3SourceRecordCollections = await this.db.listAllSourceRecordCollectionsV3();
588
+ const source_record_aggregates = await Promise.all(v3SourceRecordCollections.map((collection) => collection
501
589
  .aggregate([
502
590
  {
503
591
  $collStats: {
@@ -507,13 +595,84 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
507
595
  ])
508
596
  .toArray()
509
597
  .catch(ignoreNotExisting)));
598
+ const v3StorageConfig = getMongoStorageConfig(storage.STORAGE_VERSION_3);
599
+ const v3Db = this.db.versioned(v3StorageConfig);
600
+ const objectStorageDefinitionUsage = await ObjectStorageUsage.readAllDefinitionUsage(v3Db);
601
+ const v3StreamDocs = (await this.db.sync_rules
602
+ .find({ storage_version: { $gte: storage.STORAGE_VERSION_3 } }, { projection: { _id: 1, sync_configs: 1 } })
603
+ .toArray());
604
+ const collectionSizes = new Map();
605
+ const addCollectionSize = (collection, aggregate, prefix, sizes) => {
606
+ const match = collection.collectionName.match(new RegExp(`^${prefix}(\\d+)_(.+)$`));
607
+ if (match == null) {
608
+ return;
609
+ }
610
+ sizes.set(`${prefix}${match[1]}:${match[2]}`, Number(aggregate[0]?.storageStats?.size ?? 0));
611
+ };
612
+ v3OperationCollections.forEach((collection, index) => addCollectionSize(collection, v3_operation_aggregates[index], 'bucket_data_', collectionSizes));
613
+ v3ParameterCollections.forEach((collection, index) => addCollectionSize(collection, v3_parameter_aggregates[index], 'parameter_index_', collectionSizes));
614
+ v3SourceRecordCollections.forEach((collection, index) => addCollectionSize(collection, source_record_aggregates[index], 'source_records_', collectionSizes));
615
+ const syncConfigIds = v3StreamDocs.flatMap((stream) => (stream.sync_configs ?? []).map((config) => config._id));
616
+ const syncConfigDefinitions = syncConfigIds.length == 0
617
+ ? []
618
+ : await v3Db.syncConfigDefinitions
619
+ .find({ _id: { $in: syncConfigIds } }, { projection: { _id: 1, rule_mapping: 1, version_label: 1 } })
620
+ .toArray();
621
+ const syncConfigDefinitionsById = new Map(syncConfigDefinitions.map((definition) => [definition._id.toHexString(), definition]));
622
+ const sourceTablesByStream = new Map();
623
+ await Promise.all(v3StreamDocs.map(async (stream) => {
624
+ const sourceTables = await v3Db
625
+ .sourceTables(stream._id)
626
+ .find({}, { projection: { _id: 1, bucket_data_source_ids: 1, parameter_lookup_source_ids: 1 } })
627
+ .toArray();
628
+ sourceTablesByStream.set(stream._id, sourceTables);
629
+ }));
630
+ const objectStorageSizeByDefinition = new Map();
631
+ for (const usage of objectStorageDefinitionUsage) {
632
+ objectStorageSizeByDefinition.set(`${usage.replication_stream_id}:${usage.definition_id}`, Number(usage.active_bytes));
633
+ }
634
+ const sumCollectionSizes = (prefix, streamId, ids) => [...ids].reduce((total, id) => total + (collectionSizes.get(`${prefix}${streamId}:${id}`) ?? 0), 0);
635
+ const syncConfigMetrics = [];
636
+ for (const stream of v3StreamDocs) {
637
+ const sourceTables = sourceTablesByStream.get(stream._id) ?? [];
638
+ for (const syncConfig of stream.sync_configs ?? []) {
639
+ const definition = syncConfigDefinitionsById.get(syncConfig._id.toHexString());
640
+ if (definition == null) {
641
+ continue;
642
+ }
643
+ const mapping = SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(definition.rule_mapping);
644
+ const bucketDefinitionIds = mapping.allBucketDefinitionIds();
645
+ const parameterIndexIds = mapping.allParameterIndexIds();
646
+ const bucketDefinitionIdSet = new Set(bucketDefinitionIds);
647
+ const parameterIndexIdSet = new Set(parameterIndexIds);
648
+ const replicationSize = sourceTables.reduce((total, sourceTable) => {
649
+ if (!(sourceTable.bucket_data_source_ids ?? []).some((id) => bucketDefinitionIdSet.has(id)) &&
650
+ !(sourceTable.parameter_lookup_source_ids ?? []).some((id) => parameterIndexIdSet.has(id))) {
651
+ return total;
652
+ }
653
+ return total + (collectionSizes.get(`source_records_${stream._id}:${sourceTable._id.toHexString()}`) ?? 0);
654
+ }, 0);
655
+ syncConfigMetrics.push({
656
+ sync_config_id: syncConfig._id.toHexString(),
657
+ sync_config_state: String(syncConfig.state),
658
+ version_label: definition.version_label,
659
+ attributed_bucket_data_bytes: sumCollectionSizes('bucket_data_', stream._id, bucketDefinitionIdSet),
660
+ attributed_parameter_indexes_bytes: sumCollectionSizes('parameter_index_', stream._id, parameterIndexIdSet),
661
+ attributed_source_records_bytes: replicationSize,
662
+ attributed_object_storage_bytes: [...bucketDefinitionIdSet].reduce((total, definitionId) => total + (objectStorageSizeByDefinition.get(`${stream._id}:${definitionId}`) ?? 0), 0)
663
+ });
664
+ }
665
+ }
666
+ const totalObjectStorageSize = objectStorageDefinitionUsage.reduce((total, usage) => total + usage.active_bytes, 0n);
510
667
  return {
511
668
  operations_size_bytes: Number(operations_aggregate[0].storageStats.size) +
512
669
  v3_operation_aggregates.reduce((total, aggregate) => total + Number(aggregate[0].storageStats.size), 0),
513
670
  parameters_size_bytes: Number(parameters_aggregate[0].storageStats.size) +
514
671
  v3_parameter_aggregates.reduce((total, aggregate) => total + Number(aggregate[0].storageStats.size), 0),
515
672
  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)
673
+ source_record_aggregates.reduce((total, aggregate) => total + Number(aggregate[0]?.storageStats?.size ?? 0), 0),
674
+ object_storage_size_bytes: Number(totalObjectStorageSize),
675
+ sync_config_metrics: syncConfigMetrics
517
676
  };
518
677
  }
519
678
  async getPowerSyncInstanceId() {