@powersync/service-module-mongodb-storage 0.14.0 → 0.15.1

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 (65) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/storage/MongoBucketStorage.js +16 -3
  3. package/dist/storage/MongoBucketStorage.js.map +1 -1
  4. package/dist/storage/implementation/MongoBucketBatch.d.ts +13 -11
  5. package/dist/storage/implementation/MongoBucketBatch.js +208 -127
  6. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  7. package/dist/storage/implementation/MongoChecksums.d.ts +4 -4
  8. package/dist/storage/implementation/MongoChecksums.js +1 -0
  9. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  10. package/dist/storage/implementation/MongoCompactor.d.ts +8 -2
  11. package/dist/storage/implementation/MongoCompactor.js +50 -21
  12. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  13. package/dist/storage/implementation/MongoParameterCompactor.d.ts +2 -2
  14. package/dist/storage/implementation/MongoParameterCompactor.js +13 -1
  15. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  16. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +2 -7
  17. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  18. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +9 -4
  19. package/dist/storage/implementation/MongoSyncBucketStorage.js +35 -33
  20. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  21. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +3 -3
  22. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  23. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +4 -4
  24. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  25. package/dist/storage/implementation/OperationBatch.js +3 -2
  26. package/dist/storage/implementation/OperationBatch.js.map +1 -1
  27. package/dist/storage/implementation/PersistedBatch.d.ts +11 -4
  28. package/dist/storage/implementation/PersistedBatch.js +42 -11
  29. package/dist/storage/implementation/PersistedBatch.js.map +1 -1
  30. package/dist/storage/implementation/db.d.ts +35 -1
  31. package/dist/storage/implementation/db.js +99 -0
  32. package/dist/storage/implementation/db.js.map +1 -1
  33. package/dist/storage/implementation/models.d.ts +15 -3
  34. package/dist/storage/implementation/models.js +2 -1
  35. package/dist/storage/implementation/models.js.map +1 -1
  36. package/dist/utils/test-utils.d.ts +4 -1
  37. package/dist/utils/test-utils.js +15 -12
  38. package/dist/utils/test-utils.js.map +1 -1
  39. package/dist/utils/util.d.ts +2 -1
  40. package/dist/utils/util.js +15 -1
  41. package/dist/utils/util.js.map +1 -1
  42. package/package.json +6 -6
  43. package/src/storage/MongoBucketStorage.ts +29 -8
  44. package/src/storage/implementation/MongoBucketBatch.ts +263 -177
  45. package/src/storage/implementation/MongoChecksums.ts +5 -3
  46. package/src/storage/implementation/MongoCompactor.ts +53 -24
  47. package/src/storage/implementation/MongoParameterCompactor.ts +17 -4
  48. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +3 -11
  49. package/src/storage/implementation/MongoSyncBucketStorage.ts +33 -26
  50. package/src/storage/implementation/MongoSyncRulesLock.ts +3 -3
  51. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -4
  52. package/src/storage/implementation/OperationBatch.ts +3 -2
  53. package/src/storage/implementation/PersistedBatch.ts +42 -11
  54. package/src/storage/implementation/db.ts +129 -1
  55. package/src/storage/implementation/models.ts +18 -4
  56. package/src/utils/test-utils.ts +15 -12
  57. package/src/utils/util.ts +17 -2
  58. package/test/src/__snapshots__/storage.test.ts.snap +201 -0
  59. package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
  60. package/test/src/__snapshots__/storage_sync.test.ts.snap +1111 -16
  61. package/test/src/storage.test.ts +9 -7
  62. package/test/src/storage_compacting.test.ts +117 -45
  63. package/test/src/storage_sync.test.ts +53 -51
  64. package/test/src/util.ts +3 -3
  65. package/tsconfig.tsbuildinfo +1 -1
@@ -10,13 +10,16 @@ import {
10
10
  CheckpointEventDocument,
11
11
  ClientConnectionDocument,
12
12
  CurrentDataDocument,
13
+ CurrentDataDocumentV3,
13
14
  CustomWriteCheckpointDocument,
14
15
  IdSequenceDocument,
15
16
  InstanceDocument,
16
17
  SourceTableDocument,
18
+ StorageConfig,
17
19
  SyncRuleDocument,
18
20
  WriteCheckpointDocument
19
21
  } from './models.js';
22
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
20
23
 
21
24
  export interface PowerSyncMongoOptions {
22
25
  /**
@@ -27,6 +30,7 @@ export interface PowerSyncMongoOptions {
27
30
 
28
31
  export class PowerSyncMongo {
29
32
  readonly current_data: mongo.Collection<CurrentDataDocument>;
33
+ readonly v3_current_data: mongo.Collection<CurrentDataDocumentV3>;
30
34
  readonly bucket_data: mongo.Collection<BucketDataDocument>;
31
35
  readonly bucket_parameters: mongo.Collection<BucketParameterDocument>;
32
36
  readonly op_id_sequence: mongo.Collection<IdSequenceDocument>;
@@ -51,7 +55,8 @@ export class PowerSyncMongo {
51
55
  });
52
56
  this.db = db;
53
57
 
54
- this.current_data = db.collection<CurrentDataDocument>('current_data');
58
+ this.current_data = db.collection('current_data');
59
+ this.v3_current_data = db.collection('v3_current_data');
55
60
  this.bucket_data = db.collection('bucket_data');
56
61
  this.bucket_parameters = db.collection('bucket_parameters');
57
62
  this.op_id_sequence = db.collection('op_id_sequence');
@@ -66,11 +71,16 @@ export class PowerSyncMongo {
66
71
  this.connection_report_events = this.db.collection('connection_report_events');
67
72
  }
68
73
 
74
+ versioned(storageConfig: StorageConfig) {
75
+ return new VersionedPowerSyncMongo(this, storageConfig);
76
+ }
77
+
69
78
  /**
70
79
  * Clear all collections.
71
80
  */
72
81
  async clear() {
73
82
  await this.current_data.deleteMany({});
83
+ await this.v3_current_data.deleteMany({});
74
84
  await this.bucket_data.deleteMany({});
75
85
  await this.bucket_parameters.deleteMany({});
76
86
  await this.op_id_sequence.deleteMany({});
@@ -171,6 +181,124 @@ export class PowerSyncMongo {
171
181
  { name: 'dirty_count' }
172
182
  );
173
183
  }
184
+
185
+ async initializeStorageVersion(storageConfig: StorageConfig) {
186
+ if (storageConfig.softDeleteCurrentData) {
187
+ // Initialize the v3_current_data collection, which is used for the new storage version.
188
+ // No-op if this already exists
189
+ await this.v3_current_data.createIndex(
190
+ {
191
+ '_id.g': 1,
192
+ pending_delete: 1
193
+ },
194
+ {
195
+ partialFilterExpression: { pending_delete: { $exists: true } },
196
+ name: 'pending_delete'
197
+ }
198
+ );
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * This is similar to PowerSyncMongo, but blocks access to certain collections based on the storage version.
205
+ */
206
+ export class VersionedPowerSyncMongo {
207
+ readonly client: mongo.MongoClient;
208
+ readonly db: mongo.Db;
209
+
210
+ readonly storageConfig: StorageConfig;
211
+ #upstream: PowerSyncMongo;
212
+
213
+ constructor(upstream: PowerSyncMongo, storageConfig: StorageConfig) {
214
+ this.#upstream = upstream;
215
+ this.client = upstream.client;
216
+ this.db = upstream.db;
217
+ this.storageConfig = storageConfig;
218
+ }
219
+
220
+ /**
221
+ * Uses either `current_data` or `v3_current_data` collection based on the storage version.
222
+ *
223
+ * Use in places where it does not matter which version is used.
224
+ */
225
+ get common_current_data(): mongo.Collection<CurrentDataDocument> {
226
+ if (this.storageConfig.softDeleteCurrentData) {
227
+ return this.#upstream.v3_current_data;
228
+ } else {
229
+ return this.#upstream.current_data;
230
+ }
231
+ }
232
+
233
+ get v1_current_data() {
234
+ if (this.storageConfig.softDeleteCurrentData) {
235
+ throw new ServiceAssertionError(
236
+ 'current_data collection should not be used when softDeleteCurrentData is enabled'
237
+ );
238
+ }
239
+ return this.#upstream.current_data;
240
+ }
241
+
242
+ get v3_current_data() {
243
+ if (!this.storageConfig.softDeleteCurrentData) {
244
+ throw new ServiceAssertionError(
245
+ 'v3_current_data collection should not be used when softDeleteCurrentData is disabled'
246
+ );
247
+ }
248
+ return this.#upstream.v3_current_data;
249
+ }
250
+
251
+ get bucket_data() {
252
+ return this.#upstream.bucket_data;
253
+ }
254
+
255
+ get bucket_parameters() {
256
+ return this.#upstream.bucket_parameters;
257
+ }
258
+
259
+ get op_id_sequence() {
260
+ return this.#upstream.op_id_sequence;
261
+ }
262
+
263
+ get sync_rules() {
264
+ return this.#upstream.sync_rules;
265
+ }
266
+
267
+ get source_tables() {
268
+ return this.#upstream.source_tables;
269
+ }
270
+
271
+ get custom_write_checkpoints() {
272
+ return this.#upstream.custom_write_checkpoints;
273
+ }
274
+
275
+ get write_checkpoints() {
276
+ return this.#upstream.write_checkpoints;
277
+ }
278
+
279
+ get instance() {
280
+ return this.#upstream.instance;
281
+ }
282
+
283
+ get locks() {
284
+ return this.#upstream.locks;
285
+ }
286
+
287
+ get bucket_state() {
288
+ return this.#upstream.bucket_state;
289
+ }
290
+
291
+ get checkpoint_events() {
292
+ return this.#upstream.checkpoint_events;
293
+ }
294
+
295
+ get connection_report_events() {
296
+ return this.#upstream.connection_report_events;
297
+ }
298
+
299
+ notifyCheckpoint() {
300
+ return this.#upstream.notifyCheckpoint();
301
+ }
174
302
  }
175
303
 
176
304
  export function createPowerSyncMongo(config: MongoStorageConfig, options?: lib_mongo.MongoConnectionOptions) {
@@ -2,6 +2,7 @@ import { InternalOpId, SerializedSyncPlan, storage } from '@powersync/service-co
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';
5
+ import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
5
6
 
6
7
  /**
7
8
  * Replica id uniquely identifying a row on the source database.
@@ -37,6 +38,19 @@ export interface CurrentDataDocument {
37
38
  lookups: bson.Binary[];
38
39
  }
39
40
 
41
+ export interface CurrentDataDocumentV3 {
42
+ _id: SourceKey;
43
+ data: bson.Binary;
44
+ buckets: CurrentBucket[];
45
+ lookups: bson.Binary[];
46
+ /**
47
+ * If set, this can be deleted, once there is a consistent checkpoint >= pending_delete.
48
+ *
49
+ * This must only be set if buckets = [], lookups = [].
50
+ */
51
+ pending_delete?: bigint;
52
+ }
53
+
40
54
  export interface CurrentBucket {
41
55
  bucket: string;
42
56
  table: string;
@@ -111,12 +125,12 @@ export interface BucketStateDocument {
111
125
  op_id: InternalOpId;
112
126
  count: number;
113
127
  checksum: bigint;
114
- bytes: number | null;
128
+ bytes: number | bigint | null;
115
129
  };
116
130
 
117
131
  estimate_since_compact?: {
118
132
  count: number;
119
- bytes: number;
133
+ bytes: number | bigint;
120
134
  };
121
135
  }
122
136
 
@@ -221,10 +235,10 @@ export interface StorageConfig extends storage.StorageVersionConfig {
221
235
 
222
236
  const LONG_CHECKSUMS_STORAGE_VERSION = 2;
223
237
 
224
- export function getMongoStorageConfig(storageVersion: number): StorageConfig | undefined {
238
+ export function getMongoStorageConfig(storageVersion: number): StorageConfig {
225
239
  const baseConfig = storage.STORAGE_VERSION_CONFIG[storageVersion];
226
240
  if (baseConfig == null) {
227
- return undefined;
241
+ throw new ServiceError(ErrorCode.PSYNC_S1005, `Unsupported storage version ${storageVersion}`);
228
242
  }
229
243
 
230
244
  return { ...baseConfig, longChecksums: storageVersion >= LONG_CHECKSUMS_STORAGE_VERSION };
@@ -11,22 +11,25 @@ export type MongoTestStorageOptions = {
11
11
  };
12
12
 
13
13
  export function mongoTestStorageFactoryGenerator(factoryOptions: MongoTestStorageOptions) {
14
- return async (options?: TestStorageOptions) => {
15
- const db = connectMongoForTests(factoryOptions.url, factoryOptions.isCI);
14
+ return {
15
+ factory: async (options?: TestStorageOptions) => {
16
+ const db = connectMongoForTests(factoryOptions.url, factoryOptions.isCI);
16
17
 
17
- // None of the tests insert data into this collection, so it was never created
18
- if (!(await db.db.listCollections({ name: db.bucket_parameters.collectionName }).hasNext())) {
19
- await db.db.createCollection('bucket_parameters');
20
- }
18
+ // None of the tests insert data into this collection, so it was never created
19
+ if (!(await db.db.listCollections({ name: db.bucket_parameters.collectionName }).hasNext())) {
20
+ await db.db.createCollection('bucket_parameters');
21
+ }
21
22
 
22
- // Full migrations are not currently run for tests, so we manually create this
23
- await db.createCheckpointEventsCollection();
23
+ // Full migrations are not currently run for tests, so we manually create this
24
+ await db.createCheckpointEventsCollection();
24
25
 
25
- if (!options?.doNotClear) {
26
- await db.clear();
27
- }
26
+ if (!options?.doNotClear) {
27
+ await db.clear();
28
+ }
28
29
 
29
- return new MongoBucketStorage(db, { slot_name_prefix: 'test_' }, factoryOptions.internalOptions);
30
+ return new MongoBucketStorage(db, { slot_name_prefix: 'test_' }, factoryOptions.internalOptions);
31
+ },
32
+ tableIdStrings: false
30
33
  };
31
34
  }
32
35
 
package/src/utils/util.ts CHANGED
@@ -91,10 +91,10 @@ export function mapOpEntry(row: BucketDataDocument): utils.OplogEntry {
91
91
  }
92
92
  }
93
93
 
94
- export function replicaIdToSubkey(table: bson.ObjectId, id: storage.ReplicaId): string {
94
+ export function replicaIdToSubkey(table: storage.SourceTableId, id: storage.ReplicaId): string {
95
95
  if (storage.isUUID(id)) {
96
96
  // Special case for UUID for backwards-compatiblity
97
- return `${table.toHexString()}/${id.toHexString()}`;
97
+ return `${tableIdString(table)}/${id.toHexString()}`;
98
98
  } else {
99
99
  // Hashed UUID from the table and id
100
100
  const repr = bson.serialize({ table, id });
@@ -102,6 +102,21 @@ export function replicaIdToSubkey(table: bson.ObjectId, id: storage.ReplicaId):
102
102
  }
103
103
  }
104
104
 
105
+ export function mongoTableId(table: storage.SourceTableId): bson.ObjectId {
106
+ if (typeof table == 'string') {
107
+ throw new ServiceAssertionError(`Got string table id, expected ObjectId`);
108
+ }
109
+ return table;
110
+ }
111
+
112
+ function tableIdString(table: storage.SourceTableId) {
113
+ if (typeof table == 'string') {
114
+ return table;
115
+ } else {
116
+ return table.toHexString();
117
+ }
118
+ }
119
+
105
120
  export function setSessionSnapshotTime(session: mongo.ClientSession, time: bson.Timestamp) {
106
121
  // This is a workaround for the lack of direct support for snapshot reads in the MongoDB driver.
107
122
  if (!session.snapshotEnabled) {
@@ -0,0 +1,201 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Mongo Sync Bucket Storage - Data > (insert, delete, insert), (delete) 1`] = `
4
+ [
5
+ {
6
+ "checksum": 2871785649,
7
+ "object_id": "test1",
8
+ "op": "PUT",
9
+ },
10
+ {
11
+ "checksum": 2872534815,
12
+ "object_id": "test1",
13
+ "op": "REMOVE",
14
+ },
15
+ {
16
+ "checksum": 2871785649,
17
+ "object_id": "test1",
18
+ "op": "PUT",
19
+ },
20
+ {
21
+ "checksum": 2872534815,
22
+ "object_id": "test1",
23
+ "op": "REMOVE",
24
+ },
25
+ ]
26
+ `;
27
+
28
+ exports[`Mongo Sync Bucket Storage - Data > (insert, delete, insert), (delete) 2`] = `
29
+ [
30
+ {
31
+ "checksum": 2871785649,
32
+ "object_id": "test1",
33
+ "op": "PUT",
34
+ },
35
+ {
36
+ "checksum": 2872534815,
37
+ "object_id": "test1",
38
+ "op": "REMOVE",
39
+ },
40
+ {
41
+ "checksum": 2871785649,
42
+ "object_id": "test1",
43
+ "op": "PUT",
44
+ },
45
+ {
46
+ "checksum": 2872534815,
47
+ "object_id": "test1",
48
+ "op": "REMOVE",
49
+ },
50
+ ]
51
+ `;
52
+
53
+ exports[`Mongo Sync Bucket Storage - Data > (insert, delete, insert), (delete) 3`] = `
54
+ [
55
+ {
56
+ "checksum": 2871785649,
57
+ "object_id": "test1",
58
+ "op": "PUT",
59
+ },
60
+ {
61
+ "checksum": 2872534815,
62
+ "object_id": "test1",
63
+ "op": "REMOVE",
64
+ },
65
+ {
66
+ "checksum": 2871785649,
67
+ "object_id": "test1",
68
+ "op": "PUT",
69
+ },
70
+ {
71
+ "checksum": 2872534815,
72
+ "object_id": "test1",
73
+ "op": "REMOVE",
74
+ },
75
+ ]
76
+ `;
77
+
78
+ exports[`Mongo Sync Bucket Storage - Data - v1 > (insert, delete, insert), (delete) 1`] = `
79
+ [
80
+ {
81
+ "checksum": 2871785649,
82
+ "object_id": "test1",
83
+ "op": "PUT",
84
+ },
85
+ {
86
+ "checksum": 2872534815,
87
+ "object_id": "test1",
88
+ "op": "REMOVE",
89
+ },
90
+ {
91
+ "checksum": 2871785649,
92
+ "object_id": "test1",
93
+ "op": "PUT",
94
+ },
95
+ {
96
+ "checksum": 2872534815,
97
+ "object_id": "test1",
98
+ "op": "REMOVE",
99
+ },
100
+ ]
101
+ `;
102
+
103
+ exports[`Mongo Sync Bucket Storage - Data - v2 > (insert, delete, insert), (delete) 1`] = `
104
+ [
105
+ {
106
+ "checksum": 2871785649,
107
+ "object_id": "test1",
108
+ "op": "PUT",
109
+ },
110
+ {
111
+ "checksum": 2872534815,
112
+ "object_id": "test1",
113
+ "op": "REMOVE",
114
+ },
115
+ {
116
+ "checksum": 2871785649,
117
+ "object_id": "test1",
118
+ "op": "PUT",
119
+ },
120
+ {
121
+ "checksum": 2872534815,
122
+ "object_id": "test1",
123
+ "op": "REMOVE",
124
+ },
125
+ ]
126
+ `;
127
+
128
+ exports[`Mongo Sync Bucket Storage - Data - v3 > (insert, delete, insert), (delete) 1`] = `
129
+ [
130
+ {
131
+ "checksum": 2871785649,
132
+ "object_id": "test1",
133
+ "op": "PUT",
134
+ },
135
+ {
136
+ "checksum": 2872534815,
137
+ "object_id": "test1",
138
+ "op": "REMOVE",
139
+ },
140
+ {
141
+ "checksum": 2871785649,
142
+ "object_id": "test1",
143
+ "op": "PUT",
144
+ },
145
+ {
146
+ "checksum": 2872534815,
147
+ "object_id": "test1",
148
+ "op": "REMOVE",
149
+ },
150
+ ]
151
+ `;
152
+
153
+ exports[`Mongo Sync Bucket Storage - split buckets > (insert, delete, insert), (delete) 1`] = `
154
+ [
155
+ {
156
+ "checksum": 2871785649,
157
+ "object_id": "test1",
158
+ "op": "PUT",
159
+ },
160
+ {
161
+ "checksum": 2872534815,
162
+ "object_id": "test1",
163
+ "op": "REMOVE",
164
+ },
165
+ {
166
+ "checksum": 2871785649,
167
+ "object_id": "test1",
168
+ "op": "PUT",
169
+ },
170
+ {
171
+ "checksum": 2872534815,
172
+ "object_id": "test1",
173
+ "op": "REMOVE",
174
+ },
175
+ ]
176
+ `;
177
+
178
+ exports[`Mongo Sync Bucket Storage - split operations > (insert, delete, insert), (delete) 1`] = `
179
+ [
180
+ {
181
+ "checksum": 2871785649,
182
+ "object_id": "test1",
183
+ "op": "PUT",
184
+ },
185
+ {
186
+ "checksum": 2872534815,
187
+ "object_id": "test1",
188
+ "op": "REMOVE",
189
+ },
190
+ {
191
+ "checksum": 2871785649,
192
+ "object_id": "test1",
193
+ "op": "PUT",
194
+ },
195
+ {
196
+ "checksum": 2872534815,
197
+ "object_id": "test1",
198
+ "op": "REMOVE",
199
+ },
200
+ ]
201
+ `;
@@ -0,0 +1,17 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Mongo Sync Bucket Storage Compact > partial checksums after compacting (2) 1`] = `
4
+ {
5
+ "bucket": "1#global[]",
6
+ "checksum": -1481659821,
7
+ "count": 1,
8
+ }
9
+ `;
10
+
11
+ exports[`Mongo Sync Bucket Storage Compact > partial checksums after compacting 1`] = `
12
+ {
13
+ "bucket": "1#global[]",
14
+ "checksum": 1874612650,
15
+ "count": 4,
16
+ }
17
+ `;