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

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