@powersync/service-module-mongodb-storage 0.13.2 → 0.14.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 (47) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/migrations/db/migrations/1770213298299-storage-version.d.ts +3 -0
  3. package/dist/migrations/db/migrations/1770213298299-storage-version.js +29 -0
  4. package/dist/migrations/db/migrations/1770213298299-storage-version.js.map +1 -0
  5. package/dist/storage/MongoBucketStorage.d.ts +7 -15
  6. package/dist/storage/MongoBucketStorage.js +13 -51
  7. package/dist/storage/MongoBucketStorage.js.map +1 -1
  8. package/dist/storage/implementation/MongoChecksums.d.ts +5 -2
  9. package/dist/storage/implementation/MongoChecksums.js +7 -4
  10. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  11. package/dist/storage/implementation/MongoCompactor.js +42 -17
  12. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  13. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +2 -12
  14. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +24 -24
  15. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  16. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +4 -2
  17. package/dist/storage/implementation/MongoSyncBucketStorage.js +4 -1
  18. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  19. package/dist/storage/implementation/models.d.ts +13 -1
  20. package/dist/storage/implementation/models.js +9 -1
  21. package/dist/storage/implementation/models.js.map +1 -1
  22. package/dist/storage/storage-index.d.ts +0 -1
  23. package/dist/storage/storage-index.js +0 -1
  24. package/dist/storage/storage-index.js.map +1 -1
  25. package/dist/utils/test-utils.d.ts +3 -4
  26. package/dist/utils/test-utils.js +2 -2
  27. package/dist/utils/test-utils.js.map +1 -1
  28. package/package.json +7 -7
  29. package/src/migrations/db/migrations/1770213298299-storage-version.ts +44 -0
  30. package/src/storage/MongoBucketStorage.ts +21 -59
  31. package/src/storage/implementation/MongoChecksums.ts +14 -6
  32. package/src/storage/implementation/MongoCompactor.ts +49 -19
  33. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +26 -32
  34. package/src/storage/implementation/MongoSyncBucketStorage.ts +16 -5
  35. package/src/storage/implementation/models.ts +25 -1
  36. package/src/storage/storage-index.ts +0 -1
  37. package/src/utils/test-utils.ts +3 -4
  38. package/test/src/__snapshots__/storage_sync.test.ts.snap +1116 -21
  39. package/test/src/storage_compacting.test.ts +28 -22
  40. package/test/src/storage_sync.test.ts +27 -14
  41. package/test/src/util.ts +3 -0
  42. package/tsconfig.tsbuildinfo +1 -1
  43. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -10
  44. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -17
  45. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  46. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -20
  47. package/test/src/__snapshots__/storage.test.ts.snap +0 -25
@@ -63,6 +63,7 @@ const DEFAULT_MOVE_BATCH_LIMIT = 2000;
63
63
  const DEFAULT_MOVE_BATCH_QUERY_LIMIT = 10_000;
64
64
  const DEFAULT_MIN_BUCKET_CHANGES = 10;
65
65
  const DEFAULT_MIN_CHANGE_RATIO = 0.1;
66
+ const DIRTY_BUCKET_SCAN_BATCH_SIZE = 2_000;
66
67
 
67
68
  /** This default is primarily for tests. */
68
69
  const DEFAULT_MEMORY_LIMIT_MB = 64;
@@ -538,31 +539,60 @@ export class MongoCompactor {
538
539
  let lastId = { g: this.group_id, b: new mongo.MinKey() as any };
539
540
  const maxId = { g: this.group_id, b: new mongo.MaxKey() as any };
540
541
  while (true) {
541
- const batch = await this.db.bucket_state
542
- .find(
543
- {
544
- _id: { $gt: lastId, $lt: maxId },
545
- 'estimate_since_compact.count': { $gte: options.minBucketChanges }
546
- },
547
- {
548
- projection: {
549
- _id: 1,
550
- estimate_since_compact: 1,
551
- compacted_state: 1
542
+ // To avoid timeouts from too many buckets not meeting the minBucketChanges criteria, we use an aggregation pipeline
543
+ // to scan a fixed batch of buckets at a time, but only return buckets that meet the criteria, rather than limiting
544
+ // on the output number.
545
+ const [result] = await this.db.bucket_state
546
+ .aggregate<{
547
+ buckets: Pick<BucketStateDocument, '_id' | 'estimate_since_compact' | 'compacted_state'>[];
548
+ cursor: Pick<BucketStateDocument, '_id'>[];
549
+ }>(
550
+ [
551
+ {
552
+ $match: {
553
+ _id: { $gt: lastId, $lt: maxId }
554
+ }
552
555
  },
553
- sort: {
554
- _id: 1
556
+ {
557
+ $sort: { _id: 1 }
555
558
  },
556
- limit: 2000,
557
- maxTimeMS: MONGO_OPERATION_TIMEOUT_MS
558
- }
559
+ {
560
+ // Scan a fixed number of docs each query so sparse matches don't block progress.
561
+ $limit: DIRTY_BUCKET_SCAN_BATCH_SIZE
562
+ },
563
+ {
564
+ $facet: {
565
+ // This is the results for the batch
566
+ buckets: [
567
+ {
568
+ $match: {
569
+ 'estimate_since_compact.count': { $gte: options.minBucketChanges }
570
+ }
571
+ },
572
+ {
573
+ $project: {
574
+ _id: 1,
575
+ estimate_since_compact: 1,
576
+ compacted_state: 1
577
+ }
578
+ }
579
+ ],
580
+ // This is used for the next query.
581
+ cursor: [{ $sort: { _id: -1 } }, { $limit: 1 }, { $project: { _id: 1 } }]
582
+ }
583
+ }
584
+ ],
585
+ { maxTimeMS: MONGO_OPERATION_TIMEOUT_MS }
559
586
  )
560
587
  .toArray();
561
- if (batch.length == 0) {
588
+
589
+ const cursor = result?.cursor?.[0];
590
+ if (cursor == null) {
562
591
  break;
563
592
  }
564
- lastId = batch[batch.length - 1]._id;
565
- const mapped = batch.map((b) => {
593
+ lastId = cursor._id;
594
+
595
+ const mapped = (result?.buckets ?? []).map((b) => {
566
596
  const updatedCount = b.estimate_since_compact?.count ?? 0;
567
597
  const totalCount = (b.compacted_state?.count ?? 0) + updatedCount;
568
598
  const updatedBytes = b.estimate_since_compact?.bytes ?? 0;
@@ -1,48 +1,42 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import { storage } from '@powersync/service-core';
3
- import { SqlSyncRules } from '@powersync/service-sync-rules';
4
- import { MongoPersistedSyncRules } from './MongoPersistedSyncRules.js';
5
3
  import { MongoSyncRulesLock } from './MongoSyncRulesLock.js';
6
4
  import { PowerSyncMongo } from './db.js';
7
- import { SyncRuleDocument } from './models.js';
8
-
9
- export class MongoPersistedSyncRulesContent implements storage.PersistedSyncRulesContent {
10
- public readonly slot_name: string;
11
-
12
- public readonly id: number;
13
- public readonly sync_rules_content: string;
14
- public readonly last_checkpoint_lsn: string | null;
15
- public readonly last_fatal_error: string | null;
16
- public readonly last_fatal_error_ts: Date | null;
17
- public readonly last_keepalive_ts: Date | null;
18
- public readonly last_checkpoint_ts: Date | null;
19
- public readonly active: boolean;
5
+ import { getMongoStorageConfig, SyncRuleDocument } from './models.js';
6
+ import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
20
7
 
8
+ export class MongoPersistedSyncRulesContent extends storage.PersistedSyncRulesContent {
21
9
  public current_lock: MongoSyncRulesLock | null = null;
22
10
 
23
11
  constructor(
24
12
  private db: PowerSyncMongo,
25
13
  doc: mongo.WithId<SyncRuleDocument>
26
14
  ) {
27
- this.id = doc._id;
28
- this.sync_rules_content = doc.content;
29
- this.last_checkpoint_lsn = doc.last_checkpoint_lsn;
30
- // Handle legacy values
31
- this.slot_name = doc.slot_name ?? `powersync_${this.id}`;
32
- this.last_fatal_error = doc.last_fatal_error;
33
- this.last_fatal_error_ts = doc.last_fatal_error_ts;
34
- this.last_checkpoint_ts = doc.last_checkpoint_ts;
35
- this.last_keepalive_ts = doc.last_keepalive_ts;
36
- this.active = doc.state == 'ACTIVE';
15
+ super({
16
+ id: doc._id,
17
+ sync_rules_content: doc.content,
18
+ compiled_plan: doc.serialized_plan ?? null,
19
+ last_checkpoint_lsn: doc.last_checkpoint_lsn,
20
+ // Handle legacy values
21
+ slot_name: doc.slot_name ?? `powersync_${doc._id}`,
22
+ last_fatal_error: doc.last_fatal_error,
23
+ last_fatal_error_ts: doc.last_fatal_error_ts,
24
+ last_checkpoint_ts: doc.last_checkpoint_ts,
25
+ last_keepalive_ts: doc.last_keepalive_ts,
26
+ active: doc.state == 'ACTIVE',
27
+ storageVersion: doc.storage_version ?? storage.LEGACY_STORAGE_VERSION
28
+ });
37
29
  }
38
30
 
39
- parsed(options: storage.ParseSyncRulesOptions) {
40
- return new MongoPersistedSyncRules(
41
- this.id,
42
- SqlSyncRules.fromYaml(this.sync_rules_content, options),
43
- this.last_checkpoint_lsn,
44
- this.slot_name
45
- );
31
+ getStorageConfig() {
32
+ const storageConfig = getMongoStorageConfig(this.storageVersion);
33
+ if (storageConfig == null) {
34
+ throw new ServiceError(
35
+ ErrorCode.PSYNC_S1005,
36
+ `Unsupported storage version ${this.storageVersion} for sync rules ${this.id}`
37
+ );
38
+ }
39
+ return storageConfig;
46
40
  }
47
41
 
48
42
  async lock() {
@@ -32,7 +32,14 @@ import * as timers from 'timers/promises';
32
32
  import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '../../utils/util.js';
33
33
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
34
34
  import { PowerSyncMongo } from './db.js';
35
- import { BucketDataDocument, BucketDataKey, BucketStateDocument, SourceKey, SourceTableDocument } from './models.js';
35
+ import {
36
+ BucketDataDocument,
37
+ BucketDataKey,
38
+ BucketStateDocument,
39
+ SourceKey,
40
+ SourceTableDocument,
41
+ StorageConfig
42
+ } from './models.js';
36
43
  import { MongoBucketBatch } from './MongoBucketBatch.js';
37
44
  import { MongoChecksumOptions, MongoChecksums } from './MongoChecksums.js';
38
45
  import { MongoCompactor } from './MongoCompactor.js';
@@ -40,7 +47,8 @@ import { MongoParameterCompactor } from './MongoParameterCompactor.js';
40
47
  import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
41
48
 
42
49
  export interface MongoSyncBucketStorageOptions {
43
- checksumOptions?: MongoChecksumOptions;
50
+ checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig'>;
51
+ storageConfig: StorageConfig;
44
52
  }
45
53
 
46
54
  /**
@@ -69,12 +77,15 @@ export class MongoSyncBucketStorage
69
77
  public readonly group_id: number,
70
78
  private readonly sync_rules: storage.PersistedSyncRulesContent,
71
79
  public readonly slot_name: string,
72
- writeCheckpointMode?: storage.WriteCheckpointMode,
73
- options?: MongoSyncBucketStorageOptions
80
+ writeCheckpointMode: storage.WriteCheckpointMode | undefined,
81
+ options: MongoSyncBucketStorageOptions
74
82
  ) {
75
83
  super();
76
84
  this.db = factory.db;
77
- this.checksums = new MongoChecksums(this.db, this.group_id, options?.checksumOptions);
85
+ this.checksums = new MongoChecksums(this.db, this.group_id, {
86
+ ...options.checksumOptions,
87
+ storageConfig: options?.storageConfig
88
+ });
78
89
  this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
79
90
  db: this.db,
80
91
  mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
@@ -1,4 +1,4 @@
1
- import { InternalOpId, storage } from '@powersync/service-core';
1
+ import { InternalOpId, SerializedSyncPlan, storage } from '@powersync/service-core';
2
2
  import { SqliteJsonValue } from '@powersync/service-sync-rules';
3
3
  import * as bson from 'bson';
4
4
  import { event_types } from '@powersync/service-types';
@@ -199,11 +199,35 @@ export interface SyncRuleDocument {
199
199
  last_fatal_error_ts: Date | null;
200
200
 
201
201
  content: string;
202
+ serialized_plan?: SerializedSyncPlan | null;
202
203
 
203
204
  lock?: {
204
205
  id: string;
205
206
  expires_at: Date;
206
207
  } | null;
208
+
209
+ storage_version?: number;
210
+ }
211
+
212
+ export interface StorageConfig extends storage.StorageVersionConfig {
213
+ /**
214
+ * When true, bucket_data.checksum is guaranteed to be persisted as a Long.
215
+ *
216
+ * When false, it could also have been persisted as an Int32 or Double, in which case it must be converted to
217
+ * a Long before summing.
218
+ */
219
+ longChecksums: boolean;
220
+ }
221
+
222
+ const LONG_CHECKSUMS_STORAGE_VERSION = 2;
223
+
224
+ export function getMongoStorageConfig(storageVersion: number): StorageConfig | undefined {
225
+ const baseConfig = storage.STORAGE_VERSION_CONFIG[storageVersion];
226
+ if (baseConfig == null) {
227
+ return undefined;
228
+ }
229
+
230
+ return { ...baseConfig, longChecksums: storageVersion >= LONG_CHECKSUMS_STORAGE_VERSION };
207
231
  }
208
232
 
209
233
  export interface CheckpointEventDocument {
@@ -2,7 +2,6 @@ export * from './implementation/db.js';
2
2
  export * from './implementation/models.js';
3
3
  export * from './implementation/MongoBucketBatch.js';
4
4
  export * from './implementation/MongoIdSequence.js';
5
- export * from './implementation/MongoPersistedSyncRules.js';
6
5
  export * from './implementation/MongoPersistedSyncRulesContent.js';
7
6
  export * from './implementation/MongoStorageProvider.js';
8
7
  export * from './implementation/MongoSyncBucketStorage.js';
@@ -1,14 +1,13 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { PowerSyncMongo } from '../storage/implementation/db.js';
3
2
  import { TestStorageOptions } from '@powersync/service-core';
3
+ import { MongoBucketStorage, MongoBucketStorageOptions } from '../storage/MongoBucketStorage.js';
4
4
  import { MongoReportStorage } from '../storage/MongoReportStorage.js';
5
- import { MongoBucketStorage } from '../storage/MongoBucketStorage.js';
6
- import { MongoSyncBucketStorageOptions } from '../storage/implementation/MongoSyncBucketStorage.js';
5
+ import { PowerSyncMongo } from '../storage/implementation/db.js';
7
6
 
8
7
  export type MongoTestStorageOptions = {
9
8
  url: string;
10
9
  isCI: boolean;
11
- internalOptions?: MongoSyncBucketStorageOptions;
10
+ internalOptions?: MongoBucketStorageOptions;
12
11
  };
13
12
 
14
13
  export function mongoTestStorageFactoryGenerator(factoryOptions: MongoTestStorageOptions) {