@powersync/service-module-mongodb-storage 0.0.0-dev-20250214100224 → 0.0.0-dev-20250303114151

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 (41) hide show
  1. package/CHANGELOG.md +45 -3
  2. package/dist/storage/MongoBucketStorage.d.ts +7 -17
  3. package/dist/storage/MongoBucketStorage.js +48 -183
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +2 -2
  6. package/dist/storage/implementation/MongoBucketBatch.js +6 -6
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +0 -6
  9. package/dist/storage/implementation/MongoCompactor.js +1 -2
  10. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  11. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +16 -3
  12. package/dist/storage/implementation/MongoSyncBucketStorage.js +212 -16
  13. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  14. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -2
  15. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  16. package/dist/storage/implementation/OperationBatch.d.ts +1 -0
  17. package/dist/storage/implementation/OperationBatch.js +3 -0
  18. package/dist/storage/implementation/OperationBatch.js.map +1 -1
  19. package/dist/storage/implementation/PersistedBatch.js +7 -6
  20. package/dist/storage/implementation/PersistedBatch.js.map +1 -1
  21. package/dist/storage/implementation/db.js +1 -1
  22. package/dist/storage/implementation/db.js.map +1 -1
  23. package/dist/storage/implementation/models.d.ts +1 -0
  24. package/dist/storage/implementation/util.d.ts +0 -14
  25. package/dist/storage/implementation/util.js +0 -38
  26. package/dist/storage/implementation/util.js.map +1 -1
  27. package/package.json +7 -7
  28. package/src/storage/MongoBucketStorage.ts +51 -216
  29. package/src/storage/implementation/MongoBucketBatch.ts +6 -9
  30. package/src/storage/implementation/MongoCompactor.ts +2 -10
  31. package/src/storage/implementation/MongoSyncBucketStorage.ts +292 -37
  32. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +1 -3
  33. package/src/storage/implementation/OperationBatch.ts +4 -0
  34. package/src/storage/implementation/PersistedBatch.ts +7 -6
  35. package/src/storage/implementation/db.ts +1 -1
  36. package/src/storage/implementation/models.ts +5 -0
  37. package/src/storage/implementation/util.ts +0 -45
  38. package/test/src/__snapshots__/storage_sync.test.ts.snap +138 -0
  39. package/test/src/storage_compacting.test.ts +1 -7
  40. package/test/src/storage_sync.test.ts +1 -1
  41. package/tsconfig.tsbuildinfo +1 -1
@@ -1,20 +1,44 @@
1
1
  import * as lib_mongo from '@powersync/lib-service-mongodb';
2
2
  import { mongo } from '@powersync/lib-service-mongodb';
3
- import { DisposableObserver, logger, ServiceAssertionError } from '@powersync/lib-services-framework';
4
- import { storage, utils } from '@powersync/service-core';
3
+ import {
4
+ BaseObserver,
5
+ ErrorCode,
6
+ logger,
7
+ ServiceAssertionError,
8
+ ServiceError
9
+ } from '@powersync/lib-services-framework';
10
+ import {
11
+ BroadcastIterable,
12
+ CHECKPOINT_INVALIDATE_ALL,
13
+ CheckpointChanges,
14
+ GetCheckpointChangesOptions,
15
+ ReplicationCheckpoint,
16
+ SourceTable,
17
+ storage,
18
+ utils,
19
+ WatchWriteCheckpointOptions
20
+ } from '@powersync/service-core';
5
21
  import { SqliteJsonRow, SqliteJsonValue, SqlSyncRules } from '@powersync/service-sync-rules';
6
22
  import * as bson from 'bson';
23
+ import { wrapWithAbort } from 'ix/asynciterable/operators/withabort.js';
7
24
  import * as timers from 'timers/promises';
8
25
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
9
26
  import { PowerSyncMongo } from './db.js';
10
- import { BucketDataDocument, BucketDataKey, SourceKey } from './models.js';
27
+ import {
28
+ BucketDataDocument,
29
+ BucketDataKey,
30
+ SourceKey,
31
+ SourceTableDocument,
32
+ SyncRuleCheckpointState,
33
+ SyncRuleDocument
34
+ } from './models.js';
11
35
  import { MongoBucketBatch } from './MongoBucketBatch.js';
12
36
  import { MongoCompactor } from './MongoCompactor.js';
13
37
  import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
14
38
  import { idPrefixFilter, mapOpEntry, readSingleBatch } from './util.js';
15
39
 
16
40
  export class MongoSyncBucketStorage
17
- extends DisposableObserver<storage.SyncRulesBucketStorageListener>
41
+ extends BaseObserver<storage.SyncRulesBucketStorageListener>
18
42
  implements storage.SyncRulesBucketStorage
19
43
  {
20
44
  private readonly db: PowerSyncMongo;
@@ -147,17 +171,17 @@ export class MongoSyncBucketStorage
147
171
  let result: storage.ResolveTableResult | null = null;
148
172
  await this.db.client.withSession(async (session) => {
149
173
  const col = this.db.source_tables;
150
- let doc = await col.findOne(
151
- {
152
- group_id: group_id,
153
- connection_id: connection_id,
154
- relation_id: objectId,
155
- schema_name: schema,
156
- table_name: table,
157
- replica_id_columns2: columns
158
- },
159
- { session }
160
- );
174
+ let filter: Partial<SourceTableDocument> = {
175
+ group_id: group_id,
176
+ connection_id: connection_id,
177
+ schema_name: schema,
178
+ table_name: table,
179
+ replica_id_columns2: columns
180
+ };
181
+ if (objectId != null) {
182
+ filter.relation_id = objectId;
183
+ }
184
+ let doc = await col.findOne(filter, { session });
161
185
  if (doc == null) {
162
186
  doc = {
163
187
  _id: new bson.ObjectId(),
@@ -186,31 +210,40 @@ export class MongoSyncBucketStorage
186
210
  sourceTable.syncData = options.sync_rules.tableSyncsData(sourceTable);
187
211
  sourceTable.syncParameters = options.sync_rules.tableSyncsParameters(sourceTable);
188
212
 
213
+ let dropTables: storage.SourceTable[] = [];
214
+ // Detect tables that are either renamed, or have different replica_id_columns
215
+ let truncateFilter = [{ schema_name: schema, table_name: table }] as any[];
216
+ if (objectId != null) {
217
+ // Only detect renames if the source uses relation ids.
218
+ truncateFilter.push({ relation_id: objectId });
219
+ }
189
220
  const truncate = await col
190
221
  .find(
191
222
  {
192
223
  group_id: group_id,
193
224
  connection_id: connection_id,
194
225
  _id: { $ne: doc._id },
195
- $or: [{ relation_id: objectId }, { schema_name: schema, table_name: table }]
226
+ $or: truncateFilter
196
227
  },
197
228
  { session }
198
229
  )
199
230
  .toArray();
231
+ dropTables = truncate.map(
232
+ (doc) =>
233
+ new storage.SourceTable(
234
+ doc._id,
235
+ connection_tag,
236
+ doc.relation_id,
237
+ doc.schema_name,
238
+ doc.table_name,
239
+ doc.replica_id_columns2?.map((c) => ({ name: c.name, typeOid: c.type_oid, type: c.type })) ?? [],
240
+ doc.snapshot_done ?? true
241
+ )
242
+ );
243
+
200
244
  result = {
201
245
  table: sourceTable,
202
- dropTables: truncate.map(
203
- (doc) =>
204
- new storage.SourceTable(
205
- doc._id,
206
- connection_tag,
207
- doc.relation_id ?? 0,
208
- doc.schema_name,
209
- doc.table_name,
210
- doc.replica_id_columns2?.map((c) => ({ name: c.name, typeOid: c.type_oid, type: c.type })) ?? [],
211
- doc.snapshot_done ?? true
212
- )
213
- )
246
+ dropTables: dropTables
214
247
  };
215
248
  });
216
249
  return result!;
@@ -297,13 +330,7 @@ export class MongoSyncBucketStorage
297
330
  // 1. We can calculate the document size accurately without serializing again.
298
331
  // 2. We can delay parsing the results until it's needed.
299
332
  // We manually use bson.deserialize below
300
- raw: true,
301
-
302
- // Since we're using raw: true and parsing ourselves later, we don't need bigint
303
- // support here.
304
- // Disabling due to https://jira.mongodb.org/browse/NODE-6165, and the fact that this
305
- // is one of our most common queries.
306
- useBigInt64: false
333
+ raw: true
307
334
  }
308
335
  ) as unknown as mongo.FindCursor<Buffer>;
309
336
 
@@ -324,7 +351,7 @@ export class MongoSyncBucketStorage
324
351
 
325
352
  // Ordered by _id, meaning buckets are grouped together
326
353
  for (let rawData of data) {
327
- const row = bson.deserialize(rawData, storage.BSON_DESERIALIZE_OPTIONS) as BucketDataDocument;
354
+ const row = bson.deserialize(rawData, storage.BSON_DESERIALIZE_INTERNAL_OPTIONS) as BucketDataDocument;
328
355
  const bucket = row._id.b;
329
356
 
330
357
  if (currentBatch == null || currentBatch.bucket != bucket || batchSize >= sizeLimit) {
@@ -577,7 +604,7 @@ export class MongoSyncBucketStorage
577
604
  await this.db.sync_rules.updateMany(
578
605
  {
579
606
  _id: { $ne: this.group_id },
580
- state: storage.SyncRuleState.ACTIVE
607
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
581
608
  },
582
609
  {
583
610
  $set: {
@@ -608,4 +635,232 @@ export class MongoSyncBucketStorage
608
635
  async compact(options?: storage.CompactOptions) {
609
636
  return new MongoCompactor(this.db, this.group_id, options).compact();
610
637
  }
638
+
639
+ private makeActiveCheckpoint(doc: SyncRuleCheckpointState | null) {
640
+ return {
641
+ checkpoint: utils.timestampToOpId(doc?.last_checkpoint ?? 0n),
642
+ lsn: doc?.last_checkpoint_lsn ?? null
643
+ };
644
+ }
645
+
646
+ /**
647
+ * Instance-wide watch on the latest available checkpoint (op_id + lsn).
648
+ */
649
+ private async *watchActiveCheckpoint(signal: AbortSignal): AsyncIterable<ReplicationCheckpoint> {
650
+ // Use this form instead of (doc: SyncRuleCheckpointState | null = null),
651
+ // otherwise we get weird "doc: never" issues.
652
+ let doc = null as SyncRuleCheckpointState | null;
653
+ let clusterTime = null as mongo.Timestamp | null;
654
+ const syncRulesId = this.group_id;
655
+
656
+ await this.db.client.withSession(async (session) => {
657
+ doc = await this.db.sync_rules.findOne(
658
+ {
659
+ _id: syncRulesId,
660
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
661
+ },
662
+ {
663
+ session,
664
+ sort: { _id: -1 },
665
+ limit: 1,
666
+ projection: {
667
+ _id: 1,
668
+ state: 1,
669
+ last_checkpoint: 1,
670
+ last_checkpoint_lsn: 1
671
+ }
672
+ }
673
+ );
674
+ const time = session.clusterTime?.clusterTime ?? null;
675
+ clusterTime = time;
676
+ });
677
+ if (clusterTime == null) {
678
+ throw new ServiceError(ErrorCode.PSYNC_S2401, 'Could not get clusterTime');
679
+ }
680
+
681
+ if (signal.aborted) {
682
+ return;
683
+ }
684
+
685
+ if (doc == null) {
686
+ // Sync rules not present or not active.
687
+ // Abort the connections - clients will have to retry later.
688
+ // Should this error instead?
689
+ return;
690
+ }
691
+
692
+ yield this.makeActiveCheckpoint(doc);
693
+
694
+ // We only watch changes to the active sync rules.
695
+ // If it changes to inactive, we abort and restart with the new sync rules.
696
+
697
+ const pipeline = this.getChangeStreamPipeline();
698
+
699
+ const stream = this.db.sync_rules.watch(pipeline, {
700
+ // Start at the cluster time where we got the initial doc, to make sure
701
+ // we don't skip any updates.
702
+ // This may result in the first operation being a duplicate, but we filter
703
+ // it out anyway.
704
+ startAtOperationTime: clusterTime
705
+ });
706
+
707
+ signal.addEventListener(
708
+ 'abort',
709
+ () => {
710
+ stream.close();
711
+ },
712
+ { once: true }
713
+ );
714
+
715
+ let lastOp: storage.ReplicationCheckpoint | null = null;
716
+ let lastDoc: SyncRuleCheckpointState | null = doc;
717
+
718
+ for await (const update of stream.stream()) {
719
+ if (signal.aborted) {
720
+ break;
721
+ }
722
+ if (update.operationType != 'insert' && update.operationType != 'update' && update.operationType != 'replace') {
723
+ continue;
724
+ }
725
+
726
+ const doc = await this.getOperationDoc(lastDoc, update as lib_mongo.mongo.ChangeStreamDocument<SyncRuleDocument>);
727
+ if (doc == null) {
728
+ // Irrelevant update
729
+ continue;
730
+ }
731
+ if (doc.state != storage.SyncRuleState.ACTIVE && doc.state != storage.SyncRuleState.ERRORED) {
732
+ // Sync rules have changed - abort and restart.
733
+ // Should this error instead?
734
+ break;
735
+ }
736
+
737
+ lastDoc = doc;
738
+
739
+ const op = this.makeActiveCheckpoint(doc);
740
+ // Check for LSN / checkpoint changes - ignore other metadata changes
741
+ if (lastOp == null || op.lsn != lastOp.lsn || op.checkpoint != lastOp.checkpoint) {
742
+ lastOp = op;
743
+ yield op;
744
+ }
745
+ }
746
+ }
747
+
748
+ // Nothing is done here until a subscriber starts to iterate
749
+ private readonly sharedIter = new BroadcastIterable((signal) => {
750
+ return this.watchActiveCheckpoint(signal);
751
+ });
752
+
753
+ /**
754
+ * User-specific watch on the latest checkpoint and/or write checkpoint.
755
+ */
756
+ async *watchWriteCheckpoint(options: WatchWriteCheckpointOptions): AsyncIterable<storage.StorageCheckpointUpdate> {
757
+ const { user_id, signal } = options;
758
+ let lastCheckpoint: utils.OpId | null = null;
759
+ let lastWriteCheckpoint: bigint | null = null;
760
+
761
+ const iter = wrapWithAbort(this.sharedIter, signal);
762
+ for await (const event of iter) {
763
+ const { checkpoint, lsn } = event;
764
+
765
+ // lsn changes are not important by itself.
766
+ // What is important is:
767
+ // 1. checkpoint (op_id) changes.
768
+ // 2. write checkpoint changes for the specific user
769
+
770
+ const lsnFilters: Record<string, string> = lsn ? { 1: lsn } : {};
771
+
772
+ const currentWriteCheckpoint = await this.lastWriteCheckpoint({
773
+ user_id,
774
+ heads: {
775
+ ...lsnFilters
776
+ }
777
+ });
778
+
779
+ if (currentWriteCheckpoint == lastWriteCheckpoint && checkpoint == lastCheckpoint) {
780
+ // No change - wait for next one
781
+ // In some cases, many LSNs may be produced in a short time.
782
+ // Add a delay to throttle the write checkpoint lookup a bit.
783
+ await timers.setTimeout(20 + 10 * Math.random());
784
+ continue;
785
+ }
786
+
787
+ const updates: CheckpointChanges =
788
+ lastCheckpoint == null
789
+ ? {
790
+ invalidateDataBuckets: true,
791
+ invalidateParameterBuckets: true,
792
+ updatedDataBuckets: [],
793
+ updatedParameterBucketDefinitions: []
794
+ }
795
+ : await this.getCheckpointChanges({
796
+ lastCheckpoint: lastCheckpoint,
797
+ nextCheckpoint: checkpoint
798
+ });
799
+
800
+ lastWriteCheckpoint = currentWriteCheckpoint;
801
+ lastCheckpoint = checkpoint;
802
+
803
+ yield {
804
+ base: event,
805
+ writeCheckpoint: currentWriteCheckpoint,
806
+ update: updates
807
+ };
808
+ }
809
+ }
810
+
811
+ private async getOperationDoc(
812
+ lastDoc: SyncRuleCheckpointState,
813
+ update: lib_mongo.mongo.ChangeStreamDocument<SyncRuleDocument>
814
+ ): Promise<SyncRuleCheckpointState | null> {
815
+ if (update.operationType == 'insert' || update.operationType == 'replace') {
816
+ return update.fullDocument;
817
+ } else if (update.operationType == 'update') {
818
+ const updatedFields = update.updateDescription.updatedFields ?? {};
819
+ if (lastDoc._id != update.documentKey._id) {
820
+ throw new ServiceAssertionError(`Sync rules id mismatch: ${lastDoc._id} != ${update.documentKey._id}`);
821
+ }
822
+
823
+ const mergedDoc: SyncRuleCheckpointState = {
824
+ _id: lastDoc._id,
825
+ last_checkpoint: updatedFields.last_checkpoint ?? lastDoc.last_checkpoint,
826
+ last_checkpoint_lsn: updatedFields.last_checkpoint_lsn ?? lastDoc.last_checkpoint_lsn,
827
+ state: updatedFields.state ?? lastDoc.state
828
+ };
829
+
830
+ return mergedDoc;
831
+ } else {
832
+ // Unknown event type
833
+ return null;
834
+ }
835
+ }
836
+
837
+ private getChangeStreamPipeline() {
838
+ const syncRulesId = this.group_id;
839
+ const pipeline: mongo.Document[] = [
840
+ {
841
+ $match: {
842
+ 'documentKey._id': syncRulesId,
843
+ operationType: { $in: ['insert', 'update', 'replace'] }
844
+ }
845
+ },
846
+ {
847
+ $project: {
848
+ operationType: 1,
849
+ 'documentKey._id': 1,
850
+ 'updateDescription.updatedFields.state': 1,
851
+ 'updateDescription.updatedFields.last_checkpoint': 1,
852
+ 'updateDescription.updatedFields.last_checkpoint_lsn': 1,
853
+ 'fullDocument._id': 1,
854
+ 'fullDocument.state': 1,
855
+ 'fullDocument.last_checkpoint': 1,
856
+ 'fullDocument.last_checkpoint_lsn': 1
857
+ }
858
+ }
859
+ ];
860
+ return pipeline;
861
+ }
862
+
863
+ async getCheckpointChanges(options: GetCheckpointChangesOptions): Promise<CheckpointChanges> {
864
+ return CHECKPOINT_INVALIDATE_ALL;
865
+ }
611
866
  }
@@ -1,7 +1,6 @@
1
1
  import * as framework from '@powersync/lib-services-framework';
2
2
  import { storage } from '@powersync/service-core';
3
3
  import { PowerSyncMongo } from './db.js';
4
- import { safeBulkWrite } from './util.js';
5
4
 
6
5
  export type MongoCheckpointAPIOptions = {
7
6
  db: PowerSyncMongo;
@@ -127,8 +126,7 @@ export async function batchCreateCustomWriteCheckpoints(
127
126
  return;
128
127
  }
129
128
 
130
- await safeBulkWrite(
131
- db.custom_write_checkpoints,
129
+ await db.custom_write_checkpoints.bulkWrite(
132
130
  checkpoints.map((checkpointOptions) => ({
133
131
  updateOne: {
134
132
  filter: { user_id: checkpointOptions.user_id, sync_rules_id: checkpointOptions.sync_rules_id },
@@ -41,6 +41,10 @@ export class OperationBatch {
41
41
  return this.batch.length >= MAX_BATCH_COUNT || this.currentSize > MAX_RECORD_BATCH_SIZE;
42
42
  }
43
43
 
44
+ hasData() {
45
+ return this.length > 0;
46
+ }
47
+
44
48
  /**
45
49
  *
46
50
  * @param sizes Map of source key to estimated size of the current_data document, or undefined if current_data is not persisted.
@@ -15,7 +15,7 @@ import {
15
15
  CurrentDataDocument,
16
16
  SourceKey
17
17
  } from './models.js';
18
- import { replicaIdToSubkey, safeBulkWrite } from './util.js';
18
+ import { replicaIdToSubkey } from './util.js';
19
19
 
20
20
  /**
21
21
  * Maximum size of operations we write in a single transaction.
@@ -245,33 +245,34 @@ export class PersistedBatch {
245
245
  }
246
246
 
247
247
  async flush(db: PowerSyncMongo, session: mongo.ClientSession) {
248
+ const startAt = performance.now();
248
249
  if (this.bucketData.length > 0) {
249
- // calculate total size
250
- await safeBulkWrite(db.bucket_data, this.bucketData, {
250
+ await db.bucket_data.bulkWrite(this.bucketData, {
251
251
  session,
252
252
  // inserts only - order doesn't matter
253
253
  ordered: false
254
254
  });
255
255
  }
256
256
  if (this.bucketParameters.length > 0) {
257
- await safeBulkWrite(db.bucket_parameters, this.bucketParameters, {
257
+ await db.bucket_parameters.bulkWrite(this.bucketParameters, {
258
258
  session,
259
259
  // inserts only - order doesn't matter
260
260
  ordered: false
261
261
  });
262
262
  }
263
263
  if (this.currentData.length > 0) {
264
- await safeBulkWrite(db.current_data, this.currentData, {
264
+ await db.current_data.bulkWrite(this.currentData, {
265
265
  session,
266
266
  // may update and delete data within the same batch - order matters
267
267
  ordered: true
268
268
  });
269
269
  }
270
270
 
271
+ const duration = performance.now() - startAt;
271
272
  logger.info(
272
273
  `powersync_${this.group_id} Flushed ${this.bucketData.length} + ${this.bucketParameters.length} + ${
273
274
  this.currentData.length
274
- } updates, ${Math.round(this.currentSize / 1024)}kb. Last op_id: ${this.debugLastOpId}`
275
+ } updates, ${Math.round(this.currentSize / 1024)}kb in ${duration.toFixed(0)}ms. Last op_id: ${this.debugLastOpId}`
275
276
  );
276
277
 
277
278
  this.bucketData = [];
@@ -41,7 +41,7 @@ export class PowerSyncMongo {
41
41
  this.client = client;
42
42
 
43
43
  const db = client.db(options?.database, {
44
- ...storage.BSON_DESERIALIZE_OPTIONS
44
+ ...storage.BSON_DESERIALIZE_INTERNAL_OPTIONS
45
45
  });
46
46
  this.db = db;
47
47
 
@@ -141,6 +141,11 @@ export interface SyncRuleDocument {
141
141
  content: string;
142
142
  }
143
143
 
144
+ export type SyncRuleCheckpointState = Pick<
145
+ SyncRuleDocument,
146
+ 'last_checkpoint' | 'last_checkpoint_lsn' | '_id' | 'state'
147
+ >;
148
+
144
149
  export interface CustomWriteCheckpointDocument {
145
150
  _id: bson.ObjectId;
146
151
  user_id: string;
@@ -124,48 +124,3 @@ export const connectMongoForTests = (url: string, isCI: boolean) => {
124
124
  });
125
125
  return new PowerSyncMongo(client);
126
126
  };
127
-
128
- /**
129
- * MongoDB bulkWrite internally splits the operations into batches
130
- * so that no batch exceeds 16MB. However, there are cases where
131
- * the batch size is very close to 16MB, where additional metadata
132
- * on the server pushes it over the limit, resulting in this error
133
- * from the server:
134
- *
135
- * > MongoBulkWriteError: BSONObj size: 16814023 (0x1008FC7) is invalid. Size must be between 0 and 16793600(16MB) First element: insert: "bucket_data"
136
- *
137
- * We work around the issue by doing our own batching, limiting the
138
- * batch size to 15MB. This does add additional overhead with
139
- * BSON.calculateObjectSize.
140
- */
141
- export async function safeBulkWrite<T extends mongo.Document>(
142
- collection: mongo.Collection<T>,
143
- operations: mongo.AnyBulkWriteOperation<T>[],
144
- options: mongo.BulkWriteOptions
145
- ) {
146
- // Must be below 16MB.
147
- // We could probably go a little closer, but 15MB is a safe threshold.
148
- const BULK_WRITE_LIMIT = 15 * 1024 * 1024;
149
-
150
- let batch: mongo.AnyBulkWriteOperation<T>[] = [];
151
- let currentSize = 0;
152
- // Estimated overhead per operation, should be smaller in reality.
153
- const keySize = 8;
154
- for (let op of operations) {
155
- const bsonSize =
156
- mongo.BSON.calculateObjectSize(op, {
157
- checkKeys: false,
158
- ignoreUndefined: true
159
- } as any) + keySize;
160
- if (batch.length > 0 && currentSize + bsonSize > BULK_WRITE_LIMIT) {
161
- await collection.bulkWrite(batch, options);
162
- currentSize = 0;
163
- batch = [];
164
- }
165
- batch.push(op);
166
- currentSize += bsonSize;
167
- }
168
- if (batch.length > 0) {
169
- await collection.bulkWrite(batch, options);
170
- }
171
- }