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

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 (46) hide show
  1. package/CHANGELOG.md +56 -8
  2. package/dist/migrations/MongoMigrationAgent.js +3 -0
  3. package/dist/migrations/MongoMigrationAgent.js.map +1 -1
  4. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +2 -1
  5. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
  6. package/dist/storage/MongoBucketStorage.d.ts +2 -3
  7. package/dist/storage/MongoBucketStorage.js +61 -39
  8. package/dist/storage/MongoBucketStorage.js.map +1 -1
  9. package/dist/storage/implementation/MongoBucketBatch.d.ts +1 -1
  10. package/dist/storage/implementation/MongoBucketBatch.js +37 -24
  11. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  12. package/dist/storage/implementation/MongoCompactor.js +12 -4
  13. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoIdSequence.js +3 -1
  15. package/dist/storage/implementation/MongoIdSequence.js.map +1 -1
  16. package/dist/storage/implementation/MongoPersistedSyncRules.js +4 -0
  17. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -1
  18. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +9 -1
  19. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  20. package/dist/storage/implementation/MongoStorageProvider.js +2 -2
  21. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  22. package/dist/storage/implementation/MongoSyncBucketStorage.js +16 -9
  23. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  24. package/dist/storage/implementation/MongoSyncRulesLock.js +7 -3
  25. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  26. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +2 -0
  27. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  28. package/dist/storage/implementation/OperationBatch.js +10 -6
  29. package/dist/storage/implementation/OperationBatch.js.map +1 -1
  30. package/dist/storage/implementation/PersistedBatch.js +14 -13
  31. package/dist/storage/implementation/PersistedBatch.js.map +1 -1
  32. package/dist/storage/implementation/db.js +12 -0
  33. package/dist/storage/implementation/db.js.map +1 -1
  34. package/dist/storage/implementation/util.js +3 -3
  35. package/dist/storage/implementation/util.js.map +1 -1
  36. package/package.json +7 -7
  37. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +2 -1
  38. package/src/storage/MongoBucketStorage.ts +39 -18
  39. package/src/storage/implementation/MongoBucketBatch.ts +20 -6
  40. package/src/storage/implementation/MongoCompactor.ts +4 -2
  41. package/src/storage/implementation/MongoIdSequence.ts +3 -1
  42. package/src/storage/implementation/MongoStorageProvider.ts +4 -2
  43. package/src/storage/implementation/MongoSyncBucketStorage.ts +3 -3
  44. package/src/storage/implementation/MongoSyncRulesLock.ts +5 -2
  45. package/test/src/storage_compacting.test.ts +6 -1
  46. package/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@powersync/service-module-mongodb-storage",
3
3
  "repository": "https://github.com/powersync-ja/powersync-service",
4
4
  "types": "dist/index.d.ts",
5
- "version": "0.0.0-dev-20250117095455",
5
+ "version": "0.0.0-dev-20250214100224",
6
6
  "main": "dist/index.js",
7
7
  "license": "FSL-1.1-Apache-2.0",
8
8
  "type": "module",
@@ -27,16 +27,16 @@
27
27
  "ix": "^5.0.0",
28
28
  "lru-cache": "^10.2.2",
29
29
  "uuid": "^9.0.1",
30
- "@powersync/lib-services-framework": "0.0.0-dev-20250117095455",
31
- "@powersync/service-core": "0.0.0-dev-20250117095455",
30
+ "@powersync/lib-services-framework": "0.5.1",
31
+ "@powersync/service-core": "0.0.0-dev-20250214100224",
32
32
  "@powersync/service-jsonbig": "0.17.10",
33
- "@powersync/service-sync-rules": "0.23.1",
34
- "@powersync/service-types": "0.0.0-dev-20250117095455",
35
- "@powersync/lib-service-mongodb": "0.0.0-dev-20250117095455"
33
+ "@powersync/service-sync-rules": "0.23.4",
34
+ "@powersync/service-types": "0.8.0",
35
+ "@powersync/lib-service-mongodb": "0.4.1"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/uuid": "^9.0.4",
39
- "@powersync/service-core-tests": "0.0.0-dev-20250117095455"
39
+ "@powersync/service-core-tests": "0.0.0-dev-20250214100224"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc -b",
@@ -2,6 +2,7 @@ import * as lib_mongo from '@powersync/lib-service-mongodb';
2
2
  import { storage as core_storage, migrations } from '@powersync/service-core';
3
3
  import * as storage from '../../../storage/storage-index.js';
4
4
  import { MongoStorageConfig } from '../../../types/types.js';
5
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
5
6
 
6
7
  interface LegacySyncRulesDocument extends storage.SyncRuleDocument {
7
8
  /**
@@ -65,7 +66,7 @@ export const up: migrations.PowerSyncMigrationFunction = async (context) => {
65
66
  const remaining = await db.sync_rules.find({ state: null as any }).toArray();
66
67
  if (remaining.length > 0) {
67
68
  const slots = remaining.map((doc) => doc.slot_name).join(', ');
68
- throw new Error(`Invalid state for sync rules: ${slots}`);
69
+ throw new ServiceAssertionError(`Invalid state for sync rules: ${slots}`);
69
70
  }
70
71
  } finally {
71
72
  await db.client.close();
@@ -5,7 +5,7 @@ import * as timers from 'timers/promises';
5
5
 
6
6
  import { storage, sync, utils } from '@powersync/service-core';
7
7
 
8
- import { DisposableObserver, logger } from '@powersync/lib-services-framework';
8
+ import { DisposableObserver, ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
9
9
  import { v4 as uuid } from 'uuid';
10
10
 
11
11
  import * as lib_mongo from '@powersync/lib-service-mongodb';
@@ -84,22 +84,36 @@ export class MongoBucketStorage
84
84
  return storage;
85
85
  }
86
86
 
87
- async configureSyncRules(sync_rules: string, options?: { lock?: boolean }) {
87
+ async getSystemIdentifier(): Promise<storage.BucketStorageSystemIdentifier> {
88
+ const { setName: id } = await this.db.db.command({
89
+ hello: 1
90
+ });
91
+ if (id == null) {
92
+ throw new ServiceError(
93
+ ErrorCode.PSYNC_S1342,
94
+ 'Standalone MongoDB instances are not supported - use a replicaset.'
95
+ );
96
+ }
97
+
98
+ return {
99
+ id,
100
+ type: lib_mongo.MONGO_CONNECTION_TYPE
101
+ };
102
+ }
103
+
104
+ async configureSyncRules(options: storage.UpdateSyncRulesOptions) {
88
105
  const next = await this.getNextSyncRulesContent();
89
106
  const active = await this.getActiveSyncRulesContent();
90
107
 
91
- if (next?.sync_rules_content == sync_rules) {
108
+ if (next?.sync_rules_content == options.content) {
92
109
  logger.info('Sync rules from configuration unchanged');
93
110
  return { updated: false };
94
- } else if (next == null && active?.sync_rules_content == sync_rules) {
111
+ } else if (next == null && active?.sync_rules_content == options.content) {
95
112
  logger.info('Sync rules from configuration unchanged');
96
113
  return { updated: false };
97
114
  } else {
98
115
  logger.info('Sync rules updated from configuration');
99
- const persisted_sync_rules = await this.updateSyncRules({
100
- content: sync_rules,
101
- lock: options?.lock
102
- });
116
+ const persisted_sync_rules = await this.updateSyncRules(options);
103
117
  return { updated: true, persisted_sync_rules, lock: persisted_sync_rules.current_lock ?? undefined };
104
118
  }
105
119
  }
@@ -113,7 +127,8 @@ export class MongoBucketStorage
113
127
  if (next != null && next.slot_name == slot_name) {
114
128
  // We need to redo the "next" sync rules
115
129
  await this.updateSyncRules({
116
- content: next.sync_rules_content
130
+ content: next.sync_rules_content,
131
+ validate: false
117
132
  });
118
133
  // Pro-actively stop replicating
119
134
  await this.db.sync_rules.updateOne(
@@ -130,7 +145,8 @@ export class MongoBucketStorage
130
145
  } else if (next == null && active?.slot_name == slot_name) {
131
146
  // Slot removed for "active" sync rules, while there is no "next" one.
132
147
  await this.updateSyncRules({
133
- content: active.sync_rules_content
148
+ content: active.sync_rules_content,
149
+ validate: false
134
150
  });
135
151
 
136
152
  // Pro-actively stop replicating
@@ -149,13 +165,18 @@ export class MongoBucketStorage
149
165
  }
150
166
 
151
167
  async updateSyncRules(options: storage.UpdateSyncRulesOptions): Promise<MongoPersistedSyncRulesContent> {
152
- // Parse and validate before applying any changes
153
- const parsed = SqlSyncRules.fromYaml(options.content, {
154
- // No schema-based validation at this point
155
- schema: undefined,
156
- defaultSchema: 'not_applicable', // Not needed for validation
157
- throwOnError: true
158
- });
168
+ if (options.validate) {
169
+ // Parse and validate before applying any changes
170
+ SqlSyncRules.fromYaml(options.content, {
171
+ // No schema-based validation at this point
172
+ schema: undefined,
173
+ defaultSchema: 'not_applicable', // Not needed for validation
174
+ throwOnError: true
175
+ });
176
+ } else {
177
+ // We do not validate sync rules at this point.
178
+ // That is done when using the sync rules, so that the diagnostics API can report the errors.
179
+ }
159
180
 
160
181
  let rules: MongoPersistedSyncRulesContent | undefined = undefined;
161
182
 
@@ -433,7 +454,7 @@ export class MongoBucketStorage
433
454
  clusterTime = time;
434
455
  });
435
456
  if (clusterTime == null) {
436
- throw new Error('Could not get clusterTime');
457
+ throw new ServiceError(ErrorCode.PSYNC_S2401, 'Could not get clusterTime');
437
458
  }
438
459
 
439
460
  if (signal.aborted) {
@@ -2,7 +2,15 @@ import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import { SqlEventDescriptor, SqliteRow, SqlSyncRules } from '@powersync/service-sync-rules';
3
3
  import * as bson from 'bson';
4
4
 
5
- import { container, DisposableObserver, errors, logger } from '@powersync/lib-services-framework';
5
+ import {
6
+ container,
7
+ DisposableObserver,
8
+ ErrorCode,
9
+ errors,
10
+ logger,
11
+ ReplicationAssertionError,
12
+ ServiceError
13
+ } from '@powersync/lib-services-framework';
6
14
  import { SaveOperationTag, storage, utils } from '@powersync/service-core';
7
15
  import * as timers from 'node:timers/promises';
8
16
  import { PowerSyncMongo } from './db.js';
@@ -140,7 +148,7 @@ export class MongoBucketBatch
140
148
  this.batch = resumeBatch;
141
149
 
142
150
  if (last_op == null) {
143
- throw new Error('Unexpected last_op == null');
151
+ throw new ReplicationAssertionError('Unexpected last_op == null');
144
152
  }
145
153
 
146
154
  this.persisted_op = last_op;
@@ -294,7 +302,7 @@ export class MongoBucketBatch
294
302
  return null;
295
303
  }
296
304
  } else {
297
- throw new Error(`${record.tag} not supported with skipExistingRows: true`);
305
+ throw new ReplicationAssertionError(`${record.tag} not supported with skipExistingRows: true`);
298
306
  }
299
307
  }
300
308
 
@@ -348,7 +356,7 @@ export class MongoBucketBatch
348
356
  afterData = new bson.Binary(bson.serialize(after!));
349
357
  // We additionally make sure it's <= 15MB - we need some margin for metadata.
350
358
  if (afterData.length() > MAX_ROW_SIZE) {
351
- throw new Error(`Row too large: ${afterData.length()}`);
359
+ throw new ServiceError(ErrorCode.PSYNC_S1002, `Row too large: ${afterData.length()}`);
352
360
  }
353
361
  } catch (e) {
354
362
  // Replace with empty values, equivalent to TOAST values
@@ -548,7 +556,7 @@ export class MongoBucketBatch
548
556
  logger.info(`${this.slot_name} ${description} - try ${flushTry}`);
549
557
  }
550
558
  if (flushTry > 20 && Date.now() > lastTry) {
551
- throw new Error('Max transaction tries exceeded');
559
+ throw new ServiceError(ErrorCode.PSYNC_S1402, 'Max transaction tries exceeded');
552
560
  }
553
561
 
554
562
  const next_op_id_doc = await this.db.op_id_sequence.findOneAndUpdate(
@@ -607,7 +615,9 @@ export class MongoBucketBatch
607
615
 
608
616
  private lastWaitingLogThottled = 0;
609
617
 
610
- async commit(lsn: string): Promise<boolean> {
618
+ async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<boolean> {
619
+ const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
620
+
611
621
  await this.flush();
612
622
 
613
623
  if (this.last_checkpoint_lsn != null && lsn < this.last_checkpoint_lsn) {
@@ -645,6 +655,10 @@ export class MongoBucketBatch
645
655
  return false;
646
656
  }
647
657
 
658
+ if (!createEmptyCheckpoints && this.persisted_op == null) {
659
+ return false;
660
+ }
661
+
648
662
  const now = new Date();
649
663
  const update: Partial<SyncRuleDocument> = {
650
664
  last_checkpoint_lsn: lsn,
@@ -1,5 +1,5 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { logger } from '@powersync/lib-services-framework';
2
+ import { logger, ReplicationAssertionError } from '@powersync/lib-services-framework';
3
3
  import { storage, utils } from '@powersync/service-core';
4
4
 
5
5
  import { PowerSyncMongo } from './db.js';
@@ -335,7 +335,9 @@ export class MongoCompactor {
335
335
  }
336
336
  }
337
337
  } else {
338
- throw new Error(`Unexpected ${op.op} operation at ${op._id.g}:${op._id.b}:${op._id.o}`);
338
+ throw new ReplicationAssertionError(
339
+ `Unexpected ${op.op} operation at ${op._id.g}:${op._id.b}:${op._id.o}`
340
+ );
339
341
  }
340
342
  }
341
343
  if (!gotAnOp) {
@@ -1,3 +1,5 @@
1
+ import { ReplicationAssertionError } from '@powersync/lib-services-framework';
2
+
1
3
  /**
2
4
  * Manages op_id or similar sequence in memory.
3
5
  *
@@ -9,7 +11,7 @@ export class MongoIdSequence {
9
11
 
10
12
  constructor(last: bigint) {
11
13
  if (typeof last != 'bigint') {
12
- throw new Error(`BigInt required, got ${last} ${typeof last}`);
14
+ throw new ReplicationAssertionError(`BigInt required, got ${last} ${typeof last}`);
13
15
  }
14
16
  this._last = last;
15
17
  }
@@ -1,5 +1,5 @@
1
1
  import * as lib_mongo from '@powersync/lib-service-mongodb';
2
- import { logger } from '@powersync/lib-services-framework';
2
+ import { logger, ServiceAssertionError } from '@powersync/lib-services-framework';
3
3
  import { storage } from '@powersync/service-core';
4
4
  import { MongoStorageConfig } from '../../types/types.js';
5
5
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
@@ -16,7 +16,9 @@ export class MongoStorageProvider implements storage.BucketStorageProvider {
16
16
  const { storage } = resolvedConfig;
17
17
  if (storage.type != this.type) {
18
18
  // This should not be reached since the generation should be managed externally.
19
- throw new Error(`Cannot create MongoDB bucket storage with provided config ${storage.type} !== ${this.type}`);
19
+ throw new ServiceAssertionError(
20
+ `Cannot create MongoDB bucket storage with provided config ${storage.type} !== ${this.type}`
21
+ );
20
22
  }
21
23
 
22
24
  const decodedConfig = MongoStorageConfig.decode(storage as any);
@@ -1,6 +1,6 @@
1
1
  import * as lib_mongo from '@powersync/lib-service-mongodb';
2
2
  import { mongo } from '@powersync/lib-service-mongodb';
3
- import { DisposableObserver, logger } from '@powersync/lib-services-framework';
3
+ import { DisposableObserver, logger, ServiceAssertionError } from '@powersync/lib-services-framework';
4
4
  import { storage, utils } from '@powersync/service-core';
5
5
  import { SqliteJsonRow, SqliteJsonValue, SqlSyncRules } from '@powersync/service-sync-rules';
6
6
  import * as bson from 'bson';
@@ -344,7 +344,7 @@ export class MongoSyncBucketStorage
344
344
 
345
345
  start ??= dataBuckets.get(bucket);
346
346
  if (start == null) {
347
- throw new Error(`data for unexpected bucket: ${bucket}`);
347
+ throw new ServiceAssertionError(`data for unexpected bucket: ${bucket}`);
348
348
  }
349
349
  currentBatch = {
350
350
  bucket,
@@ -479,7 +479,7 @@ export class MongoSyncBucketStorage
479
479
  }
480
480
  );
481
481
  if (doc == null) {
482
- throw new Error('Cannot find sync rules status');
482
+ throw new ServiceAssertionError('Cannot find sync rules status');
483
483
  }
484
484
 
485
485
  return {
@@ -1,6 +1,6 @@
1
1
  import crypto from 'crypto';
2
2
 
3
- import { logger } from '@powersync/lib-services-framework';
3
+ import { ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
4
4
  import { storage } from '@powersync/service-core';
5
5
  import { PowerSyncMongo } from './db.js';
6
6
 
@@ -33,7 +33,10 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
33
33
  );
34
34
 
35
35
  if (doc == null) {
36
- throw new Error(`Sync rules: ${sync_rules.id} have been locked by another process for replication.`);
36
+ throw new ServiceError(
37
+ ErrorCode.PSYNC_S1003,
38
+ `Sync rules: ${sync_rules.id} have been locked by another process for replication.`
39
+ );
37
40
  }
38
41
  return new MongoSyncRulesLock(db, sync_rules.id, lockId);
39
42
  }
@@ -3,4 +3,9 @@ import { register } from '@powersync/service-core-tests';
3
3
  import { describe } from 'vitest';
4
4
  import { INITIALIZED_MONGO_STORAGE_FACTORY } from './util.js';
5
5
 
6
- describe('Mongo Sync Bucket Storage Compact', () => register.registerCompactTests<MongoCompactOptions>(INITIALIZED_MONGO_STORAGE_FACTORY, { clearBatchLimit: 2, moveBatchLimit: 1, moveBatchQueryLimit: 1 }));
6
+ describe('Mongo Sync Bucket Storage Compact', () =>
7
+ register.registerCompactTests<MongoCompactOptions>(INITIALIZED_MONGO_STORAGE_FACTORY, {
8
+ clearBatchLimit: 2,
9
+ moveBatchLimit: 1,
10
+ moveBatchQueryLimit: 1
11
+ }));