@powersync/service-module-mongodb-storage 0.0.0-dev-20260203155513 → 0.0.0-dev-20260223080959

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 (66) hide show
  1. package/CHANGELOG.md +56 -10
  2. package/dist/migrations/db/migrations/1770213298299-storage-version.js +29 -0
  3. package/dist/migrations/db/migrations/1770213298299-storage-version.js.map +1 -0
  4. package/dist/storage/MongoBucketStorage.d.ts +7 -15
  5. package/dist/storage/MongoBucketStorage.js +12 -51
  6. package/dist/storage/MongoBucketStorage.js.map +1 -1
  7. package/dist/storage/MongoReportStorage.d.ts +1 -11
  8. package/dist/storage/MongoReportStorage.js +1 -321
  9. package/dist/storage/MongoReportStorage.js.map +1 -1
  10. package/dist/storage/implementation/MongoChecksums.d.ts +5 -2
  11. package/dist/storage/implementation/MongoChecksums.js +7 -4
  12. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  13. package/dist/storage/implementation/MongoCompactor.d.ts +16 -1
  14. package/dist/storage/implementation/MongoCompactor.js +80 -23
  15. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  16. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +2 -12
  17. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +23 -24
  18. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  19. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +5 -2
  20. package/dist/storage/implementation/MongoSyncBucketStorage.js +42 -40
  21. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  22. package/dist/storage/implementation/db.d.ts +0 -10
  23. package/dist/storage/implementation/db.js +0 -30
  24. package/dist/storage/implementation/db.js.map +1 -1
  25. package/dist/storage/implementation/models.d.ts +11 -0
  26. package/dist/storage/implementation/models.js +9 -1
  27. package/dist/storage/implementation/models.js.map +1 -1
  28. package/dist/storage/storage-index.d.ts +0 -1
  29. package/dist/storage/storage-index.js +0 -1
  30. package/dist/storage/storage-index.js.map +1 -1
  31. package/dist/utils/test-utils.d.ts +3 -4
  32. package/dist/utils/test-utils.js +2 -2
  33. package/dist/utils/test-utils.js.map +1 -1
  34. package/dist/utils/util.d.ts +0 -7
  35. package/dist/utils/util.js +3 -27
  36. package/dist/utils/util.js.map +1 -1
  37. package/package.json +7 -7
  38. package/src/migrations/db/migrations/1770213298299-storage-version.ts +44 -0
  39. package/src/storage/MongoBucketStorage.ts +20 -59
  40. package/src/storage/MongoReportStorage.ts +4 -369
  41. package/src/storage/implementation/MongoChecksums.ts +14 -6
  42. package/src/storage/implementation/MongoCompactor.ts +94 -25
  43. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +25 -32
  44. package/src/storage/implementation/MongoSyncBucketStorage.ts +60 -44
  45. package/src/storage/implementation/db.ts +0 -32
  46. package/src/storage/implementation/models.ts +23 -0
  47. package/src/storage/storage-index.ts +0 -1
  48. package/src/utils/test-utils.ts +3 -4
  49. package/src/utils/util.ts +3 -36
  50. package/test/src/__snapshots__/storage_sync.test.ts.snap +1116 -21
  51. package/test/src/compression.test.ts +17 -0
  52. package/test/src/connection-report-storage.test.ts +6 -2
  53. package/test/src/storage_compacting.test.ts +29 -22
  54. package/test/src/storage_sync.test.ts +27 -14
  55. package/test/src/util.ts +3 -0
  56. package/test/tsconfig.json +3 -7
  57. package/tsconfig.tsbuildinfo +1 -1
  58. package/dist/migrations/db/migrations/1770037239303-sync-reporting.js +0 -44
  59. package/dist/migrations/db/migrations/1770037239303-sync-reporting.js.map +0 -1
  60. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -10
  61. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -17
  62. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  63. package/src/migrations/db/migrations/1770037239303-sync-reporting.ts +0 -74
  64. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -20
  65. package/test/src/__snapshots__/storage.test.ts.snap +0 -25
  66. /package/dist/migrations/db/migrations/{1770037239303-sync-reporting.d.ts → 1770213298299-storage-version.d.ts} +0 -0
@@ -1,48 +1,41 @@
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
+ last_checkpoint_lsn: doc.last_checkpoint_lsn,
19
+ // Handle legacy values
20
+ slot_name: doc.slot_name ?? `powersync_${doc._id}`,
21
+ last_fatal_error: doc.last_fatal_error,
22
+ last_fatal_error_ts: doc.last_fatal_error_ts,
23
+ last_checkpoint_ts: doc.last_checkpoint_ts,
24
+ last_keepalive_ts: doc.last_keepalive_ts,
25
+ active: doc.state == 'ACTIVE',
26
+ storageVersion: doc.storage_version ?? storage.LEGACY_STORAGE_VERSION
27
+ });
37
28
  }
38
29
 
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
- );
30
+ getStorageConfig() {
31
+ const storageConfig = getMongoStorageConfig(this.storageVersion);
32
+ if (storageConfig == null) {
33
+ throw new ServiceError(
34
+ ErrorCode.PSYNC_S1005,
35
+ `Unsupported storage version ${this.storageVersion} for sync rules ${this.id}`
36
+ );
37
+ }
38
+ return storageConfig;
46
39
  }
47
40
 
48
41
  async lock() {
@@ -15,6 +15,7 @@ import {
15
15
  InternalOpId,
16
16
  internalToExternalOpId,
17
17
  maxLsn,
18
+ mergeAsyncIterables,
18
19
  PopulateChecksumCacheOptions,
19
20
  PopulateChecksumCacheResults,
20
21
  ProtocolOpId,
@@ -31,7 +32,14 @@ import * as timers from 'timers/promises';
31
32
  import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '../../utils/util.js';
32
33
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
33
34
  import { PowerSyncMongo } from './db.js';
34
- 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';
35
43
  import { MongoBucketBatch } from './MongoBucketBatch.js';
36
44
  import { MongoChecksumOptions, MongoChecksums } from './MongoChecksums.js';
37
45
  import { MongoCompactor } from './MongoCompactor.js';
@@ -39,7 +47,8 @@ import { MongoParameterCompactor } from './MongoParameterCompactor.js';
39
47
  import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
40
48
 
41
49
  export interface MongoSyncBucketStorageOptions {
42
- checksumOptions?: MongoChecksumOptions;
50
+ checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig'>;
51
+ storageConfig: StorageConfig;
43
52
  }
44
53
 
45
54
  /**
@@ -68,12 +77,15 @@ export class MongoSyncBucketStorage
68
77
  public readonly group_id: number,
69
78
  private readonly sync_rules: storage.PersistedSyncRulesContent,
70
79
  public readonly slot_name: string,
71
- writeCheckpointMode?: storage.WriteCheckpointMode,
72
- options?: MongoSyncBucketStorageOptions
80
+ writeCheckpointMode: storage.WriteCheckpointMode | undefined,
81
+ options: MongoSyncBucketStorageOptions
73
82
  ) {
74
83
  super();
75
84
  this.db = factory.db;
76
- 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
+ });
77
89
  this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
78
90
  db: this.db,
79
91
  mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
@@ -694,53 +706,39 @@ export class MongoSyncBucketStorage
694
706
  * Instance-wide watch on the latest available checkpoint (op_id + lsn).
695
707
  */
696
708
  private async *watchActiveCheckpoint(signal: AbortSignal): AsyncIterable<ReplicationCheckpoint> {
697
- const stream = this.checkpointChangesStream(signal);
698
-
699
709
  if (signal.aborted) {
700
710
  return;
701
711
  }
702
712
 
713
+ // If the stream is idle, we wait a max of a minute (CHECKPOINT_TIMEOUT_MS) before we get another checkpoint,
714
+ // to avoid stale checkpoint snapshots. This is what checkpointTimeoutStream() is for.
715
+ // Essentially, even if there are no actual checkpoint changes, we want a new snapshotTime every minute or so,
716
+ // to ensure that any new clients connecting will get a valid snapshotTime.
717
+ const stream = mergeAsyncIterables(
718
+ [this.checkpointChangesStream(signal), this.checkpointTimeoutStream(signal)],
719
+ signal
720
+ );
721
+
703
722
  // We only watch changes to the active sync rules.
704
723
  // If it changes to inactive, we abort and restart with the new sync rules.
705
- try {
706
- while (true) {
707
- // If the stream is idle, we wait a max of a minute (CHECKPOINT_TIMEOUT_MS)
708
- // before we get another checkpoint, to avoid stale checkpoint snapshots.
709
- const timeout = timers
710
- .setTimeout(CHECKPOINT_TIMEOUT_MS, { done: false }, { signal })
711
- .catch(() => ({ done: true }));
712
- try {
713
- const result = await Promise.race([stream.next(), timeout]);
714
- if (result.done) {
715
- break;
716
- }
717
- } catch (e) {
718
- if (e.name == 'AbortError') {
719
- break;
720
- }
721
- throw e;
722
- }
723
-
724
- if (signal.aborted) {
725
- // Would likely have been caught by the signal on the timeout or the upstream stream, but we check here anyway
726
- break;
727
- }
728
-
729
- const op = await this.getCheckpointInternal();
730
- if (op == null) {
731
- // Sync rules have changed - abort and restart.
732
- // We do a soft close of the stream here - no error
733
- break;
734
- }
724
+ for await (const _ of stream) {
725
+ if (signal.aborted) {
726
+ // Would likely have been caught by the signal on the timeout or the upstream stream, but we check here anyway
727
+ break;
728
+ }
735
729
 
736
- // Previously, we only yielded when the checkpoint or lsn changed.
737
- // However, we always want to use the latest snapshotTime, so we skip that filtering here.
738
- // That filtering could be added in the per-user streams if needed, but in general the capped collection
739
- // should already only contain useful changes in most cases.
740
- yield op;
730
+ const op = await this.getCheckpointInternal();
731
+ if (op == null) {
732
+ // Sync rules have changed - abort and restart.
733
+ // We do a soft close of the stream here - no error
734
+ break;
741
735
  }
742
- } finally {
743
- await stream.return(null);
736
+
737
+ // Previously, we only yielded when the checkpoint or lsn changed.
738
+ // However, we always want to use the latest snapshotTime, so we skip that filtering here.
739
+ // That filtering could be added in the per-user streams if needed, but in general the capped collection
740
+ // should already only contain useful changes in most cases.
741
+ yield op;
744
742
  }
745
743
  }
746
744
 
@@ -900,6 +898,24 @@ export class MongoSyncBucketStorage
900
898
  }
901
899
  }
902
900
 
901
+ private async *checkpointTimeoutStream(signal: AbortSignal): AsyncGenerator<void> {
902
+ while (!signal.aborted) {
903
+ try {
904
+ await timers.setTimeout(CHECKPOINT_TIMEOUT_MS, undefined, { signal });
905
+ } catch (e) {
906
+ if (e.name == 'AbortError') {
907
+ // This is how we typically abort this stream, when all listeners are done
908
+ return;
909
+ }
910
+ throw e;
911
+ }
912
+
913
+ if (!signal.aborted) {
914
+ yield;
915
+ }
916
+ }
917
+ }
918
+
903
919
  private async getDataBucketChanges(
904
920
  options: GetCheckpointChangesOptions
905
921
  ): Promise<Pick<CheckpointChanges, 'updatedDataBuckets' | 'invalidateDataBuckets'>> {
@@ -39,8 +39,6 @@ export class PowerSyncMongo {
39
39
  readonly bucket_state: mongo.Collection<BucketStateDocument>;
40
40
  readonly checkpoint_events: mongo.Collection<CheckpointEventDocument>;
41
41
  readonly connection_report_events: mongo.Collection<ClientConnectionDocument>;
42
- readonly sync_report_events: mongo.Collection<any>;
43
- readonly bucket_report_events: mongo.Collection<any>;
44
42
 
45
43
  readonly client: mongo.MongoClient;
46
44
  readonly db: mongo.Db;
@@ -66,8 +64,6 @@ export class PowerSyncMongo {
66
64
  this.bucket_state = this.db.collection('bucket_state');
67
65
  this.checkpoint_events = this.db.collection('checkpoint_events');
68
66
  this.connection_report_events = this.db.collection('connection_report_events');
69
- this.sync_report_events = this.db.collection('sync_report_events');
70
- this.bucket_report_events = this.db.collection('bucket_report_events');
71
67
  }
72
68
 
73
69
  /**
@@ -149,34 +145,6 @@ export class PowerSyncMongo {
149
145
  await this.db.createCollection('connection_report_events');
150
146
  }
151
147
 
152
- /**
153
- * Only use in migrations and tests.
154
- */
155
- async createSyncReportingCollection() {
156
- const existingCollections = await this.db
157
- .listCollections({ name: 'sync_report_events' }, { nameOnly: false })
158
- .toArray();
159
- const collection = existingCollections[0];
160
- if (collection != null) {
161
- return;
162
- }
163
- await this.db.createCollection('sync_report_events');
164
- }
165
-
166
- /**
167
- * Only use in migrations and tests.
168
- */
169
- async createBucketReportingCollection() {
170
- const existingCollections = await this.db
171
- .listCollections({ name: 'bucket_report_events' }, { nameOnly: false })
172
- .toArray();
173
- const collection = existingCollections[0];
174
- if (collection != null) {
175
- return;
176
- }
177
- await this.db.createCollection('bucket_report_events');
178
- }
179
-
180
148
  /**
181
149
  * Only use in migrations and tests.
182
150
  */
@@ -204,6 +204,29 @@ export interface SyncRuleDocument {
204
204
  id: string;
205
205
  expires_at: Date;
206
206
  } | null;
207
+
208
+ storage_version?: number;
209
+ }
210
+
211
+ export interface StorageConfig extends storage.StorageVersionConfig {
212
+ /**
213
+ * When true, bucket_data.checksum is guaranteed to be persisted as a Long.
214
+ *
215
+ * When false, it could also have been persisted as an Int32 or Double, in which case it must be converted to
216
+ * a Long before summing.
217
+ */
218
+ longChecksums: boolean;
219
+ }
220
+
221
+ const LONG_CHECKSUMS_STORAGE_VERSION = 2;
222
+
223
+ export function getMongoStorageConfig(storageVersion: number): StorageConfig | undefined {
224
+ const baseConfig = storage.STORAGE_VERSION_CONFIG[storageVersion];
225
+ if (baseConfig == null) {
226
+ return undefined;
227
+ }
228
+
229
+ return { ...baseConfig, longChecksums: storageVersion >= LONG_CHECKSUMS_STORAGE_VERSION };
207
230
  }
208
231
 
209
232
  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) {
package/src/utils/util.ts CHANGED
@@ -142,6 +142,9 @@ export const createPaginatedConnectionQuery = async <T extends mongo.Document>(
142
142
 
143
143
  const items = await findCursor.limit(limit).toArray();
144
144
  const count = items.length;
145
+ /** The returned total has been defaulted to 0 due to the overhead using documentCount from the mogo driver.
146
+ * cursor.count has been deprecated.
147
+ * */
145
148
  return {
146
149
  items,
147
150
  count,
@@ -150,39 +153,3 @@ export const createPaginatedConnectionQuery = async <T extends mongo.Document>(
150
153
  more: !(count !== limit)
151
154
  };
152
155
  };
153
-
154
- export const createPaginatedSyncCheckpointQuery = async <T extends mongo.Document>(
155
- query: mongo.Filter<T>,
156
- collection: mongo.Collection<T>,
157
- limit: number,
158
- cursor?: string
159
- ) => {
160
- const createQuery = (cursor?: string) => {
161
- if (!cursor) {
162
- return query;
163
- }
164
- const date = { $lt: new Date(cursor), $gte: query.date.$gte };
165
-
166
- return {
167
- ...query,
168
- ...date
169
- } as mongo.Filter<T>;
170
- };
171
-
172
- const findCursor = collection.find(createQuery(cursor), {
173
- sort: {
174
- /** We are sorting by date at date descending to match cursor Postgres implementation */
175
- date: -1
176
- }
177
- });
178
-
179
- const items = await findCursor.limit(limit).toArray();
180
- const count = items.length;
181
- return {
182
- items,
183
- count,
184
- /** Setting the cursor to the connected at date of the last item in the list */
185
- cursor: count === limit ? items[items.length - 1].date.toISOString() : undefined,
186
- more: !(count !== limit)
187
- };
188
- };