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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +5 -0
  3. package/dist/storage/MongoBucketStorage.js +7 -1
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +2 -1
  6. package/dist/storage/implementation/MongoBucketBatch.js +9 -8
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoStorageProvider.js +4 -2
  9. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  10. package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
  11. package/dist/storage/implementation/MongoWriteBatch.js +149 -0
  12. package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
  13. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +2 -2
  14. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  15. package/dist/storage/implementation/common/PersistedBatch.d.ts +5 -4
  16. package/dist/storage/implementation/common/PersistedBatch.js +6 -4
  17. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  18. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
  19. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
  20. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  21. package/dist/storage/implementation/db.d.ts +5 -0
  22. package/dist/storage/implementation/db.js +13 -0
  23. package/dist/storage/implementation/db.js.map +1 -1
  24. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +13 -6
  25. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  26. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +2 -1
  27. package/dist/storage/implementation/v1/MongoCompactorV1.js +23 -17
  28. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  29. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
  30. package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
  31. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  32. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +2 -1
  33. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +17 -14
  34. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  35. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +6 -1
  36. package/dist/storage/implementation/v3/MongoCompactorV3.js +164 -69
  37. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  38. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +12 -8
  39. package/dist/storage/implementation/v3/PersistedBatchV3.js +25 -32
  40. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  41. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -1
  42. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  43. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -3
  44. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +7 -9
  45. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  46. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +3 -2
  47. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +4 -7
  48. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -1
  49. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +21 -3
  50. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  51. package/dist/types/types.d.ts +5 -0
  52. package/dist/types/types.js +12 -0
  53. package/dist/types/types.js.map +1 -1
  54. package/dist/utils/test-utils.js +1 -0
  55. package/dist/utils/test-utils.js.map +1 -1
  56. package/package.json +6 -6
  57. package/src/storage/MongoBucketStorage.ts +12 -1
  58. package/src/storage/implementation/MongoBucketBatch.ts +13 -10
  59. package/src/storage/implementation/MongoStorageProvider.ts +12 -2
  60. package/src/storage/implementation/MongoWriteBatch.ts +172 -0
  61. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -2
  62. package/src/storage/implementation/common/PersistedBatch.ts +12 -8
  63. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
  64. package/src/storage/implementation/db.ts +14 -0
  65. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +17 -8
  66. package/src/storage/implementation/v1/MongoCompactorV1.ts +29 -26
  67. package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
  68. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +51 -49
  69. package/src/storage/implementation/v3/MongoCompactorV3.ts +276 -97
  70. package/src/storage/implementation/v3/PersistedBatchV3.ts +36 -36
  71. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +8 -1
  72. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +11 -10
  73. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +8 -9
  74. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +23 -3
  75. package/src/types/types.ts +18 -0
  76. package/src/utils/test-utils.ts +1 -0
  77. package/test/src/chunk_compaction_config.test.ts +38 -0
  78. package/test/src/object_storage_usage.test.ts +16 -4
  79. package/test/src/storage_compacting.test.ts +257 -5
  80. package/test/src/storage_s3_reading.test.ts +47 -1
  81. package/tsconfig.tsbuildinfo +1 -1
@@ -8,6 +8,7 @@ import { JSONBig } from '@powersync/service-jsonbig';
8
8
  import { mongoTableId, replicaIdToSubkey } from '../../../utils/util.js';
9
9
  import { currentBucketKey, MAX_ROW_SIZE } from '../MongoBucketBatchShared.js';
10
10
  import { MongoIdSequence } from '../MongoIdSequence.js';
11
+ import { MongoWriteBatch } from '../MongoWriteBatch.js';
11
12
  import type { VersionedPowerSyncMongo } from '../db.js';
12
13
  import { TaggedBucketParameterDocument } from '../models.js';
13
14
  import { ObjectStorage } from '../v3/object-storage/ObjectStorage.js';
@@ -216,13 +217,13 @@ export abstract class PersistedBatch {
216
217
 
217
218
  protected abstract get currentDataCount(): number;
218
219
 
219
- protected abstract flushBucketData(session: mongo.ClientSession): Promise<void>;
220
+ protected abstract queueBucketData(writes: MongoWriteBatch): Promise<void>;
220
221
 
221
- protected abstract flushBucketParameters(session: mongo.ClientSession): Promise<void>;
222
+ protected abstract queueBucketParameters(writes: MongoWriteBatch): void;
222
223
 
223
- protected abstract flushCurrentData(session: mongo.ClientSession): Promise<void>;
224
+ protected abstract queueCurrentData(writes: MongoWriteBatch): void;
224
225
 
225
- protected abstract flushBucketStates(session: mongo.ClientSession): Promise<void>;
226
+ protected abstract queueBucketStates(writes: MongoWriteBatch): void;
226
227
 
227
228
  protected abstract resetCurrentData(): void;
228
229
 
@@ -348,24 +349,27 @@ export abstract class PersistedBatch {
348
349
  async flush(session: mongo.ClientSession, options?: storage.BucketBatchCommitOptions) {
349
350
  const startAt = performance.now();
350
351
  let flushedSomething = false;
352
+ const writes = this.db.createWriteBatch(session, { ordered: false });
351
353
  if (this.bucketDataCount > 0) {
352
354
  flushedSomething = true;
353
- await this.flushBucketData(session);
355
+ await this.queueBucketData(writes);
354
356
  }
355
357
  if (this.bucketParameters.length > 0) {
356
358
  flushedSomething = true;
357
- await this.flushBucketParameters(session);
359
+ this.queueBucketParameters(writes);
358
360
  }
359
361
  if (this.currentDataCount > 0) {
360
362
  flushedSomething = true;
361
- await this.flushCurrentData(session);
363
+ this.queueCurrentData(writes);
362
364
  }
363
365
 
364
366
  if (this.bucketStates.size > 0) {
365
367
  flushedSomething = true;
366
- await this.flushBucketStates(session);
368
+ this.queueBucketStates(writes);
367
369
  }
368
370
 
371
+ await writes.execute();
372
+
369
373
  if (flushedSomething) {
370
374
  const duration = Math.round(performance.now() - startAt);
371
375
  if (options?.oldestUncommittedChange != null) {
@@ -58,6 +58,10 @@ export abstract class BaseVersionedPowerSyncMongo {
58
58
  return this.upstream.notifyCheckpoint();
59
59
  }
60
60
 
61
+ createWriteBatch(session: mongo.ClientSession | undefined, options: { ordered: boolean }) {
62
+ return this.upstream.createWriteBatch(session, options);
63
+ }
64
+
61
65
  protected sourceRecordsCollectionName(replicationStreamId: number, sourceTableId: mongo.ObjectId) {
62
66
  return this.upstream.sourceRecordsCollectionName(replicationStreamId, sourceTableId);
63
67
  }
@@ -17,6 +17,7 @@ import {
17
17
  SyncRuleDocumentBase,
18
18
  WriteCheckpointDocument
19
19
  } from './models.js';
20
+ import { MongoWriteBatch } from './MongoWriteBatch.js';
20
21
  import {
21
22
  BucketDataDocumentV1,
22
23
  BucketParameterDocument,
@@ -54,6 +55,19 @@ export class PowerSyncMongo {
54
55
 
55
56
  readonly client: mongo.MongoClient;
56
57
  readonly db: mongo.Db;
58
+ private get clientBulkWriteSupport(): boolean {
59
+ // The driver exposes topology at runtime, but omits this internal property
60
+ // from its public types. Reuse its handshake results without another command.
61
+ const client = this.client as mongo.MongoClient & { topology?: { description: mongo.TopologyDescription } };
62
+ const servers = [...(client.topology?.description.servers.values() ?? [])];
63
+ // MongoDB 8.0 introduced client bulkWrite at wire version 25. Unknown / mixed-version
64
+ // topologies use collection writes until all known servers support it.
65
+ return servers.length > 0 && servers.every((server) => server.maxWireVersion >= 25);
66
+ }
67
+
68
+ createWriteBatch(session: mongo.ClientSession | undefined, options: { ordered: boolean }): MongoWriteBatch {
69
+ return new MongoWriteBatch(this.client, this.clientBulkWriteSupport, session, options);
70
+ }
57
71
 
58
72
  constructor(client: mongo.MongoClient, options?: PowerSyncMongoOptions) {
59
73
  this.client = client;
@@ -55,8 +55,15 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
55
55
  session: mongo.ClientSession,
56
56
  opId: InternalOpId
57
57
  ): Promise<void> {
58
+ // Only the final checkpoint per user/stream is visible after this transaction.
59
+ const checkpoints = new Map(
60
+ this.write_checkpoint_batch.map((checkpoint) => [
61
+ JSON.stringify([checkpoint.sync_rules_id, checkpoint.user_id]),
62
+ checkpoint
63
+ ])
64
+ );
58
65
  await this.db.custom_write_checkpoints.bulkWrite(
59
- this.write_checkpoint_batch.map((checkpointOptions) => {
66
+ [...checkpoints.values()].map((checkpointOptions) => {
60
67
  const set: Record<string, unknown> = {
61
68
  checkpoint: checkpointOptions.checkpoint,
62
69
  sync_rules_id: checkpointOptions.sync_rules_id,
@@ -77,7 +84,7 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
77
84
  }
78
85
  };
79
86
  }),
80
- { session }
87
+ { session, ordered: false }
81
88
  );
82
89
  }
83
90
 
@@ -462,7 +469,9 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
462
469
  const ids = tables.map((table) => mongoTableId(table.id));
463
470
 
464
471
  await this.withTransaction(async () => {
465
- await this.db.sourceTablesV1(this.replicationStreamId).updateMany(
472
+ const writes = this.db.createWriteBatch(session, { ordered: false });
473
+ writes.updateMany(
474
+ this.db.sourceTablesV1(this.replicationStreamId),
466
475
  { _id: { $in: ids } },
467
476
  {
468
477
  $set: {
@@ -471,12 +480,12 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
471
480
  $unset: {
472
481
  snapshot_status: 1
473
482
  }
474
- },
475
- { session }
483
+ }
476
484
  );
477
485
 
478
486
  if (no_checkpoint_before_lsn != null) {
479
- await this.db.sync_rules.updateOne(
487
+ writes.updateOne(
488
+ this.db.sync_rules,
480
489
  {
481
490
  _id: this.replicationStreamId
482
491
  },
@@ -487,10 +496,10 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
487
496
  $max: {
488
497
  no_checkpoint_before: no_checkpoint_before_lsn
489
498
  }
490
- },
491
- { session: this.session }
499
+ }
492
500
  );
493
501
  }
502
+ await writes.execute();
494
503
  });
495
504
  return tables.map((table) => {
496
505
  const copy = table.clone();
@@ -13,6 +13,7 @@ import { BucketDefinitionId } from '@powersync/service-sync-rules';
13
13
  import { BucketDataDoc } from '../common/BucketDataDoc.js';
14
14
  import { BucketStateDocumentBase, LEGACY_BUCKET_DATA_DEFINITION_ID } from '../models.js';
15
15
  import { MongoCompactOptions, MongoCompactor } from '../MongoCompactor.js';
16
+ import { MongoWriteBatch } from '../MongoWriteBatch.js';
16
17
  import { cacheKey } from '../OperationBatch.js';
17
18
  import { BucketDataDocumentV1, BucketStateDocumentV1 } from './models.js';
18
19
  import type { MongoSyncBucketStorageV1 } from './MongoSyncBucketStorageV1.js';
@@ -296,11 +297,10 @@ export class MongoCompactorV1 extends MongoCompactor {
296
297
  }
297
298
 
298
299
  private async flushBucketStateUpdates() {
299
- if (this.bucketStateUpdates.length > 0) {
300
- this.logger.info(`Updating ${this.bucketStateUpdates.length} bucket states`);
301
- await this.writeBucketStateUpdates();
302
- this.bucketStateUpdates = [];
303
- }
300
+ const writes = this.db.createWriteBatch(undefined, { ordered: false });
301
+ this.writeBucketStateUpdates(writes);
302
+ await writes.execute();
303
+ this.bucketStateUpdates = [];
304
304
  }
305
305
 
306
306
  private async updateChecksumsBatch(buckets: Pick<DirtyBucket, 'bucket' | 'definitionId'>[]) {
@@ -340,10 +340,12 @@ export class MongoCompactorV1 extends MongoCompactor {
340
340
  await this.flushBucketStateUpdates();
341
341
  }
342
342
 
343
- protected async writeBucketStateUpdates(): Promise<void> {
344
- await this.db.bucketStateV1.bulkWrite(
345
- this.bucketStateUpdates as mongo.AnyBulkWriteOperation<BucketStateDocumentV1>[],
346
- { ordered: false }
343
+ protected writeBucketStateUpdates(writes: MongoWriteBatch): void {
344
+ if (this.bucketStateUpdates.length === 0) return;
345
+ this.logger.info(`Updating ${this.bucketStateUpdates.length} bucket states`);
346
+ writes.bulkWriteUnordered(
347
+ this.db.bucketStateV1,
348
+ this.bucketStateUpdates as mongo.AnyBulkWriteOperation<BucketStateDocumentV1>[]
347
349
  );
348
350
  }
349
351
 
@@ -541,17 +543,19 @@ export class MongoCompactorV1 extends MongoCompactor {
541
543
  }
542
544
 
543
545
  private async flush(bucketContext: SingleBucketStoreV1) {
546
+ // Persist data changes before advertising the resulting bucket state.
547
+ const writes = this.db.createWriteBatch(undefined, { ordered: true });
544
548
  if (this.updates.length > 0) {
545
549
  this.logger.info(`Compacting ${this.updates.length} ops`);
546
- await bucketContext.collection.bulkWrite(this.updates, {
547
- // Order is not important. Since checksums are not affected, these operations can happen in any order,
548
- // and it's fine if the operations are partially applied. Each individual operation is atomic.
549
- ordered: false
550
- });
551
- this.updates = [];
550
+ // Order is not important. Since checksums are not affected, these operations can happen in any order,
551
+ // and it's fine if the operations are partially applied. Each individual operation is atomic.
552
+ writes.bulkWriteUnordered(bucketContext.collection, this.updates);
552
553
  }
553
554
 
554
- await this.flushBucketStateUpdates();
555
+ this.writeBucketStateUpdates(writes);
556
+ await writes.execute();
557
+ this.updates = [];
558
+ this.bucketStateUpdates = [];
555
559
  }
556
560
 
557
561
  /**
@@ -622,15 +626,13 @@ export class MongoCompactorV1 extends MongoCompactor {
622
626
  }
623
627
 
624
628
  this.logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${lastOp?.o}`);
625
- await bucketContext.collection.deleteMany(
626
- {
627
- _id: {
628
- $gte: bucketContext.minId,
629
- $lte: bucketContext.docId(lastOp!.o)
630
- }
631
- },
632
- { session }
633
- );
629
+ const writes = this.db.createWriteBatch(session, { ordered: true });
630
+ writes.deleteMany(bucketContext.collection, {
631
+ _id: {
632
+ $gte: bucketContext.minId,
633
+ $lte: bucketContext.docId(lastOp!.o)
634
+ }
635
+ });
634
636
 
635
637
  const op = bucketContext.toPersistedDocument({
636
638
  o: lastOp!.o,
@@ -639,7 +641,8 @@ export class MongoCompactorV1 extends MongoCompactor {
639
641
  data: null,
640
642
  target_op: targetOp
641
643
  });
642
- await bucketContext.collection.insertOne(op, { session });
644
+ writes.insertOne(bucketContext.collection, op);
645
+ await writes.execute();
643
646
 
644
647
  opCountDiff = -numberOfOpsToClear + 1;
645
648
  },
@@ -6,6 +6,7 @@ import * as bson from 'bson';
6
6
  import { BucketDataSource, BucketDefinitionId } from '@powersync/service-sync-rules';
7
7
  import { mongoTableId } from '../../../utils/util.js';
8
8
  import { EMPTY_DATA } from '../MongoBucketBatchShared.js';
9
+ import { MongoWriteBatch } from '../MongoWriteBatch.js';
9
10
  import {
10
11
  BucketStateUpdate,
11
12
  PersistedBatch,
@@ -25,7 +26,9 @@ import {
25
26
  export class PersistedBatchV1 extends PersistedBatch {
26
27
  declare protected readonly db: VersionedPowerSyncMongoV1;
27
28
 
28
- currentData: mongo.AnyBulkWriteOperation<CurrentDataDocument>[] = [];
29
+ // Each upsert supplies the complete source-record state. Only its final
30
+ // state matters within a flush; bucket and parameter history is kept separately.
31
+ currentData = new Map<string, mongo.AnyBulkWriteOperation<CurrentDataDocument>>();
29
32
 
30
33
  protected checkDefinitionId(_definitionId: BucketDefinitionId | null): BucketDefinitionId {
31
34
  // V1 storage doesn't persist the id, and we don't use it.
@@ -94,7 +97,7 @@ export class PersistedBatchV1 extends PersistedBatch {
94
97
  }
95
98
 
96
99
  hardDeleteCurrentData(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId) {
97
- this.currentData.push({
100
+ this.currentData.set(this.currentDataKey(sourceTableId, replicaId), {
98
101
  deleteOne: {
99
102
  filter: { _id: this.currentDataId(sourceTableId, replicaId) }
100
103
  }
@@ -124,7 +127,7 @@ export class PersistedBatchV1 extends PersistedBatch {
124
127
  return lookup.lookup;
125
128
  });
126
129
 
127
- this.currentData.push({
130
+ this.currentData.set(this.currentDataKey(values.sourceTableId, values.replicaId), {
128
131
  updateOne: {
129
132
  filter: { _id: this.currentDataId(values.sourceTableId, values.replicaId) },
130
133
  update: {
@@ -141,57 +144,45 @@ export class PersistedBatchV1 extends PersistedBatch {
141
144
  }
142
145
 
143
146
  protected get currentDataCount() {
144
- return this.currentData.length;
147
+ return this.currentData.size;
145
148
  }
146
149
 
147
- protected async flushBucketData(session: mongo.ClientSession) {
148
- await this.db.bucketDataV1.bulkWrite(
150
+ protected async queueBucketData(writes: MongoWriteBatch) {
151
+ writes.bulkWriteUnordered(
152
+ this.db.bucketDataV1,
149
153
  this.bucketData.map((document) => ({
150
154
  insertOne: {
151
155
  document: serializeBucketDataV1(document)
152
156
  }
153
- })),
154
- {
155
- session,
156
- ordered: false
157
- }
157
+ }))
158
158
  );
159
159
  }
160
160
 
161
- protected async flushBucketParameters(session: mongo.ClientSession) {
162
- await this.db.parameterIndexV1.bulkWrite(
161
+ protected queueBucketParameters(writes: MongoWriteBatch): void {
162
+ writes.bulkWriteUnordered(
163
+ this.db.parameterIndexV1,
163
164
  this.bucketParameters.map((document) => ({
164
165
  insertOne: {
165
166
  document: taggedBucketParameterDocumentToV1(document)
166
167
  }
167
- })),
168
- {
169
- session,
170
- ordered: false
171
- }
168
+ }))
172
169
  );
173
170
  }
174
171
 
175
- protected async flushCurrentData(session: mongo.ClientSession) {
176
- if (this.currentData.length == 0) {
172
+ protected queueCurrentData(writes: MongoWriteBatch): void {
173
+ if (this.currentData.size == 0) {
177
174
  return;
178
175
  }
179
176
 
180
- await this.db.sourceRecordsV1.bulkWrite(this.currentData, {
181
- session,
182
- ordered: true
183
- });
177
+ writes.bulkWriteUnordered(this.db.sourceRecordsV1, [...this.currentData.values()]);
184
178
  }
185
179
 
186
- protected async flushBucketStates(session: mongo.ClientSession) {
187
- await this.db.bucketStateV1.bulkWrite(this.getBucketStateUpdates(), {
188
- session,
189
- ordered: false
190
- });
180
+ protected queueBucketStates(writes: MongoWriteBatch): void {
181
+ writes.bulkWriteUnordered(this.db.bucketStateV1, this.getBucketStateUpdates());
191
182
  }
192
183
 
193
184
  protected resetCurrentData() {
194
- this.currentData = [];
185
+ this.currentData.clear();
195
186
  }
196
187
 
197
188
  private getBucketStateUpdates(): mongo.AnyBulkWriteOperation<BucketStateDocumentV1>[] {
@@ -219,6 +210,10 @@ export class PersistedBatchV1 extends PersistedBatch {
219
210
  });
220
211
  }
221
212
 
213
+ private currentDataKey(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId): string {
214
+ return Buffer.from(bson.serialize({ t: sourceTableId, k: replicaId })).toString('base64');
215
+ }
216
+
222
217
  private currentDataId(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId): SourceKey {
223
218
  return {
224
219
  g: this.group_id,
@@ -7,6 +7,7 @@ import { mongoTableId } from '../../../utils/util.js';
7
7
  import { canCheckpointState } from '../CheckpointState.js';
8
8
  import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
9
9
  import { MongoParsedSyncConfigSet } from '../MongoParsedSyncConfigSet.js';
10
+ import { MongoWriteBatch } from '../MongoWriteBatch.js';
10
11
  import { stopReplicationStreamPipeline } from '../SyncRuleStateUpdate.js';
11
12
  import { PersistedBatch } from '../common/PersistedBatch.js';
12
13
  import { SourceRecordStore } from '../common/SourceRecordStore.js';
@@ -60,14 +61,12 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
60
61
  return this.store;
61
62
  }
62
63
 
63
- protected override async onReplicationTransactionFlush(
64
- session: lib_mongo.mongo.ClientSession,
65
- lastOp: bigint
66
- ): Promise<void> {
64
+ protected override onReplicationTransactionFlush(writes: MongoWriteBatch, lastOp: bigint): void {
67
65
  // Durably advance the stream-level head of persisted ops within the flush transaction.
68
66
  // This ensures a checkpoint created later (even by an empty commit, or by a freshly-appended
69
67
  // config that replicates nothing) covers all ops persisted before a potential crash.
70
- await this.db.sync_rules.updateOne(
68
+ writes.updateOne(
69
+ this.db.sync_rules,
71
70
  {
72
71
  _id: this.replicationStreamId
73
72
  },
@@ -75,8 +74,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
75
74
  $max: {
76
75
  last_persisted_op: lastOp
77
76
  }
78
- },
79
- { session }
77
+ }
80
78
  );
81
79
  }
82
80
 
@@ -652,7 +650,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
652
650
  const syncConfigIds = this.relevantSyncConfigIdsForTables(tables);
653
651
 
654
652
  await this.withTransaction(async () => {
655
- await this.db.sourceTables(this.replicationStreamId).updateMany(
653
+ const writes = this.db.createWriteBatch(session, { ordered: false });
654
+ writes.updateMany(
655
+ this.db.sourceTables(this.replicationStreamId),
656
656
  { _id: { $in: ids } },
657
657
  {
658
658
  $set: {
@@ -661,12 +661,12 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
661
661
  $unset: {
662
662
  snapshot_status: 1
663
663
  }
664
- },
665
- { session }
664
+ }
666
665
  );
667
666
 
668
667
  if (no_checkpoint_before_lsn != null && syncConfigIds.length > 0) {
669
- await this.db.sync_rules.updateOne(
668
+ writes.updateOne(
669
+ this.db.sync_rules,
670
670
  {
671
671
  _id: this.replicationStreamId,
672
672
  'sync_configs._id': { $in: syncConfigIds }
@@ -680,12 +680,12 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
680
680
  }
681
681
  },
682
682
  {
683
- session: this.session,
684
683
  // Only set for sync configs that use this table
685
684
  arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
686
685
  }
687
686
  );
688
687
  }
688
+ await writes.execute();
689
689
  });
690
690
  return tables.map((table) => {
691
691
  const copy = table.clone();
@@ -706,9 +706,11 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
706
706
  this.validateCustomCheckpointEventId(checkpoint.event_id)
707
707
  );
708
708
 
709
+ const writes = this.db.createWriteBatch(session, { ordered: false });
709
710
  for (const [eventId, checkpoints] of checkpointsByEvent) {
710
- await this.batchCreateEventCustomWriteCheckpoints(session, opId, eventId, checkpoints);
711
+ this.batchCreateEventCustomWriteCheckpoints(writes, opId, eventId, checkpoints);
711
712
  }
713
+ await writes.execute();
712
714
  }
713
715
 
714
716
  protected override async prepareCustomWriteCheckpoints(): Promise<void> {
@@ -741,48 +743,48 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
741
743
  return eventId;
742
744
  }
743
745
 
744
- private async batchCreateEventCustomWriteCheckpoints(
745
- session: lib_mongo.mongo.ClientSession,
746
+ private batchCreateEventCustomWriteCheckpoints(
747
+ writes: MongoWriteBatch,
746
748
  opId: InternalOpId,
747
749
  eventId: EventDefinitionId,
748
750
  checkpoints: storage.CustomWriteCheckpointOptions[]
749
- ): Promise<void> {
750
- await this.db
751
- .customCheckpointRequests({
751
+ ): void {
752
+ // A repeated user within an event replaces the complete checkpoint state.
753
+ const uniqueCheckpoints = new Map(checkpoints.map((checkpoint) => [checkpoint.user_id, checkpoint]));
754
+ writes.bulkWriteUnordered(
755
+ this.db.customCheckpointRequests({
752
756
  eventId,
753
757
  replicationStreamId: this.replicationStreamId
754
- })
755
- .bulkWrite(
756
- checkpoints.map((checkpointOptions) => {
757
- const set: Partial<CustomCheckpointRequestDocumentV3> = {
758
- user_id: checkpointOptions.user_id,
759
- checkpoint: checkpointOptions.checkpoint,
760
- op_id: opId
761
- };
762
- if (checkpointOptions.checkpoint_requested_at != null) {
763
- set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
764
- }
758
+ }),
759
+ [...uniqueCheckpoints.values()].map((checkpointOptions) => {
760
+ const set: Partial<CustomCheckpointRequestDocumentV3> = {
761
+ user_id: checkpointOptions.user_id,
762
+ checkpoint: checkpointOptions.checkpoint,
763
+ op_id: opId
764
+ };
765
+ if (checkpointOptions.checkpoint_requested_at != null) {
766
+ set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
767
+ }
765
768
 
766
- return {
767
- updateOne: {
768
- filter: {
769
- user_id: checkpointOptions.user_id
770
- },
771
- update: {
772
- $set: set,
773
- ...(checkpointOptions.checkpoint_requested_at == null
774
- ? {
775
- $unset: {
776
- checkpoint_requested_at: 1
777
- }
769
+ return {
770
+ updateOne: {
771
+ filter: {
772
+ user_id: checkpointOptions.user_id
773
+ },
774
+ update: {
775
+ $set: set,
776
+ ...(checkpointOptions.checkpoint_requested_at == null
777
+ ? {
778
+ $unset: {
779
+ checkpoint_requested_at: 1
778
780
  }
779
- : {})
780
- },
781
- upsert: true
782
- }
783
- };
784
- }),
785
- { session }
786
- );
781
+ }
782
+ : {})
783
+ },
784
+ upsert: true
785
+ }
786
+ };
787
+ })
788
+ );
787
789
  }
788
790
  }