@powersync/service-module-mongodb-storage 0.7.1 → 0.8.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 (29) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/migrations/db/migrations/1741697235857-bucket-state-index.d.ts +3 -0
  3. package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js +28 -0
  4. package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js.map +1 -0
  5. package/dist/storage/implementation/MongoCompactor.js +3 -1
  6. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  7. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +7 -4
  8. package/dist/storage/implementation/MongoSyncBucketStorage.js +128 -31
  9. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  10. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +15 -2
  11. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +193 -17
  12. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  13. package/dist/storage/implementation/PersistedBatch.d.ts +8 -0
  14. package/dist/storage/implementation/PersistedBatch.js +44 -0
  15. package/dist/storage/implementation/PersistedBatch.js.map +1 -1
  16. package/dist/storage/implementation/db.d.ts +2 -1
  17. package/dist/storage/implementation/db.js +4 -0
  18. package/dist/storage/implementation/db.js.map +1 -1
  19. package/dist/storage/implementation/models.d.ts +17 -0
  20. package/package.json +4 -4
  21. package/src/migrations/db/migrations/1741697235857-bucket-state-index.ts +40 -0
  22. package/src/storage/implementation/MongoCompactor.ts +3 -1
  23. package/src/storage/implementation/MongoSyncBucketStorage.ts +164 -35
  24. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +262 -25
  25. package/src/storage/implementation/PersistedBatch.ts +52 -0
  26. package/src/storage/implementation/db.ts +5 -0
  27. package/src/storage/implementation/models.ts +18 -0
  28. package/test/src/__snapshots__/storage_sync.test.ts.snap +171 -0
  29. package/tsconfig.tsbuildinfo +1 -1
@@ -11,6 +11,7 @@ import { PowerSyncMongo } from './db.js';
11
11
  import {
12
12
  BucketDataDocument,
13
13
  BucketParameterDocument,
14
+ BucketStateDocument,
14
15
  CurrentBucket,
15
16
  CurrentDataDocument,
16
17
  SourceKey
@@ -48,6 +49,7 @@ export class PersistedBatch {
48
49
  bucketData: mongo.AnyBulkWriteOperation<BucketDataDocument>[] = [];
49
50
  bucketParameters: mongo.AnyBulkWriteOperation<BucketParameterDocument>[] = [];
50
51
  currentData: mongo.AnyBulkWriteOperation<CurrentDataDocument>[] = [];
52
+ bucketStates: Map<string, BucketStateUpdate> = new Map();
51
53
 
52
54
  /**
53
55
  * For debug logging only.
@@ -66,6 +68,19 @@ export class PersistedBatch {
66
68
  this.currentSize = writtenSize;
67
69
  }
68
70
 
71
+ private incrementBucket(bucket: string, op_id: InternalOpId) {
72
+ let existingState = this.bucketStates.get(bucket);
73
+ if (existingState) {
74
+ existingState.lastOp = op_id;
75
+ existingState.incrementCount += 1;
76
+ } else {
77
+ this.bucketStates.set(bucket, {
78
+ lastOp: op_id,
79
+ incrementCount: 1
80
+ });
81
+ }
82
+ }
83
+
69
84
  saveBucketData(options: {
70
85
  op_seq: MongoIdSequence;
71
86
  sourceKey: storage.ReplicaId;
@@ -120,6 +135,7 @@ export class PersistedBatch {
120
135
  }
121
136
  }
122
137
  });
138
+ this.incrementBucket(k.bucket, op_id);
123
139
  }
124
140
 
125
141
  for (let bd of remaining_buckets.values()) {
@@ -147,6 +163,7 @@ export class PersistedBatch {
147
163
  }
148
164
  });
149
165
  this.currentSize += 200;
166
+ this.incrementBucket(bd.bucket, op_id);
150
167
  }
151
168
  }
152
169
 
@@ -277,6 +294,14 @@ export class PersistedBatch {
277
294
  });
278
295
  }
279
296
 
297
+ if (this.bucketStates.size > 0) {
298
+ await db.bucket_state.bulkWrite(this.getBucketStateUpdates(), {
299
+ session,
300
+ // Per-bucket operation - order doesn't matter
301
+ ordered: false
302
+ });
303
+ }
304
+
280
305
  const duration = performance.now() - startAt;
281
306
  logger.info(
282
307
  `powersync_${this.group_id} Flushed ${this.bucketData.length} + ${this.bucketParameters.length} + ${
@@ -287,7 +312,34 @@ export class PersistedBatch {
287
312
  this.bucketData = [];
288
313
  this.bucketParameters = [];
289
314
  this.currentData = [];
315
+ this.bucketStates.clear();
290
316
  this.currentSize = 0;
291
317
  this.debugLastOpId = null;
292
318
  }
319
+
320
+ private getBucketStateUpdates(): mongo.AnyBulkWriteOperation<BucketStateDocument>[] {
321
+ return Array.from(this.bucketStates.entries()).map(([bucket, state]) => {
322
+ return {
323
+ updateOne: {
324
+ filter: {
325
+ _id: {
326
+ g: this.group_id,
327
+ b: bucket
328
+ }
329
+ },
330
+ update: {
331
+ $set: {
332
+ last_op: state.lastOp
333
+ }
334
+ },
335
+ upsert: true
336
+ }
337
+ } satisfies mongo.AnyBulkWriteOperation<BucketStateDocument>;
338
+ });
339
+ }
340
+ }
341
+
342
+ interface BucketStateUpdate {
343
+ lastOp: InternalOpId;
344
+ incrementCount: number;
293
345
  }
@@ -6,6 +6,7 @@ import { MongoStorageConfig } from '../../types/types.js';
6
6
  import {
7
7
  BucketDataDocument,
8
8
  BucketParameterDocument,
9
+ BucketStateDocument,
9
10
  CurrentDataDocument,
10
11
  CustomWriteCheckpointDocument,
11
12
  IdSequenceDocument,
@@ -33,6 +34,7 @@ export class PowerSyncMongo {
33
34
  readonly write_checkpoints: mongo.Collection<WriteCheckpointDocument>;
34
35
  readonly instance: mongo.Collection<InstanceDocument>;
35
36
  readonly locks: mongo.Collection<lib_mongo.locks.Lock>;
37
+ readonly bucket_state: mongo.Collection<BucketStateDocument>;
36
38
 
37
39
  readonly client: mongo.MongoClient;
38
40
  readonly db: mongo.Db;
@@ -55,6 +57,7 @@ export class PowerSyncMongo {
55
57
  this.write_checkpoints = db.collection('write_checkpoints');
56
58
  this.instance = db.collection('instance');
57
59
  this.locks = this.db.collection('locks');
60
+ this.bucket_state = this.db.collection('bucket_state');
58
61
  }
59
62
 
60
63
  /**
@@ -70,6 +73,8 @@ export class PowerSyncMongo {
70
73
  await this.write_checkpoints.deleteMany({});
71
74
  await this.instance.deleteOne({});
72
75
  await this.locks.deleteMany({});
76
+ await this.bucket_state.deleteMany({});
77
+ await this.custom_write_checkpoints.deleteMany({});
73
78
  }
74
79
 
75
80
  /**
@@ -75,6 +75,24 @@ export interface SourceTableDocument {
75
75
  snapshot_done: boolean | undefined;
76
76
  }
77
77
 
78
+ /**
79
+ * Record the state of each bucket.
80
+ *
81
+ * Right now, this is just used to track when buckets are updated, for efficient incremental sync.
82
+ * In the future, this could be used to track operation counts, both for diagnostic purposes, and for
83
+ * determining when a compact and/or defragment could be beneficial.
84
+ *
85
+ * Note: There is currently no migration to populate this collection from existing data - it is only
86
+ * populated by new updates.
87
+ */
88
+ export interface BucketStateDocument {
89
+ _id: {
90
+ g: number;
91
+ b: string;
92
+ };
93
+ last_op: bigint;
94
+ }
95
+
78
96
  export interface IdSequenceDocument {
79
97
  _id: string;
80
98
  op_id: bigint;
@@ -357,6 +357,74 @@ exports[`sync - mongodb > sync legacy non-raw data 1`] = `
357
357
  ]
358
358
  `;
359
359
 
360
+ exports[`sync - mongodb > sync updates to data query only 1`] = `
361
+ [
362
+ {
363
+ "checkpoint": {
364
+ "buckets": [
365
+ {
366
+ "bucket": "by_user["user1"]",
367
+ "checksum": 0,
368
+ "count": 0,
369
+ "priority": 3,
370
+ },
371
+ ],
372
+ "last_op_id": "1",
373
+ "write_checkpoint": undefined,
374
+ },
375
+ },
376
+ {
377
+ "checkpoint_complete": {
378
+ "last_op_id": "1",
379
+ },
380
+ },
381
+ ]
382
+ `;
383
+
384
+ exports[`sync - mongodb > sync updates to data query only 2`] = `
385
+ [
386
+ {
387
+ "checkpoint_diff": {
388
+ "last_op_id": "2",
389
+ "removed_buckets": [],
390
+ "updated_buckets": [
391
+ {
392
+ "bucket": "by_user["user1"]",
393
+ "checksum": 1418351250,
394
+ "count": 1,
395
+ "priority": 3,
396
+ },
397
+ ],
398
+ "write_checkpoint": undefined,
399
+ },
400
+ },
401
+ {
402
+ "data": {
403
+ "after": "0",
404
+ "bucket": "by_user["user1"]",
405
+ "data": [
406
+ {
407
+ "checksum": 1418351250n,
408
+ "data": "{"id":"list1","user_id":"user1","name":"User 1"}",
409
+ "object_id": "list1",
410
+ "object_type": "lists",
411
+ "op": "PUT",
412
+ "op_id": "2",
413
+ "subkey": "0ffb7b58-d14d-5efa-be6c-c8eda74ab7a8",
414
+ },
415
+ ],
416
+ "has_more": false,
417
+ "next_after": "2",
418
+ },
419
+ },
420
+ {
421
+ "checkpoint_complete": {
422
+ "last_op_id": "2",
423
+ },
424
+ },
425
+ ]
426
+ `;
427
+
360
428
  exports[`sync - mongodb > sync updates to global data 1`] = `
361
429
  [
362
430
  {
@@ -468,3 +536,106 @@ exports[`sync - mongodb > sync updates to global data 3`] = `
468
536
  },
469
537
  ]
470
538
  `;
539
+
540
+ exports[`sync - mongodb > sync updates to parameter query + data 1`] = `
541
+ [
542
+ {
543
+ "checkpoint": {
544
+ "buckets": [],
545
+ "last_op_id": "0",
546
+ "write_checkpoint": undefined,
547
+ },
548
+ },
549
+ {
550
+ "checkpoint_complete": {
551
+ "last_op_id": "0",
552
+ },
553
+ },
554
+ ]
555
+ `;
556
+
557
+ exports[`sync - mongodb > sync updates to parameter query + data 2`] = `
558
+ [
559
+ {
560
+ "checkpoint_diff": {
561
+ "last_op_id": "2",
562
+ "removed_buckets": [],
563
+ "updated_buckets": [
564
+ {
565
+ "bucket": "by_user["user1"]",
566
+ "checksum": 1418351250,
567
+ "count": 1,
568
+ "priority": 3,
569
+ },
570
+ ],
571
+ "write_checkpoint": undefined,
572
+ },
573
+ },
574
+ {
575
+ "data": {
576
+ "after": "0",
577
+ "bucket": "by_user["user1"]",
578
+ "data": [
579
+ {
580
+ "checksum": 1418351250n,
581
+ "data": "{"id":"list1","user_id":"user1","name":"User 1"}",
582
+ "object_id": "list1",
583
+ "object_type": "lists",
584
+ "op": "PUT",
585
+ "op_id": "1",
586
+ "subkey": "0ffb7b58-d14d-5efa-be6c-c8eda74ab7a8",
587
+ },
588
+ ],
589
+ "has_more": false,
590
+ "next_after": "1",
591
+ },
592
+ },
593
+ {
594
+ "checkpoint_complete": {
595
+ "last_op_id": "2",
596
+ },
597
+ },
598
+ ]
599
+ `;
600
+
601
+ exports[`sync - mongodb > sync updates to parameter query only 1`] = `
602
+ [
603
+ {
604
+ "checkpoint": {
605
+ "buckets": [],
606
+ "last_op_id": "0",
607
+ "write_checkpoint": undefined,
608
+ },
609
+ },
610
+ {
611
+ "checkpoint_complete": {
612
+ "last_op_id": "0",
613
+ },
614
+ },
615
+ ]
616
+ `;
617
+
618
+ exports[`sync - mongodb > sync updates to parameter query only 2`] = `
619
+ [
620
+ {
621
+ "checkpoint_diff": {
622
+ "last_op_id": "1",
623
+ "removed_buckets": [],
624
+ "updated_buckets": [
625
+ {
626
+ "bucket": "by_user["user1"]",
627
+ "checksum": 0,
628
+ "count": 0,
629
+ "priority": 3,
630
+ },
631
+ ],
632
+ "write_checkpoint": undefined,
633
+ },
634
+ },
635
+ {
636
+ "checkpoint_complete": {
637
+ "last_op_id": "1",
638
+ },
639
+ },
640
+ ]
641
+ `;