@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,5 +1,11 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { HydratedSyncConfig, SqlEventDescriptor, SqliteRow, SqliteValue } from '@powersync/service-sync-rules';
2
+ import {
3
+ EventDefinitionId,
4
+ HydratedEventDescriptor,
5
+ HydratedSyncConfig,
6
+ SqliteRow,
7
+ SqliteValue
8
+ } from '@powersync/service-sync-rules';
3
9
  import * as bson from 'bson';
4
10
 
5
11
  import {
@@ -8,6 +14,7 @@ import {
8
14
  ErrorCode,
9
15
  errors,
10
16
  Logger,
17
+ ReplicationAbortedError,
11
18
  ReplicationAssertionError,
12
19
  ServiceError
13
20
  } from '@powersync/lib-services-framework';
@@ -27,21 +34,20 @@ import { mongoTableId } from '../../utils/util.js';
27
34
  import { PersistedBatch } from './common/PersistedBatch.js';
28
35
  import { LoadedSourceRecord, SourceRecordStore } from './common/SourceRecordStore.js';
29
36
  import type { VersionedPowerSyncMongo } from './db.js';
37
+ import { SyncRuleDocumentBase } from './models.js';
30
38
  import { MAX_ROW_SIZE } from './MongoBucketBatchShared.js';
31
- import { MongoIdSequence } from './MongoIdSequence.js';
39
+ import { MongoIdSequence, OpIdRangeExhausted } from './MongoIdSequence.js';
40
+ import { MongoOpIdAllocator } from './MongoOpIdAllocator.js';
32
41
  import { MongoParsedSyncConfigSet } from './MongoParsedSyncConfigSet.js';
33
- import { batchCreateCustomWriteCheckpoints } from './MongoWriteCheckpointAPI.js';
42
+ import { MongoSyncRulesLock } from './MongoSyncRulesLock.js';
43
+ import { MongoWriteBatch } from './MongoWriteBatch.js';
34
44
  import { OperationBatch, RecordOperation } from './OperationBatch.js';
35
45
  import { ObjectStorage } from './v3/object-storage/ObjectStorage.js';
36
-
37
- // Currently, we can only have a single flush() at a time, since it locks the op_id sequence.
38
- // While the MongoDB transaction retry mechanism handles this okay, using an in-process Mutex
39
- // makes it more fair and has less overhead.
40
- //
41
- // In the future, we can investigate allowing multiple replication streams operating independently.
42
- const replicationMutex = new utils.Mutex();
46
+ import { createObjectStorageUsageWriterId } from './v3/object-storage/ObjectStorageUsage.js';
43
47
 
44
48
  export interface MongoBucketBatchOptions {
49
+ replicationLock: MongoSyncRulesLock;
50
+ opIdAllocator: MongoOpIdAllocator;
45
51
  db: VersionedPowerSyncMongo;
46
52
  /**
47
53
  * The parsed sync config set for this batch.
@@ -72,6 +78,10 @@ export interface MongoBucketBatchOptions {
72
78
 
73
79
  logger: Logger;
74
80
  tracer?: PerformanceTracer<'storage' | 'evaluate'>;
81
+ /**
82
+ * Aborts in-flight object storage uploads when replication stops.
83
+ */
84
+ signal?: AbortSignal;
75
85
 
76
86
  objectStorage?: ObjectStorage;
77
87
  inlineThresholdBytes?: number;
@@ -104,9 +114,10 @@ export abstract class MongoBucketBatch
104
114
  private readonly storeCurrentData: boolean;
105
115
  public readonly skipExistingRows: boolean;
106
116
  protected readonly mapping: BucketDefinitionMapping;
117
+ protected readonly objectStorageUsageWriterId = createObjectStorageUsageWriterId();
107
118
 
108
119
  private batch: OperationBatch | null = null;
109
- private write_checkpoint_batch: storage.CustomWriteCheckpointOptions[] = [];
120
+ protected write_checkpoint_batch: storage.CustomWriteCheckpointOptions[] = [];
110
121
  private markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
111
122
  private hooks: storage.StorageHooks | undefined;
112
123
  private clearedError = false;
@@ -133,7 +144,13 @@ export abstract class MongoBucketBatch
133
144
  constructor(options: MongoBucketBatchOptions) {
134
145
  super();
135
146
  this.logger = options.logger;
136
- this.options = options;
147
+ this.options = {
148
+ ...options,
149
+ signal:
150
+ options.signal == null
151
+ ? options.replicationLock.signal
152
+ : AbortSignal.any([options.signal, options.replicationLock.signal])
153
+ };
137
154
  this.client = options.db.client;
138
155
  this.db = options.db;
139
156
  this.replicationStreamId = options.replicationStreamId;
@@ -185,6 +202,11 @@ export abstract class MongoBucketBatch
185
202
  no_checkpoint_before_lsn?: string
186
203
  ): Promise<storage.SourceTable[]>;
187
204
 
205
+ protected abstract batchCreateCustomWriteCheckpoints(session: mongo.ClientSession, opId: InternalOpId): Promise<void>;
206
+
207
+ /** Perform any version-specific setup required before writing custom checkpoints. */
208
+ protected async prepareCustomWriteCheckpoints(): Promise<void> {}
209
+
188
210
  async flush(options?: storage.BatchBucketFlushOptions): Promise<storage.FlushedResult | null> {
189
211
  let result: storage.FlushedResult | null = null;
190
212
  // One flush may be split over multiple transactions.
@@ -198,7 +220,34 @@ export abstract class MongoBucketBatch
198
220
  return result;
199
221
  }
200
222
 
201
- private async flushInner(options?: storage.BatchBucketFlushOptions): Promise<storage.FlushedResult | null> {
223
+ /** Publish the checkpoint in the final flush transaction, or use the empty-commit path. */
224
+ protected async flushAndCommit<T>(
225
+ checkpoint: (stream: SyncRuleDocumentBase, lastOp: InternalOpId) => Promise<T>,
226
+ commitWithoutFlush: () => Promise<T>,
227
+ options?: storage.BatchBucketFlushOptions,
228
+ projection?: mongo.Document
229
+ ): Promise<T> {
230
+ if (this.batch == null && this.write_checkpoint_batch.length === 0) {
231
+ return commitWithoutFlush();
232
+ }
233
+ let result!: T;
234
+ while (this.batch != null || this.write_checkpoint_batch.length > 0) {
235
+ await this.flushInner(
236
+ options,
237
+ async (stream, lastOp) => {
238
+ result = await checkpoint(stream, lastOp);
239
+ },
240
+ projection
241
+ );
242
+ }
243
+ return result;
244
+ }
245
+
246
+ private async flushInner(
247
+ options?: storage.BatchBucketFlushOptions,
248
+ checkpoint?: (stream: SyncRuleDocumentBase, lastOp: InternalOpId) => Promise<void>,
249
+ projection?: mongo.Document
250
+ ): Promise<storage.FlushedResult | null> {
202
251
  const batch = this.batch;
203
252
  let last_op: InternalOpId | null = null;
204
253
  let resumeBatch: OperationBatch | null = null;
@@ -207,23 +256,41 @@ export abstract class MongoBucketBatch
207
256
  using _ = this.tracer.span('storage', 'flush');
208
257
 
209
258
  await this.hooks?.beforeBatchFlush?.(this);
259
+ if (this.write_checkpoint_batch.length > 0) {
260
+ // Collection/index creation cannot run inside the replication transaction.
261
+ await this.prepareCustomWriteCheckpoints();
262
+ }
210
263
 
211
- await this.withReplicationTransaction(`Flushing ${batch?.length ?? 0} ops`, async (session, opSeq) => {
212
- clearedError = false;
213
- if (batch != null) {
214
- const result = await this.replicateBatch(session, batch, opSeq, options);
215
- resumeBatch = result.resumeBatch;
216
- clearedError ||= result.clearedError;
217
- }
264
+ await this.withReplicationTransaction(
265
+ `Flushing ${batch?.length ?? 0} ops`,
266
+ async (session, opSeq) => {
267
+ clearedError = false;
268
+ if (batch != null) {
269
+ const result = await this.replicateBatch(session, batch, opSeq, options);
270
+ resumeBatch = result.resumeBatch;
271
+ clearedError ||= result.clearedError;
272
+ }
218
273
 
219
- if (this.write_checkpoint_batch.length > 0) {
220
- this.logger.info(`Writing ${this.write_checkpoint_batch.length} custom write checkpoints`);
221
- await batchCreateCustomWriteCheckpoints(this.db, session, this.write_checkpoint_batch, opSeq.next());
222
- this.write_checkpoint_batch = [];
223
- }
274
+ if (this.write_checkpoint_batch.length > 0) {
275
+ this.logger.info(`Writing ${this.write_checkpoint_batch.length} custom write checkpoints`);
276
+ await this.batchCreateCustomWriteCheckpoints(session, opSeq.next());
277
+ }
224
278
 
225
- last_op = opSeq.last();
226
- });
279
+ last_op = opSeq.last();
280
+ },
281
+ async (stream, lastOp) => {
282
+ if (checkpoint != null && resumeBatch == null) {
283
+ // The checkpoint must also persist the head, including when visibility is blocked.
284
+ await checkpoint(stream, lastOp);
285
+ return true;
286
+ }
287
+ return false;
288
+ },
289
+ projection
290
+ );
291
+
292
+ // Keep checkpoints available if the transaction retries after writing them.
293
+ this.write_checkpoint_batch = [];
227
294
 
228
295
  if (clearedError) {
229
296
  this.clearedError = true;
@@ -620,17 +687,46 @@ export abstract class MongoBucketBatch
620
687
  return result;
621
688
  }
622
689
 
623
- protected async withTransaction(cb: () => Promise<void>) {
624
- using lockSpan = this.tracer.span('storage', 'internal_lock');
625
- await replicationMutex.exclusiveLock(async () => {
626
- lockSpan.end();
627
- await this.session.withTransaction(
690
+ protected async fence(session = this.session, projection?: mongo.Document) {
691
+ // Graceful cancellation belongs to the source connector's page/batch boundary.
692
+ // It must still be able to persist progress for rows already flushed. Lease
693
+ // loss is different: the fence rejects every subsequent writer transaction.
694
+ return MongoSyncRulesLock.fence(
695
+ this.db,
696
+ this.replicationStreamId,
697
+ this.options.replicationLock,
698
+ session,
699
+ projection
700
+ );
701
+ }
702
+
703
+ protected async withTransaction<T>(
704
+ cb: (stream: SyncRuleDocumentBase) => Promise<T>,
705
+ session = this.session
706
+ ): Promise<T> {
707
+ return this.withFencedTransaction(() => this.fence(session), cb, session);
708
+ }
709
+
710
+ /** The first operation must modify the stream document conditional on lease ownership. */
711
+ protected async withFencedTransaction<S, T>(
712
+ acquireFence: () => Promise<S>,
713
+ cb: (stream: S) => Promise<T>,
714
+ session = this.session
715
+ ): Promise<T> {
716
+ return this.withWriterLock(() =>
717
+ session.withTransaction(
628
718
  async () => {
719
+ const stream = await acquireFence();
629
720
  try {
630
- await cb();
721
+ const result = await cb(stream);
722
+ this.options.replicationLock.throwIfAborted();
723
+ return result;
631
724
  } catch (e: unknown) {
725
+ if (e instanceof OpIdRangeExhausted) {
726
+ throw e;
727
+ }
632
728
  if (e instanceof mongo.MongoError && e.hasErrorLabel('TransientTransactionError')) {
633
- // Likely write conflict caused by concurrent write stream replicating
729
+ // Likely write conflict caused by concurrent writes to this replication stream.
634
730
  } else {
635
731
  this.logger.warn('Transaction error', e as Error);
636
732
  }
@@ -640,102 +736,118 @@ export abstract class MongoBucketBatch
640
736
  throw e;
641
737
  }
642
738
  },
643
- { maxCommitTimeMS: 10000 }
644
- );
739
+ { maxCommitTimeMS: 10000, writeConcern: { w: 'majority' } }
740
+ )
741
+ );
742
+ }
743
+
744
+ private async withWriterLock<T>(callback: () => Promise<T>): Promise<T> {
745
+ using lockSpan = this.tracer.span('storage', 'internal_lock');
746
+ return this.options.replicationLock.writerMutex.exclusiveLock(async () => {
747
+ lockSpan.end();
748
+ this.options.replicationLock.throwIfAborted();
749
+ return callback();
645
750
  });
646
751
  }
647
752
 
648
- private async withReplicationTransaction(
649
- description: string,
650
- callback: (session: mongo.ClientSession, opSeq: MongoIdSequence) => Promise<void>
753
+ /** Single-document updates enforce ownership atomically, without a separate transaction. */
754
+ protected async updateStreamMetadata(
755
+ set: mongo.Document,
756
+ filter: mongo.Document = {},
757
+ writeConcern: mongo.WriteConcernSettings = { w: 'majority' }
651
758
  ): Promise<void> {
652
- let flushTry = 0;
653
-
654
- const start = Date.now();
655
- const lastTry = start + 90000;
656
-
657
- const session = this.session;
658
-
659
- await this.withTransaction(async () => {
660
- flushTry += 1;
661
- if (flushTry % 10 == 0) {
662
- this.logger.info(`${description} - try ${flushTry}`);
663
- }
664
- if (flushTry > 20 && Date.now() > lastTry) {
665
- throw new ServiceError(ErrorCode.PSYNC_S1402, 'Max transaction tries exceeded');
666
- }
759
+ await this.withWriterLock(() => this.writeStreamMetadata(set, filter, writeConcern));
760
+ }
667
761
 
668
- const next_op_id_doc = await this.db.op_id_sequence.findOneAndUpdate(
669
- {
670
- _id: 'main'
671
- },
672
- {
673
- $setOnInsert: { op_id: 0n },
674
- $set: {
675
- // Force update to ensure we get a mongo lock
676
- ts: Date.now()
677
- }
678
- },
679
- {
680
- upsert: true,
681
- returnDocument: 'after',
682
- session
683
- }
684
- );
685
- const opSeq = new MongoIdSequence(next_op_id_doc?.op_id ?? 0n);
762
+ /** Caller must already be inside a fenced transaction holding the writer mutex. */
763
+ protected async updateStreamMetadataInTransaction(set: mongo.Document, filter: mongo.Document = {}): Promise<void> {
764
+ await this.writeStreamMetadata(set, filter);
765
+ }
686
766
 
687
- await callback(session, opSeq);
767
+ private async writeStreamMetadata(
768
+ set: mongo.Document,
769
+ filter: mongo.Document,
770
+ writeConcern?: mongo.WriteConcernSettings
771
+ ): Promise<void> {
772
+ const result = await this.db.sync_rules.updateOne(
773
+ { ...filter, ...MongoSyncRulesLock.ownerFilter(this.replicationStreamId, this.options.replicationLock) },
774
+ [{ $set: { ...set, ...MongoSyncRulesLock.heartbeatUpdate() } }],
775
+ { session: this.session, ...(writeConcern == null ? {} : { writeConcern }) }
776
+ );
777
+ if (result.matchedCount === 0) {
778
+ throw new ReplicationAbortedError('Replication writer no longer owns the stream');
779
+ }
780
+ }
688
781
 
689
- await this.db.op_id_sequence.updateOne(
690
- {
691
- _id: 'main'
692
- },
693
- {
694
- $set: {
695
- op_id: opSeq.last()
782
+ private async withReplicationTransaction(
783
+ description: string,
784
+ callback: (session: mongo.ClientSession, opSeq: MongoIdSequence) => Promise<void>,
785
+ finish?: (stream: SyncRuleDocumentBase, lastOp: InternalOpId) => Promise<boolean>,
786
+ projection?: mongo.Document
787
+ ): Promise<void> {
788
+ let flushTry = 0;
789
+ const lastTry = Date.now() + 90000;
790
+ const allocator = this.options.opIdAllocator;
791
+ let lastOp = 0n;
792
+ // Refill outside the publication transaction, before evaluating any rows.
793
+ // Exhaustion remains a fallback for large batches or a newer stream head.
794
+ this.options.replicationLock.throwIfAborted();
795
+ await allocator.ensureCapacity();
796
+ for (;;) {
797
+ try {
798
+ await this.withFencedTransaction(
799
+ () => this.fence(this.session, projection),
800
+ async (stream) => {
801
+ flushTry += 1;
802
+ if (flushTry % 10 == 0) {
803
+ this.logger.info(`${description} - try ${flushTry}`);
804
+ }
805
+ if (flushTry > 20 && Date.now() > lastTry) {
806
+ throw new ServiceError(ErrorCode.PSYNC_S1402, 'Max transaction tries exceeded');
807
+ }
808
+ // The fence already holds this stream document for the transaction.
809
+ // A different writer may have used higher IDs since our last flush.
810
+ const opSeq = allocator.sequence(this.persistedOpHead(stream));
811
+ await callback(this.session, opSeq);
812
+ lastOp = opSeq.last();
813
+
814
+ if (!(await finish?.(stream, lastOp))) {
815
+ const writes = this.db.createWriteBatch(this.session, { ordered: false });
816
+ // Allow subclasses to persist additional flush-time state in the same transaction.
817
+ this.onReplicationTransactionFlush(writes, lastOp);
818
+ await writes.execute();
819
+ }
820
+ // Notifications and cleanup must wait until the transaction commits.
696
821
  }
697
- },
698
- {
699
- session
822
+ );
823
+ allocator.committed(lastOp);
824
+ return;
825
+ } catch (error) {
826
+ if (!(error instanceof OpIdRangeExhausted)) {
827
+ throw error;
700
828
  }
701
- );
702
-
703
- await this.db.sync_rules.updateOne(
704
- {
705
- _id: this.replicationStreamId
706
- },
707
- {
708
- $set: {
709
- last_keepalive_ts: new Date()
710
- }
711
- },
712
- { session }
713
- );
714
-
715
- // Allow subclasses to persist additional flush-time state in the same transaction
716
- // (e.g. v3 advances the stream-level last_persisted_op).
717
- await this.onReplicationTransactionFlush(session, opSeq.last());
718
- // We don't notify checkpoint here - we don't make any checkpoint updates directly
719
- });
829
+ // withTransaction has aborted every write. Reserve outside that transaction,
830
+ // then replay evaluation from the same committed stream head. Existing
831
+ // ranges can be reused on retry because no operation from this attempt committed.
832
+ this.options.replicationLock.throwIfAborted();
833
+ await allocator.reserve();
834
+ }
835
+ }
720
836
  }
721
837
 
722
- /**
723
- * Hook called inside the replication flush transaction, after ops have been persisted.
724
- *
725
- * The base implementation does nothing; v3 storage overrides this to `$max` the stream-level
726
- * `last_persisted_op` durably within the same transaction.
727
- */
728
- protected async onReplicationTransactionFlush(_session: mongo.ClientSession, _lastOp: InternalOpId): Promise<void> {
729
- // No-op by default (v1 behaviour unchanged).
730
- }
838
+ /** Highest durably persisted operation, including operations not yet checkpointed. */
839
+ protected abstract persistedOpHead(stream: SyncRuleDocumentBase): InternalOpId;
840
+
841
+ /** Advance the persisted head in the same transaction as the operations. */
842
+ protected abstract onReplicationTransactionFlush(writes: MongoWriteBatch, lastOp: InternalOpId): void;
731
843
 
732
844
  /**
733
845
  * Called after a replication transaction has successfully committed, with the last persisted op id.
734
846
  *
735
847
  * v1 storage tracks this in memory to fold into the next checkpoint. v3 storage does not need it:
736
848
  * the stream-level `last_persisted_op` is already advanced durably by
737
- * {@link onReplicationTransactionFlush} within the same transaction, and checkpoints read it
738
- * from the document.
849
+ * {@link onReplicationTransactionFlush} or the combined checkpoint update within the same
850
+ * transaction, and empty commits read it from the document.
739
851
  *
740
852
  * Keep calls to this adjacent to {@link withReplicationTransaction} usage - both must observe
741
853
  * every path that persists ops.
@@ -762,11 +874,12 @@ export abstract class MongoBucketBatch
762
874
  async save(record: storage.SaveOptions): Promise<storage.FlushedResult | null> {
763
875
  const { after, before, sourceTable, tag } = record;
764
876
  const storeCurrentData = this.storeCurrentData && sourceTable.storeCurrentData;
765
- // syncEvent is the per-table designation from resolveTables. With v3 storage, multiple
766
- // SourceTables can exist for the same ref, with a row change saved once per table -
767
- // only the designated event carrier may fire events, so each event fires once per row.
877
+ // V3 source tables own disjoint event-definition ids for each physical table. Multiple
878
+ // SourceTables may evaluate different events, but each definition is evaluated through
879
+ // at most one record for this row change.
880
+ // Legacy storage leaves eventDefinitionIds undefined and selects by table ref.
768
881
  if (sourceTable.syncEvent) {
769
- for (const event of this.getTableEvents(sourceTable)) {
882
+ for (const { event, eventId } of this.getTableEvents(sourceTable)) {
770
883
  this.iterateListeners((cb) =>
771
884
  cb.replicationEvent?.({
772
885
  batch: this,
@@ -776,7 +889,8 @@ export abstract class MongoBucketBatch
776
889
  after: after && utils.isCompleteRow(storeCurrentData, after) ? after : undefined,
777
890
  before: before && utils.isCompleteRow(storeCurrentData, before) ? before : undefined
778
891
  },
779
- event
892
+ event,
893
+ event_id: eventId
780
894
  })
781
895
  );
782
896
  }
@@ -812,7 +926,9 @@ export abstract class MongoBucketBatch
812
926
 
813
927
  await this.withTransaction(async () => {
814
928
  for (let table of sourceTables) {
815
- await this.db.commonSourceTables(this.replicationStreamId).deleteOne({ _id: mongoTableId(table.id) });
929
+ await this.db
930
+ .commonSourceTables(this.replicationStreamId)
931
+ .deleteOne({ _id: mongoTableId(table.id) }, { session: this.session });
816
932
  }
817
933
  });
818
934
 
@@ -934,11 +1050,29 @@ export abstract class MongoBucketBatch
934
1050
  }
935
1051
 
936
1052
  /**
937
- * Gets relevant {@link SqlEventDescriptor}s for the given {@link SourceTable}
1053
+ * Gets relevant {@link HydratedEventDescriptor}s for the given {@link SourceTable}
938
1054
  */
939
- protected getTableEvents(table: storage.SourceTable): SqlEventDescriptor[] {
940
- return this.sync_rules.eventDescriptors.filter((evt) =>
941
- [...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table.ref))
942
- );
1055
+ protected getTableEvents(
1056
+ table: storage.SourceTable
1057
+ ): { event: HydratedEventDescriptor; eventId?: EventDefinitionId }[] {
1058
+ // V3 storage assigns event-definition ids to each source table, so membership is authoritative.
1059
+ // Iterate the table's distinct ids and resolve each through the stream's deduped event map, so a
1060
+ // definition reused across configs has one evaluator for that id. The evaluator may still return
1061
+ // multiple payloads from matching queries. Legacy storage leaves this undefined and selects by table ref.
1062
+ if (table.eventDefinitionIds != null) {
1063
+ const eventById = this.options.parsedSyncConfig.eventById;
1064
+ const events: { event: HydratedEventDescriptor; eventId: EventDefinitionId }[] = [];
1065
+ for (const id of table.eventDefinitionIds) {
1066
+ const event = eventById.get(id);
1067
+ if (event != null && event.tableTriggersEvent(table.ref)) {
1068
+ events.push({ event, eventId: id });
1069
+ }
1070
+ }
1071
+ return events;
1072
+ }
1073
+
1074
+ return this.sync_rules.eventDescriptors
1075
+ .filter((event) => event.tableTriggersEvent(table.ref))
1076
+ .map((event) => ({ event }));
943
1077
  }
944
1078
  }